— Diana Lease, Lead Front-End Engineer

Being an agile startup allows Clause to quickly adapt to new technologies and respond to changes in the ever-evolving world of programming. That’s why, as the Lead Front-End Engineer at the company, I was excited to explore React hooks when the feature became available in React 16.8. The introduction of hooks is one of the most substantial changes to React since the framework was first released about six years ago. So, naturally, when hooks first came out, React developers had a lot of questions. The main one being: Do we really need them? Luckily, the creators of React made it easy for developers to try out using hooks without any real risk, since you can use them alongside class components in an existing app, without the need to migrate an entire codebase. One thing to note, however, is that you cannot use hooks inside of a class component.

Now that the Clause engineering team has had some time to use React hooks in our front-end app, we’d like to share our thoughts on whether or not adopting hooks has been worthwhile. (Spoiler alert: It has been!)

An obvious drawback to adopting React hooks, as is with adopting any new technology, is the learning curve. React is one of the most popular web frameworks, and it is the backbone for tons of single page applications. When you have an entire team working on the same application, it can be difficult to introduce changes to the way code is written. Developers working on the same project need to be able to understand not only the code they’ve written themselves, but also the code that others on the team have written. Again, this is where having a small team can be beneficial. After learning hooks, I was able to spend an afternoon teaching the rest of the front-end team, who picked things up very quickly!

There are four main reasons I have come to love hooks:

  1. Readability
  2. The fact that they can be less error-prone
  3. Performance
  4. Reusability

Readability

Hooks make React code much easier to read and reason about. Although learning React hooks can seem like a bit of a burden for developers who are accustomed to using class components, hooks are going to make it much easier for developers who have not previously used React to start using the framework. With React hooks, every component is a functional component! You do not need to think about classes, this, or the constructor function. Furthermore, code becomes cleaner and simpler without the need to bind a component’s methods in the constructor. Hooks also allow us to more easily co-locate related logic.

Less Error-Prone

In class components, the local state of a component needs to be defined as an object in the constructor, and this object can become large (hopefully not too large, however, as it might be a signal that your component could be better refactored into multiple components!). A large state object makes component logic confusing, and updating the state is often error-prone. State should be treated as immutable. To avoid bugs, the state object in class components should only be manipulated by using the this.setState method that class components provide.

However, with class components, it is technically possible to manipulate state directly. So if someone inadvertently wrote code manipulating state in the wrong way, you might end up with unexpected behavior that could be tedious to debug. It is easy to avoid this scenario with the useState hook. useState accepts one argument (the initial value) and returns an array of two elements: the current value and an updater function. You can have multiple useStates in a component, so instead of one state object, you can define several different pieces of state separately. This makes following the logic of a component and the piece of state being updated much easier. Using array destructuring, defining a piece of state can be written like so: const [count, setCount] = useState(0). Since we are declaring count with const, we will not be able to reassign count to a new value the way we could inadvertently do with class components. If we try to write count = 2, we will encounter a type error. This helps us reinforce the fact that we should always update state using the updater function (in this case, setCount).

Performance

Another reason I advocate using hooks is that they make it very easy to optimize a component’s performance. Firstly, in his blog post introducing hooks, Dan Abramov states, “adopting Hooks could reduce your bundle size because code using Hooks tends to minify better than equivalent code using classes”. Therefore, you may see a positive impact on performance just by the sole act of using hooks inside of functional components instead of using class components. Furthermore, there are built-in hooks that specifically help with optimization via memoization, notably, useMemo and useCallback.

useMemo accepts two arguments, a “create” function and an array of dependencies, and returns a memoized value. This value that is returned will only be recomputed if a dependency in the array has changed. This can avoid unnecessarily performing expensive computations. In the example below, someExpensiveOperation will only be run on each re-render if data has changed.

const [data, setData] = React.useState([12, 267, 54.99])

const transformedData = React.useMemo(
  () => someExpensiveOperation(data), [data]
);

Similarly, useCallback accepts a callback function and an array of dependencies, and it returns a memoized version of the callback function. useCallback is just a shorthand for writing useMemo(() => fn, deps).

Reusability

Lastly, React hooks are a great way to DRY up your code. Hooks allow you to extract logic that is used by multiple functional components and use that logic in a custom hook. A custom hook is just a function that uses other hooks, either built-in hooks or even other custom hooks. If you are using the react hooks eslint plugin, be sure to name your custom hook using the prefix use. This way, the linter will know its a hook and can apply the correct rules.

In our codebase, we often update an error on state when an API call results in an error. Depending on the shape of the error returned by the API, we need to do some manipulation of that error to extract the message that should be rendered by the component. Duplicating this logic in class components can result in longer code as well as room for errors when making changes in multiple places. I was able to easily turn this logic into a custom hook that could be used in any functional component. This custom hook is a function that takes an initial value which defaults to null, and returns an array of two elements — an error message and a method to parse the error and update the error message state. Seem familiar? I chose to make my custom hook return an array of this shape to mimic the shape of the return value of the useState hook. However, you don’t have to do this. Your custom hook is just a function and can return whatever makes sense for your use case. Below is a simplified version of my useErrorMessage custom hook:

import React from 'react';

const useErrorMessage = (initialValue = null) => {
  const [errorMessage, setErrorMessage] = React.useState(initialValue);
  const parseError = (error) => {
    if (!error) {
      setErrorMessage(null);
      return;
    }
    if (error) {
      const errorMessage = error.message; // this is an overly simplified version of the logic used in our app
      setErrorMessage(errorMessage);
    }
  };
  return [errorMessage, parseError];
};

export default useErrorMessage;

And that’s it! Now your custom hook can be imported and used in any functional component just like how you use React’s built-in hooks. Below is an example:

import React from 'react';
import useErrorMessage from ‘customHooks/useErrorMessage’;

// the following snippet would be used inside of you React component
const [errorMessage, updateError] = useErrorMessage(null);
const [data, setData] = React.useState(null);

const onSubmit = async (values) => {
  try {
    const result = await fetch(‘myUrl.com’);
    setData(result);
    updateError(null);
  } catch (err) {
    updateError(err);
    setData(null);
  }
};

After having used hooks quite a bit now, I can definitively say that adopting hooks has improved the way I write and think about components in React. The code is cleaner, more performant, and allows for ease of reuse. As noted earlier, you can start using hooks in your functional components as long as your app is using React v16.8 or greater, without the need to refactor your existing class components. However, once you fall in love with hooks, you may decide that updating your class components to be cleaner functional components is worthwhile.

Depending on the component and its logic, converting a class component to instead use hooks could take less than a minute to a few hours. If your component is a class component solely because it needs to utilize local state, it’s relatively simple to update the component to use React.useState instead of a state object in the constructor. However, if your class component uses multiple lifecycle hooks, such as componentDidMount or componentDidUpdate, it may take some time and effort to refactor your code to utilize the useEffect hook to achieve the desired behavior. My advice is to start small, get comfortable with using hooks, and I bet you’ll soon find yourself wanting to use them everywhere!