article

React Hooks and how to adopt

Hooks was a new addition to React that came in version 16.8. It was an new exciting feature because it would allow us to have flexibility when dealing with components' internal state, lifecycle code, Context API, and other React features.

Before that, developers only had one place for state in React components: this.state. Even if you have different, unrelated data, they needed to be at the same place, which makes it hard to apply the concept of code colocation – related code should be as close as possible. Working with component lifecycle logic also has the same issue. componentDidMount, componentDidUpdate, and componentWillUnmount may contain unrelated code, which makes components harder to maintain. Another complication was working with Context Consumers. It had a lot of boilerplate, because it used either Higher Order Components, or Render Props, and it could get out of hand very quickly if you had to consume from a lot of Providers.

With the new tools Hooks provides us, we can have more granularity of state and lifecycle logic. And not only that! We can also abstract and reuse that logic, something that wasn't quite possible before with class components!

I recommend watching this video from React Conf 2018, where they announced Hooks for the first time. It explains the motivation of the React team, and also some introduction and examples of the new feature. This animation that uses code from the previous video shows very clearly how Hooks can help us solve the problem of colocation.

Basic React Hooks

The React documentation does a great job explaining how to use the new Hooks, and also some new rules that you need to follow to make sure everything works as expected. Since there's already a lot of accessible information about them, I'll just have a summary here:

useState

const [state, setState] = useState(initialState);
Returns a stateful value, and a function to update it.

Your state here can be any data; there's no need to be an object. You should use multiple useState calls for different, unrelated data.

useEffect

useEffect(callback, depArray);
Accepts a function that contains imperative, possibly effectful code.

This Hook is used to run code based on lifecycle phases. It's also used to run side effects when a set of given dependencies change – this might be props, or data from other Hooks, like context and state. On top of that, it allows adding cleanup logic for those effects in an easy way. This is very useful to simplify the previous method of listening to prop changes, with componentDidUpdate and prevProps.

Here's an example. Suppose you want to do some action after another one, but you only have access to a status prop that might be pending, finished or error. You could do it like this:

class Component extends React.Component {
    componentDidUpdate(prevProps) {
        if (prevProps.status != this.props.status 
            	&& this.props.status === 'finished') {
            doSomething();
        }
    }
}

With useEffect, it becomes:

const Component = ({ status }) => {
    useEffect(() => {
        if (status === 'finished') {
            doSomething();
        }
    }, [status]);
};

Besides the documentation, I also really like this link that explains how useEffect works. It isn't only a way of doing lifecycle logic. It does change the way you code those side effects.

A quick example would be something like a paginator component. Usually, you need to fetch data: 1) when it mounts; 2) when a page changes. You may implement this like: when the user clicks the next page button, update the page state, and then call the method that fetches data. If you use useEffect, you can bind the effect to the page state, and then, the next page button click handle only needs to update the state: fetching the data would come automatically. With this, further logic that updates the page state also updates the data, which is convenient. This case is a simple example, but the applications are endless.

useContext

const value = useContext(MyContext);
Accepts a context object (the value returned from React.createContext) and returns the current context value for that context.

This Hook is handy because it makes very easy to consume contexts without nesting your JSX. Also, it opens a lot of possibilities when using custom Hooks.

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
An alternative to useState. Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

If you notice your setState's are changing at the same time, you may use useReducer to group up related data.

useCallback / useMemo

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
Returns a memoized callback.

If you used Pure Components before, you probably noticed that keeping your references from changing is necessary to optimize some components. Since you redefine a callback every time a functional component renders, useCallback is used to keep this reference unchanged.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Returns a memoized value.

useMemo is related to that, but not the same. It is used to avoid recomputing expensive calculations or rendering expensive components. Do not use this for side effects – useEffect is the right one –, the explanation for that is present in the documentation:

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

You may think you need to use it frequently, but it might not be that necessary. For a better explanation, there's this fantastic article from Kent C. Dodds explaining when to use each of them.

useRef

const refContainer = useRef(initialValue);
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

useRef works similar to useState, except it does not trigger rerenders. You can use it to hold into any value. Very useful to store callback references like setTimeout, etc.

Remaining Hooks

These are some of the commonly used Hooks. Others are used more situationally. Learn more about them in the official docs.

Third-party Hooks

A lot of your current code can also benefit from the new feature. Probably the libraries that you use already supply their own custom Hooks. Here are some examples:

React Router has useHistory, useLocation, useParams, and useRouteMatch. React-redux has useSelector and useDispatch. Formik has useField, useFormik and useFormikContext.

We also have new possibilities, SWR and react-query comes to mind. For more examples about Hooks, look at the awesome-react-hooks repository and the useHooks website.

Custom Hooks

For me, this is the most impactful change: the possibility of using Hooks – either the react ones, or from a library – in a reusable way. This level of reusability is not possible with class components.

You will likely have some business logic code that can be implemented with Hooks. Don't be afraid of extracting this logic to a Custom Hook.

