React: Fantastic Hooks and How to Use Them

May 02, 2019Brendan Daoud7 min read

react-hooks

A couple months ago, React hooks were officially released, the hype was high and we decided to put them to the test on a project. Here
is what I learnt and why I strongly advise you to give them a try also.

What are those?

Let's begin the tour with the basics: what are React hooks?

React has a very good documentation about it, I strongly advise you to read it before reading that article. But to keep it super short and simple, hooks are functions that you can call in a React function component and that let you do most of what you can do in a class component.

The basics hooks are:

  • useState to handle the internal state of the component similar to what setState do.
  • useEffect to handle lifecycle events such as componentDidMount and componentDidUpdate.
  • useContext to read React contexts.

There are all very simple to use, and in my opinion more readable than their class component equivalent.

But the one of the main assets of hooks is that you can write your own hooks using pre-existing ones. It is actually fairly easy, let's say you want a hook to toggle a boolean value, here is the kind of thing you can do:

const useToggle = (initialValue = false) => {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue((v) => !v));

  return [value, toggle, setValue];
};

const Component = () => {
  const [isDarkTheme, toggleDarkTheme] = useToggle();

  return (
    <Button dark={isDarkTheme} onCLick={toggleDarkTheme}>
      Toggle Dark Theme
    </Button>
  );
};

Feeling confused about that code? Don't worry, this article will explain most of the reasoning.

Why them though?

Well, now that you know what there are, you may argue that it can all be done with class components, why even bother?

Pros

The first thing, you will notice reading at hooks code is how clear and concise it is. There is some learning of course, but it is also really self explanatory. You read the function component from top to bottom and you have the whole story. For those who care about line count, you will also notice that it often takes less line to do the same using hooks.

But to me the real seller is that you can share pieces of logic between components thanks to hooks. Indeed, writing a custom hook and using it in several components is a great way to inject logic shared logic into them.

The injection pattern is very handy as you can inject many different pieces of code into a component. The most common pattern for class components is composition, translated as the use of Higher Order Components (HOC for short), but doing multiple composition means piling up layer after layers of HOCs that make the code hard to read and the component tree unbelievably deep. Thus, when using class component developers tend to use other means such as Redux or services for the shared logic.

An example that I found on a project I worked on was the handling of the loading state. It was all declared in a Redux store even though the loading state of a component is strongly scoped to the component and is very unlikely to be used anywhere else. I will present later a way to handle loading with hooks, but for now here is a simpler example.

Most React application will at some point have forms, here is a classic way of dealing with a form with a class component:

class Component extends React.Component {
  state = {
    username: "",
    password: ""
  };

  handleInputChange = (event) => {
    this.setState({
      [event.target.name]: event.target.value
    });
  };

  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="username"
          value={this.state.username}
          onChange={this.handleInputChange}
        />
        <input
          type="password"
          placeholder="password"
          value={this.state.password}
          onChange={this.handleInputChange}
        />
      </form>
    );
  }
}

Using hooks you can extract the handleInputChange logic and inject it back into the component, and any other component that uses inputs.

const useInput = () => {
  const [value, setValue] = useState("");
  const onChange = useCallback((event) => setValue(event.target.value), []);

  return { value, onChange };
};

const Component = () => {
  const usernameInput = useInput();
  const passwordInput = useInput();

  return (
    <form>
      <input type="text" placeholder="username" {...usernameInput} />
      <input type="password" placeholder="password" {...passwordInput} />
    </form>
  );
};

I like hooks because they allow to share logic while using intensively and efficiently the local state of a component. They also help trimming the component tree and keeping it as concise as possible, leaving only the meaningful nodes.

Cons

While very flexible hooks have also their limitations. It is currently impossible to use hooks to provide a context and no equivalent to ErrorBoundary exists.

Hooks also require some functional programming, it can be unusual those who are really into object oriented programming.

