Intro to React Hooks

The date was October 25, 2018. The setting was Las Vegas, Nevada. Facebook shocked the React development community at React Conf 2018. I was there to watch Sophie Alpert and Dan Abramov as they took the stage to present the keynote address. They were there to reveal to the crowd, over 600 people in attendance, a completely new and exciting way of writing React components. They called it "hooks."

This article will show you the fundamentals of programming React components using hooks. I'll discuss the basic hooks built into React, and we'll write a simple up/down counter component built with a custom hook.

But before we dive too deep into hooks, let's review a simple class component.

Class component

We're all familiar with writing components with classes. What's shown below is a simple up/down counter component. It uses a render prop to hand over control of the rendering details to the caller.

ClassCounter.jsx
class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: props.initialCount };
  }

  decrement = () => {
    this.setState((state) => ({ count: state.count - 1 }));
  };

  increment = () => {
    this.setState((state) => ({ count: state.count + 1 }));
  };

  render() {
    const { count } = this.state;
    const { decrement, increment } = this;
    return this.props.children({ count, decrement, increment });
  }
}

We store the current count on state by calling this.setState. The state is stored on an instance of the class. This is so that multiple components don't inadvertently share information. We can alter the count value by calling class property methods.

Remember, this is the logic for keeping the current count in state, and for incrementing and decrementing the count. It doesn't render anything by itself—the render prop does that—and yet we still treat it as a component (i.e., we render it with <Counter />).

This seemed backwards, and the React core team thought that there must be a better way.

Standing it on its head

What if we could somehow reverse everything? You know, stand it on its head. Instead of the logic calling something to render, what if we started with what you want to render, and have that call—or use—the logic to get the data that it needs? That's what hooks are all about.

With this in mind, let's start by writing a Counter function component that uses a custom hook.

