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.
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.
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.
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.
Lol remember render props? What were we thinking
— Andrew Clark (@acdlite) January 24, 2019
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.
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.
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.
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.
Important Notice: Opinions expressed here are the author’s alone. While we're proud of our engineers and employee bloggers, they are not your engineers, and you should independently verify and rely on your own judgment, not ours. All article content is made available AS IS without any warranties. Third parties and any of their content linked or mentioned in this article are not affiliated with, sponsored by or endorsed by American Express, unless otherwise explicitly noted. All trademarks and other intellectual property used or displayed remain their respective owners'. This article is © 2019 American Express Company. All Rights Reserved.