Skip to content
Refactoring

Refactoring components in React with custom hooks

React hooks can be easily created as any other function. We can create our own domain specific hooks that can be reused across the application.

I have sometimes heard people say that with the introduction of React's functional components, it's natural to expect functions to become larger and more complex. After all, the components are written as "one function", and therefore you have to accept a bit of swelling. It is also stated in React's documentation that:

 

"Now that function components can do more, it’s likely that the average function component in your codebase will become longer."

It also says that we should:

"Try to resist adding abstraction too early."

If you use CodeScene, you are probably aware that it warns you if functions become too long or complex. In light of the above, it might be tempting to think that CodeScene should be configured to be more lenient in this case. While this is entirely possible to do, I will argue that we don't have to, and we shouldn't resist adding these abstractions too much, as there are many benefits to be had, and it's not always that tricky to do. We can still keep the code health at a perfect 10!

 

Tackling complexity

We should realize that although functional components are indeed written as "one function", they can - like any other function - be composed of other functions. Every useState, useEffect, other hooks or sub-components are themselves just functions. Therefore we should be able to tackle complexity in the same way we are already familiar with: by extracting common patterns into new functions that encapsulate more complex behavior.

The obvious way of attacking complex components is to decompose them into sub-components. But this can feel unnatural or it can be difficult to find the right delineation. Sometimes we can instead find new abstractions simply by looking at the hook logic of the component.

Whenever we see a long list of useState or useEffect or other built-in primitive hooks within a component, there might be an opportunity to extract them into a custom hook. A custom hook is just another function that uses other hooks, and they are really simple to build.

The following component represents a small dashboard that lists a user's repositories (you could imagine something like GitHub). It is not a complex component, but it will serve as a good example for using hooks in this way. 


function Dashboard() {
  const [repos, setRepos] = useState<Repo[]>([]);
  const [isLoadingRepos, setIsLoadingRepos] = useState(true);
  const [repoError, setRepoError] = useState<string | null>(null);

  useEffect(() => {
    fetchRepos()
      .then((p) => setRepos(p))
      .catch((err) => setRepoError(err))
      .finally(() => setIsLoadingRepos(false));
  }, []);

  return (
    <div className="flex gap-2 mb-8">
      {isLoadingRepos && <Spinner />}
      {repoError && <span>{repoError}</span>}
      {repos.map((r) => (
        <RepoCard key={i.name} item={r} />
      ))}
    </div>
  );
}
 


To extract the hook logic into a custom hook, we can just copy the code into a function whose name starts with use (in this case useRepos): 


/**
 * Hook for fetching the list of all repos for the current user.
 */
export function useRepos() {
  const [repos, setRepos] = useState<Repo[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchRepos()
      .then((p) => setRepos(p))
      .catch((err) => setError(err))
      .finally(() => setIsLoading(false));
  }, []);

  return [repos, isLoading, error] as const;
}
  
 

The reason it has to start with use is so that the linter can detect that you are calling a hook rather than a "normal" function, which allows it to check that you are following the rules of calling a hook.

The only new thing here is the return statement and the use of as const. The typing hint simply ensures that the inferred type is exactly the same as that of the array expression: an array of 3 elements with the types Repo[], boolean and string | null, respectively. There is no rule that says it has to be an array, though. You can return anything you want from a hook.

Putting the hook into practice, our component now becomes:


function Dashboard() {
  const [repos, isLoadingRepos, repoError] = useRepos();

  return (
    <div className="flex gap-2 mb-8">
      {isLoadingRepos && <Spinner />}
      {repoError && <span>{repoError}</span>}
      {repos.map((i) => (
        <RepoCard key={i.name} item={i} />
      ))}
    </div>
  );
}

Notice that our component no longer has access to any of the setter functions. It doesn't need them as it's all encapsulated in the useRepos hook. If you do need them, you are of course free to return some, or all of them.

What are the advantages of doing this? React's documentation mentions that:

"Building your own Hooks lets you extract component logic into reusable functions."


We can easily imagine other components within the application with a need to display a list of repositories, and all they have to do now is to import the
useRepos hook. If the hook is updated - perhaps with some form of caching, or a continuous update via polling or by a more sophisticated method - then all users of the hook will reap the benefits.

But even if we don't reuse the hook, there are still advantages to doing this. Although in our example all uses of useState and useEffect came together to achieve one thing - the fetching of repos - it's quite common to see many sets of these primitives within a single component all achieving different things, and if we package the ones that "belong together" into their own hooks, it makes it easier see which of the states that have to be kept in sync when updating the code. It also gives us:
  • Smaller functions that are easier to read
  • The ability to name the concept (useRepos)
  • A natural place to document the operation (TSDoc, etc.)

 

Conclusion

We have learned that React hooks are not special and are as easily created as any other function. We can create our own domain specific hooks that can be reused across the application. There are plenty of pre-written general hooks you can find on various blogs or in "hook libraries". Remember that you can use these in your domain specific hooks just as easily as the familiar useState or useEffect hooks. An example is Dan Abramov's useInterval hook. Perhaps you have a use case similar to useRepos above, except it has to continuously poll for updates? You can then put useInterval into use in your custom hook.
 

Simon Sandlund

Elements Image

Subscribe to our newsletter

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Semper neque enim rhoncus vestibulum at maecenas. Ut sociis dignissim.

Latest Articles

AI Coding Assistants: Introducing CodeScene AI Generated Code Refactoring

AI Coding Assistants: Introducing CodeScene AI Generated Code Refactoring

AI Coding Assistants: Let's introduce you to AI generated code refactoring. Read more and join the Beta testing program.

Change coupling: visualize the cost of change

Change coupling: visualize the cost of change

Code can be hard to understand due to excess accidental complexity. Or, it can look simple, yet its behavior is anything but due to complex...

CodeScene's IDE Extension brings CodeHealth™ Analysis directly into your editor

CodeScene's IDE Extension brings CodeHealth™ Analysis directly into your...

We've just launched an IDE Extension for VS Code, helping developers tackle code complexity within the editor. Read more and try it out!