Counter.jsx
const Counter = ({ initialCount }) => {
  const { count, increment, decrement } = useCounter(initialCount);

  return (
    <div>
      <button onClick={decrement}>Decrement</button>
      <span>{count}</span>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

This is nothing more than a function component. And, as you can see, it calls useCounter to get the current count, as well as the functions to manipulate it. It deals with nothing but rendering, which allows you to focus on just that, rather than on how the data is collected.

By convention, hooks are prefixed with use.

The hook brings you back

Now that you understand the rendering part of our component, let's look at the implementation of the useCounter custom hook.

useCounter.js
import { useState } from 'react';

const useCounter = (initialCount) => {
  const [count, setCount] = useState(initialCount);

  return {
    count,
    increment: () => setCount((currentCount) => currentCount + 1),
    decrement: () => setCount((currentCount) => currentCount - 1),
  };
};

export default useCounter;

Again, the code above has absolutely nothing to do with rendering. It's simply a function that returns the current count and methods to manipulate the count. In other words, everything that our component needs to render.

But what's with the useState? Well, React now provides a set of built-in hooks that you can use to build larger custom hooks. Among the most basic is useState.

useState

useState is roughly synonymous with this.state and this.setState in class components. But unlike this.setState, you don't have to use an object. You can use any JavaScript type—boolean, string, array, object … whatever. In fact, you are encouraged to use multiple useState calls, one for each of your state values, instead of collecting them up into one object, like we are used to doing with class components.

You call useState with the initial state, and it returns an array. This array contains two elements. The first is the current state value. The second is a "setter" function. You use this to change the state value.

It is worth noting that instead of passing an initial value, you may pass a function. If you do, useState will only call it once to compute the initial value.

const someExpensiveProcedure = () => {
  let results;
  for (let i = 0; i < 10000000; i++) {
    // do someething with results
  }
  return results;
};

const [value, setValue] = useState(someExpensiveProcedure);

Destructuring

I mentioned above that useState returns an array with a current value and a setter function. It is a common practice to destructure these values directly. You will rarely, for example, ever need to store the return value directly.

const arr = useState(initialValue);

Instead, simply destructure it inline.

const [value, setValue] = useState(initialValue);

No this?

Another thing that should pop out at you when working with hooks is there is no reference to this. None at all. In fact, with hooks, you may never use this again.

React keeps track of component instances internally, leaving you free to write function components.

Are class components going away?

The short answer is "No." But let's talk about it further.

In my opinion, once you start coding with hooks, you may be hard pressed to ever write a class component again. They support most, if not all, of the capabilities of class components, but without all of the ceremony. In fact the built-in useEffect hook mimics most of the lifecycle events that we are used to.

The React core team made it very clear—and took a lot of effort to ensure—that class components could live side-by-side with hook components.

In fact, if you look at this from the standpoint of semantic versioning, 16.8.0 means that there are no breaking changes. That is important. If hooks came out in something like 17.0.0, we might have cause for alarm.

That said, if a class component fits your particular use case, then use a class component. We all like shiny new things, but at the end of the day, it's all about whatever is best from the standpoint of the user's experience and code maintainability.

When can I use them in prod?

Right now! React 16.8.0 was released today, February 6, 2019. Update your package.json dependencies and away you go!

Will hooks replace render props?

I would guess that in most cases hooks will probably end up changing the way we write components today. And that means for your day-to-day development that yes, hooks most likely will replace render props.

In fact Andrew Clark (from the React core team) tweeted this earlier.

Will there still be cases where a render prop is better suited for the task? Sure. Render props won't go away completely, but they will become less and less the norm.

Once you get comfortable with hooks, I'm sure you'll agree.

If you want to support both hooks and render props in the same package, take a look at this article on the Hydra pattern.

Performance considerations

Performance should also be a consideration when writing hooks. In my useCounter above, the reference to increment and decrement functions will change on each render. This is becasue they are created and returned as arrow functions. This may cause your navigation controls to render unnecessarily.

If rendering performance is a concern, the solution is—you guessed it—another hook. React provides a useCallback hook for just this reason. We can wrap our callback functions in useCallback and React will memoize them, assuring** that they point to the same function each time.

useCounter.js
const useCounter = (initialCount) => {
  const [count, setCount] = useState(initialCount);
  const increment = useCallback(
    () => setCount((currentCount) => currentCount + 1),
    [setCount]
  );
  const decrement = useCallback(
    () => setCount((currentCount) => currentCount - 1),
    [setCount]
  );

  return {
    count,
    increment,
    decrement,
  };
};

This is similar to the case we had with class components where it was more performant to place callback functions on the instance instead of passing lamda functions to components.

** Actually, the memoization is not guaranteed. React says that it may choose to "forget" some of the memoization for memory allocation reasons. I still believe that it is well worth the effort.

Other hooks

React provides more built-in hooks that I haven't discussed in this article; some you may never use. Others, like useEffect, you may use quite often.

You can read all about the built-in hooks in the Hooks API Reference.

Should I refactor my entire codebase?

I'm not going to tell you what to do, but if it were me, I'd leave it alone. In other words, if it works, don't touch it. I'm sure you have new code that you could be writing rather than spending your time refactoring old code to use hooks.

If you're in there anyway making a change, maybe you could do the conversion. And if you've written tests, you should be good to go.

Which brings us to…

How do I test a custom hook?

There are two answers to this question, depending on how you are distributing your hook.

If you are consuming your custom hook within a component and have no plans to ship it as a stand-alone hook, you would test the component that uses the hook just as you did with a class component (i.e., you will be testing the hook by testing the component itself—no need to specifically test the hook).

However, if your custom hook will be distributed as a general-purpose solution for others to build components with, it must be tested on its own. You might think that because it's just a function that you could test it as such. Maybe something like this.

useCounter.test.js
test('useCounter', () => {
  const { count, decrement, increment } = useCounter(1);

  expect(count).toBe(1);
  increment();
  expect(count).toBe(2);
  decrement();
  expect(count).toBe(1);
});

Simple, huh? There's only one problem. It won't work.

The base hooks that React provides must be called from within the rendering of a component. If you run the test above, you will receive this error.

Invariant Violation: Hooks can only be called inside the body of a function component.

Then what do we do? The solution is to use a test helper that wraps your custom hook in a component, but exposes what is returned from your hook to your test.

I've written such a helper that's included in Kent C. Dodds' react-testing-library. It's called testHook.

testHook

To get started, just import it from react-testing-library as follows.

import { cleanup, act, testHook } from 'react-testing-library';

Let's take the test from above and adapt it so that it will work. Here, we wrap the call to useCounter in a callback funtion passed to testHook. The working test looks like this.

useCounter.test.js
test('useCounter', () => {
  let count, decrement, increment;
  testHook(() => ({ count, decrement, increment } = useCounter(1)));

  expect(count).toBe(1);
  act(increment);
  expect(count).toBe(2);
  act(decrement);
  expect(count).toBe(1);
});

We also must place our deconstructed variables that are returned from the hook in let statements outside of the callback. This is so that they are in scope of the tests. They will also be in scope of the callback.

The only other thing that is a bit unsusal is that we must wrap any functions returned from the hook in an act. If not, we will get a warning. As we aren't passing any arguments, we can simply pass a pointer to the function. If we were to pass arguments, we would need to use a lambda.

For example, if we had a function called incrementBy that required an argument, we would need do this.

act(() => {
  incrementBy(2);
});

Conclusion

Hooks represent a huge step forward in React component development. If you aren't already using hooks now, I assure you that you will be in the coming months. I hope this article provides you with enough information to get you started on the right track.