DEV Community

crowdozer
crowdozer

Posted on

Reducing component complexity with React Hooks

This is my first post here, so go easy on me 😅


Essentially, a hook is just a javascript function prefixed with use which is allowed to invoke other hooks.1 Within reason, you can make a custom hook do whatever you want. Network requests, state management, redirecting the user, invoking other hooks, etc. You can get really creative with them.

Over the past few months, one of my favorite ways to use hooks has been to abstract out complicated component logic.

What do I mean by this?

Imagine you have a Newsfeed component. It loads and renders posts from your api. The simplest implementation is to inline everything at the top of your component:

// Newsfeed.jsx
import React, { useState } from 'react'

const Newsfeed = () => {
  const [loading, setLoading] = useState(false)
  const [posts, setPosts] = useState([])

  const getPosts = () => {
    setLoading(true)
    fetch('/v1/posts')
      .then(data => data.json())
      .then(data => setPosts(data))
      .then(() => setLoading(false))
  }

  return (
    <div>
      {posts.map((post, index) => (
        <div key={index}>
          <h1>{post.title}</h1>
        </div>
      ))}
      {loading ? (
        <p>Loading...</p>
      ) : (
        <button onClick={getPosts}>Load Posts</button>
      )}
    </div>
  )
}

export default Newsfeed
Enter fullscreen mode Exit fullscreen mode

The problem

The above implementation works, but I want you to imagine for a moment that suddenly instead of two states to manage, we have 5. We've got loading, posts, page, perhaps we've got open to track whether or not the user clicked into a post, and even replying to track if they're replying to that post. Instead of just getPosts, we have a bunch of different functions. Say, getPost, getPostComments, postComment, etc.

That's a lot of complexity to add to the top of a functional component. It's an unmaintainable amount of complexity. That isn't even considering how the render portion of that component will grow in complexity, too. Sometimes you can break one complex component into many smaller components, but you can't always cleanly separate the logic.

A solution

Just don't include the functional complexity in the component.

Take all of it, and isolate it into a hook.

Why a hook? Because the integration is easy and seamless. No need to set up reducers or contexts. You get lots of bonuses like being able to call other hooks or automatic rerenders when your state updates.

The most important concept is that our hook, when used like this, must return everything required for the component to render. You can almost think of the return value of the hook as props being passed to the component.2

Let's see what it looks like after we import the hook, destructure the values returned, and ctrl+x and ctrl+v all of the logic away.

// Newsfeed.jsx 
import React from 'react'
import useNewsfeed from './useNewsfeed'

const Newsfeed = () => {
  // Destructure the value that the hook returns 
  const {
    state: { loading, posts },
    getPosts
  } = useNewsfeed()

  return (
    <div>
      {posts.map((post, index) => (
        <div key={index}>
          <h1>{post.title}</h1>
        </div>
      ))}
      {loading ? (
        <p>Loading...</p>
      ) : (
        <button onClick={getPosts}>Load Posts</button>
      )}
    </div>
  )
}

export default Newsfeed
Enter fullscreen mode Exit fullscreen mode
// useNewsfeed.js
import { useState } from 'react'

export default () => {
  // Our hook manages our state for us 
  const [loading, setLoading] = useState(false)
  const [posts, setPosts] = useState([])

  // It also manages our functionality
  const getPosts = () => {
    setLoading(true)
    fetch('/v1/posts')
      .then(data => data.json())
      .then(data => setPosts(data))
      .then(() => setLoading(false))
  }

  // Finally, it exposes only what is required by the component
  return {
    state: { loading, posts },
    getPosts,
  }
}
Enter fullscreen mode Exit fullscreen mode

Should you do this?

The answer is... it depends. It's like asking if you should inline a styling rule or if you should put it into a stylesheet. There are valid situations for both.

There are a couple of benefits to consider:

  1. It cleanly separates your concerns without adding much complexity.3

  2. It cleans up your imports a lot. You don't have 20 imports from a component library inbetween your network request imports.

  3. Legibility. You can take one glance at the component (or the hook!) and you understand what's going on.

  4. It tends to consolidate logic into one location, which makes locating, understanding, and altering it easier.

  5. Seamless integration. No need to refactor anything.

  6. You can completely forgoe the implementation. Our hook could look like this and our component would be none the wiser:

// useNewsfeed.js

// This always returns a fixed value, meaning 
// the component always behaves as if the posts loaded already.
export default () => {
  return {
    state: { 
      loading: false,
      posts: [{
        id: 6,
        title: 'Hooks are cool'
      }]
    },
    getPosts: () => null,
  }
}
Enter fullscreen mode Exit fullscreen mode

Some very important caveats

This doesn't replace the need for separating components into multiple smaller components.

If you choose to do this, you really should be comfortable with how hooks work.

You should also familiarize yourself with how hook dependencies work. For example, if you don't properly use things like useCallback and useMemo, you might end up with infinite loops and not understand why. 😅

If you haven't, I recommend you download an extension that warns you when you're using them incorrectly to spot things like missing hook dependencies.

🧠 What do you think?

Do you already use hooks like this?
Do you hate the idea of this?
Do you take it further and create tons of custom hooks?

Lemme know below 👋



1 https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook
2 Similarly, but not exactly the same. Remember, unlike props, React is choosing when to rerender everything based on certain things happening under the hood - like the value of useState or a dependency passed to useCallback changing.
3 With a simple example like our Newsfeed app that only has two states and one function, it probably isn't worth the complexity this adds. It's up to you to decide what's right. Always ask yourself: "Can I quickly develop and maintain a mental model of this?"

Top comments (3)

Collapse
 
michaelparkadze profile image
Michael Parkadze

Great first post ;)

Collapse
 
kwameaj67 profile image
Kwame Agyenim-Boateng

I used this method to retrieve data from an API and its worked perfectly. Thanks

Collapse
 
mrilob profile image
Murilo B Silva

Amazing! I don't use React in web but I'm learning React Native and I'll use this to develop my apps. Thank you!