Finally, it is a pretty recent technology some most of it is still experimental. You will find that most libraries try to provide hooks interfaces, but often in beta phase.

Gotta write them all !

Now that we are all pumped up. What have I learnt about writing hooks.

Stand on the shoulders of giants

First thing, I realized that I wasn't the only one eager to use hooks. Most big libraries have it in their roadmap when they are not providing it already. There is a very cool connector for Redux for instance and a hook version of Formik that really changed my use of forms. So even if you may tempted to rewrite everything, consider using the hook version of libraries you are using already instead.

Know your Dependencies

What you have to keep in mind when using functional components is that the function is called each time the component is rendered. Meaning hooks will be called each time the component renders. There are ways to avoid flood of instantiations and computations, you can declare dependencies for your hooks similar to the way useEffect works. To do so, you just have to pass the dependencies array from hook to hook to the core hooks.

You can also use useMemo and useCallback to make sure that you are making only the minimum required for each change. A very nice thing when using those memoizing tools is that the value they return will be strictly the same if the dependencies do not change, so components using those values as props will not render again if no meaningful change happened.

Choose your return values

You may have noticed that the useState hook returns an array instead of the expected object. I was advised to always use and return objects as it is a way in JS to name your arguments and return values. But as hooks may be very abstract, naming return values is not always a responsibility I want to take.

As a rule of thumb, when returning values ask yourself if your user will need a specific variable name for those values of not, if you want to impose a name, use an object, if not use an array. And keep in mind that you can also use a mix of arrays and objects as destructing in ES6 is super flexible.

You can also use array and object destructuring to let your user choose what they need. So don't hesitate to give access to many return values.

Remember hooks are functions

So enjoy the whole flexibility of functional programming. If some code is repeated, you can create a function for it. Maybe it is another hooks you had not thought of, maybe it's just another function. Injection is easy, so use it if you want to.

Do not hesitate to make factories also. A hook can be easily created on the fly by calling a function. While it's not a pattern I would generalize to avoid functional mess, it can come very handy in some situation. In my project I had a very simple factory for Redux actions.

import { useDispatch } from "redux-react-hook";

const useAction = (actionCreator) => {
  const dispatch = useDispatch();

  return (payload) => dispatch(actionCreator(payload));
};

Now that you know how to feed them, what to expect from them and how to shape them, maybe you want to see an example. Time for a case study.

Time goes by...

Calling an API is a common task in React, but there is currently no consensus on the best method to achieve it. One of the challenges is that it is an asynchronous that can fail, which means that you have a state to handle. Here is my take on the matter.

You can use it directly in a component:

const Component = () => {
  const [send, { isLoading, error }] = useAsync(asyncApiCall);

  return (
    <React.Fragment>
      <button disabled={isLoading} onClick={send}>
        {isLoading ? "Sending..." : "Send"}
      </button>
      {error && "Error:" + error.message}
    </React.Fragment>
  );
};

Or in another hook in conjunction with Redux:

const useEntity = (id) => {
  const entity = useMappedState(selectEntityById(id));
  const upsertEntity = useAction(upsertEntityAction);

  const [fetch, { error, isLoading }] = useAsync(async () => {
    upsertEntity(await fetchEntity(id));
  });

  useEffect(() => {
    if (entity == null) {
      fetch();
    }
  }, []);

  return [entity, { error, isLoading }];
};

Going futher

The whole hook ecosystem is fairly new and I believe a lot of it remains to be discovered. So to finish that article, I would like to show you a simple pattern I found in a pretty cool routing library, and that blew my mind. Remember that hooks are simple functions, so they can return anything, even a component.

const useLoader = (isLoading) => (isLoading ? <div>loading...</div> : null);

const Component = () => {
  const [entity, { isLoading }] = useEntity();
  const loader = useLoader(isLoading);

  return loader || <div>{entity.name}</div>;
};
B

Brendan Daoud

Web Developer at Theodo