On our project, we have the currentUser object in the redux store. We would access this with mapStateToProps. It had some boilerplate code, but then we thought it was simpler just to create a useCurrentUser that uses useSelector from react-redux library, and that's it – straightforward yet valuable change. We also have useToggles for feature toggles and usePermissions for permissions.

Another useful example for us is that some pages have an origin parameter in its query string, and we would use this URL to go back after a form submit, for instance. We could use useLocation from React Router and extract this logic.

export function useOriginRoute(defaultRoute) {
  const location = useLocation();
  const args = parse(location.search); // from 'query-string'

  let route = defaultRoute;
  if (args.origin) {
    route = args.origin;
  }

  return route;
}

Custom Hooks are the game-changer here. Look for opportunities for them in your application; you won't regret it.

Adoption steps

After learning about React Hooks, your first thought would probably be: "Hey, this thing might be awesome for my team! I want to bring this to my project ASAP". I was also excited to start using Hooks in production right away.

Perhaps you already have a team member as excited as you, or your tech leader has already heard about React Hooks and wants to push it to the project. That makes everything easier, but if this is not your situation, you need to tackle this subject with caution.

Although coding with Hooks sounds more straightforward than the standard class components, it is not trivial to be picked up, even for experienced developers. Using them out of nowhere in your pull requests might actually slow your team down. Here at Vinta, I took the safe approach to make sure my team was aware of how this new feature works, to keep everyone on the same page, and to minimize the overhead for them. Here's what I did:

Update React version

First of all, you are to update your React version to 16.8+. Probably this is not an issue if you work in a recent project – React 16 was launched in September 2017. Otherwise, you have to work a little bit on updating React and any other dependencies that might break with this update.

Study (a lot!) about Hooks

You might have read a few articles, seen some buzz on Twitter, and checked some Github gists with Hooks magic. But these wouldn't be enough. You need to understand the motivation behind it: learn the quirks of useEffect and its array of dependencies; why rules of Hooks are essential; when to use useCallback or useMemo (or neither).

Take some time to read the documentation, as well as to look for extensive articles with plenty of Hooks examples, in order to fully grasp the concept of it. Working in a playground app is very important too. You need to be ready to show your team why Hooks can make everyone's life easier.

Introduce the idea to your team

Before proceeding, meet with your manager/tech lead. Talk to them about Hooks. Explain that you believe it will be awesome to have it on the project. Let them know it's a feature that could be adopted gradually. Also, make sure they understand you will have the ownership of teaching the team about the tool, good practices with it, and its pitfalls. After that, do a quick presentation about Hooks to your co-workers. It doesn't have to be detailed: Give a brief introduction, show them the documentation. They don't need to learn everything now, just having a gist of it is enough.

Update 3rd party libraries that use Hooks

Some widely used React libraries got cool Hooks that give a lot of benefits. Like React Router, React Redux, and others. Using these Hooks in new code, or refactoring old code with them, might be a great example to show your team how this can simplify the codebase.

Find a use case

Now that your teammates have some knowledge about Hooks, you can start finding some situations in your project where using them would be beneficial, so later, you can have some examples for them to use. Using the Hooks from React are cool, but too simple. We are looking for some nice use cases for a custom Hook. For instance, mine was a Hook to check if some specific DOM node had ellipsis, so we could add a tooltip only on elements that had ellipsis.

export function useEllipsisCheck() {
  const [hasEllipsis, setHasEllipsis] = useState(false);
  const nodeRef = useCallback((node) => {
    if (node != null && node.scrollWidth > node.offsetWidth) {
      setHasEllipsis(true);
    }
  }, []);

  return { hasEllipsis, nodeRef };
}

Very simple yet effective Hook, it made our ellipsis check logic way cleaner, and reusable! Another good example is to use a custom Hook to abstract the usage of 3rd party Hooks. Have a repetitive logic with React Router props? Replace it with a custom Hook!

Teach your colleagues

Once you start using Hooks and creating custom ones, your peers will learn a lot reviewing your pull requests, but they still might need a hand. Good descriptions and relevant comments can help with this process. Also, offer to pair programming with them when you notice an opportunity of using Hooks in their task. This is the moment they'll really notice the value of using Hooks in the code.

Success!

Awesome! At this stage, your team should be using Hooks comfortably. As time goes on, the team will find opportunities to use custom Hooks to reuse a lot of logic, either while refactoring old code or new. The cool thing about Hooks is that you can approach the same problem you had before from a new point of view, since now you have more flexibility to handle it. Don't be afraid of trying something new; it will probably be worth it in the long run.

Conclusion

In this article, I've briefly introduced the concept of React Hooks, how it can change the way you code your React components, how libraries can help with that, and my experience introducing this new feature to my colleagues at Vinta.

Have something I missed but you think it's important? Want to share your experience with React Hooks? Is there something you want to ask about? Let me know in the comments!

João Paulo Siqueira Lins

Fullstack Developer at Vinta Software.