React 16.8.0 released yesterday, bringing along with it the official stable release of the Hooks API. Yesterday, we talked about the high-level offerings that this API brings to the table, and today we'll look at how to implement this API in your next application (or even toss it into a current project).
Hooks comes as a minor version release with no breaking changes - you are free to upgrade any existing project's dependencies to 16.8.0 without having to worry about breaking existing function.
npm i -S [email protected] [email protected]
To put it briefly, hooks brings state and lifecycle methods to function components. Previously,
if you wanted to persist changes across renders or run effects when your component passed a
lifecycle checkpoint, you were forced to use a class component that extends React.Component
. With
hooks, you may now reach for this additional functionality from within your function body and have
it reliably persist between renders.
Hooks API
Without further ado, let's take a look at hooks's syntax.
import React, { useState } from 'react';
function MyComponent() {
const [name, setName] = useState('Aaron');
return <h1>{name}</h1>;
}
The first thing you'll notice is the hook import:
import { useState } from 'react';
Hooks always begin with use
and you get a handful of base hooks to work with directly from react
.
There are a few constraints you must follow when using hooks:
- Hooks must begin with
use
. - Hooks may only be called from the top level function context.
- Hooks cannot be called conditionally (for example, inside of an
if
block orfor
loop). - Hooks must be called in the same order each render.
When calling useState
, you provide an initial value and get back a copy of the state value and a
function you may use to update it. On subsequent renders, that value will hold the updated state
value.
You can imagine hooks as a way of hanging up state between renders. Below is a diagram to help you visualize this process:
Let's walk through this:
-
First render
useState
is called and initializes the first element of the state array- the getter for this element is set to the initial state value
false
- the setter for this element is set to an updater function that updates the value stored in index 0 in the state array
useState
is called again and initializes the second element of the state array- the getter for this element is set to the initial state value
'Aaron'
- the setter for this element is set to an updater function that updates the value stored in index 0 in the state array
- Update state via
setName
-
Second render
useState
is called and sees that a state value exists in index 0, so it populatesactive
andsetActive
with the existing valuesuseState
is called again and sees that a state value exists in index 1, so it populatesname
andsetName
with the existing values. Since this state value was updated, it will now contain the updated valueRoss
.
It's important to note that this is simply a visualization (hook state is actually stored as a linked list, see Additional Resources below to check the source), but I believe it makes visualizing the flow much easier.
You'll notice that useState
is called once per state value, as opposed to what you may be accustomed
to with class component state where all state values are stored as properties on a single state
object. This is convention with hooks, but not
strictly necessary.
Lifecycle Hooks
The most common hook you'll encounter after useState
is the lifecycle effect hooks useEffect
.
Here's an example:
import React, { useState, useEffect } from 'react';
import { ApiService } from '../services/api.service';
function MyComponent({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
const subscription = ApiService.subscribe(user => setUser(user));
return () => {
subscription.unsubscribe();
};
}, [id]);
return <h1>{user.name}</h1>;
}
The effect hook accepts a callback to be executed after each render. This callback may optionally
return a cleanup function to perform any necessary cleanup. This hooks also optionally accepts an
array of dependencies as a second argument. If provided, this hook will cleanup and reinitialize
any time any one of these dependencies changes (in our example above, we would want to change up
the subscription any time the component's id
prop changes).
If you only want an effect to run once upon mount (to emulate the effect of componentDidMount
in
class components), you just provide an empty array. You can also use this pattern to run code only on
dismount, as shown below:
import React, { useEffect } from 'react';
function Mounter() {
useEffect(() => {
console.log('I just mounted!');
}, []);
return /*...*/;
}
function Unmounter() {
useEffect(() => {
return () => {
console.log('I just unmounted!');
};
}, []);
return /*...*/;
}
Custom Hooks
Perhaps the coolest feature of hooks is the ability to create your own custom hooks. Component-based design lets you create reusable units for each piece of UI your application will require. Custom hooks allow you to do the same thing with function logic! Right out of the gate, this allows you to clean up commonly used state interactions such as binding an input to a state value:
import React, { useState } from 'react';
function InputComponent() {
const [value, setValue] = useState('');
function onChange(event) {
setValue(event.target.value);
}
return <input value={value} onChange={onChange} />;
}
We can then extract this into a custom hook like so:
import React, { useState } from 'react';
function useInput(defaultValue = '') {
const [value, setValue] = useState(defaultValue);
function onChange(event) {
setValue(event.target.value);
}
return [value, onChange];
}
function InputComponent() {
const [value, onChange] = useInput();
return <input value={value} onChange={onChange} />;
}
To clean it up and make it more reusable, we can move useInput
into its own file and import it when
we want to use it:
// hooks/useInput.js
import { useState } from 'react';
export function useInput(defaultValue = '') {
const [value, setValue] = useState(defaultValue);
function onChange(event) {
setValue(event.target.value);
}
return [value, onChange];
}
// InputComponent.js
import React from 'react';
import { useInput } from '../hooks/useInput';
function InputComponent() {
const [value, onChange] = useInput();
return <input value={value} onChange={onChange} />;
}
Custom hooks enable a fix it and forget it paradigm, which is great for productivity. You can return anything you want from a custom hook, and you can even compose custom hooks out of other custom hooks!
Additional Resources
Here are some other great resources that I think will prove useful in your journey to implementing React hooks:
- Making Sense of React Hooks - Dan Abramov link
- React Today and Tomorrow - Dan Abramov & Sophie Alpert link
- Hooks are Mixins with Ryan Florence - React Podcast link
- React hooks: not magic, just arrays - Rudi Yardley link
- Under the hood of React's hooks system - Eytan Manor link
- React Hooks collection - @nikgraf link
- React Hooks Documentation link
- React Hooks source link
I've compiled these and more into a talk and demo on my GitHub. Feel free to check it out.