Keeping up With React: Let’s Talk Hooks!
In previous versions of React, functional components had a purely presentational role. With Hooks, developers can now write entire applications made exclusively of functional components! Let's touch on the what, how and why behind Hooks.
Table of contents:
Not long ago, I attended an awesome meet-up in Toronto called Keeping up with React: What’s new in version 16. One of the topics we discussed was the newly added React Hooks feature (> 16.8 and Native > 0.59). Let’s touch on the what, how and why behind Hooks.
What are Hooks?
Simply put, Hooks are functions you can use inside functional components to hook them into state management and lifecycle features that were previously only available for class components. In previous versions of React, functional components had a purely presentational role (here are some props, give me some JSX 😝), because there was no way to make them stateful. With Hooks, developers can now write entire applications made exclusively of functional components!
How do Hooks work?
Two of the most important hooks introduced by React 16.8 are useState and useEffect. useState effectively replaces this.state and this.setState from class components, and can be called as many times as you like to create different pieces of state, which can now hold any primitive type. All you have to do is pass an initial value to useState and it returns the current value and set function to update that piece of state.
Even though you no longer need a single object to define the whole state of your component, if you do choose to have objects in state, a small gotcha is that the new set function overwrites state (unlike this.setState), so you have to do your own manual merging.
useEffect acts like componentDidMount, componentDidUpdate and componentWillUnmount, running after every render cycle. You provide a callback function to run a certain side effect. You can also call this hook multiple times within the component body, which provides a way to organize code by feature and not by time of execution within the component lifecycle.
useEffect also takes an optional second argument, an array of dependencies that will be diff’d to trigger the execution of the callback. (Remember how we had to manually check for diffs in componentDidUpdate?). There are basically three cases, based on the value of the second argument:
none: the callback will run after every render cycle
[ ]: the callback will run only at first render, essentially like componentDidMount
[ dependency1, dependency2 ] : the callback will run if and only if dependencies 1 and/or 2 change, behaving like componentDidUpdate (but now without manual diff checking). You can also return a cleanup function, which will run during unmount and in between every execution of the hook. If [] was provided as a second argument, that function will run only at unmount, essentially like componentWillUnmount, and you can use it to unsubscribe from event listeners, websockets or timers.
Let’s look at an example app written in both class and functional forms. The app provides buttons to select which photo album to display. The parent app manages which album was selected by the user, and passes that albumId down to its child component which fetches and renders a list of photos for that album.
import React from "react";
import AlbumListWithoutHooks from "./AlbumListWithoutHooks";
class AppWithoutHooks extends React.Component {
state = { albumId: 1 };
render() {
return (
<div>
{Array.from({ length: 10 }, (_, k) => k + 1).map(i => (
<button onClick={() => this.setState({ albumId: i })}>
Album {i}
</button>
))}
<AlbumListWithHooks album={this.state.albumId} />
</div>
);
}
}
import React, { useState } from "react";
import AlbumListWithHooks from "./AlbumListWithHooks";
const AppWithHooks = () => {
const [albumId, setAlbumId] = useState(1);
return (
<div>
{Array.from({ length: 10 }, (_, k) => k + 1).map(i => (
<button onClick={() => setAlbumId(i)}>Album {i}</button>
))}
<AlbumListWithHooks albumId={albumId} />
</div>
);
};
class AlbumListWithoutHooks extends React.Component {
state = { photos: [] };
async componentDidMount() {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/photos/?albumId=${this.props.albumId}`
);
this.setState({ photos: response.data });
}
async componentDidUpdate(prevProps) {
if (prevProps.resource !== this.props.resource) {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/photos/?albumId=${this.props.albumId}`
);
this.setState({ photos: response.data });
}
}
render() {
return (
<div>
{this.state.photos.map(photo => (
<img key={photo.id} src={photo.url} alt={photo.thumbnailUrl} />
))}
</div>
);
}
}
import React, { useState, useEffect } from "react";
import axios from "axios";
const AlbumListWithHooks = props => {
const [photos, setPhotos] = useState([]);
useEffect(() => {
(async albumId => {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/photos/?albumId=${albumId}`
);
setPhotos(response.data);
})(props.albumId);
}, [props.albumId]);
return (
<div>
{photos.map(photo => (
<img key={photo.id} src={photo.url} alt={photo.thumbnailUrl} />
))}
</div>
);
};
The repeated logic for fetching data inside componentDidMount and componentDidUpdate is gathered into a single call inside useEffect, and no manual check for a change in the “resource” prop is needed.
Custom Hooks
Possibly the most powerful aspect of Hooks is the ability to extract stateful logic as a custom functional component and use it in other functional components. This results in better logic organization, testing and reusability. Unlike previous patterns like higher-order components, Hooks don’t introduce unnecessary nesting into your component tree. As Hooks are JS functions, they may or may not take in arguments as well as return information. Just remember to define your hook with the initial use keyword to allow the built-in linter to check for violations to the Hooks Rules. Check out some cool custom hooks here.
Continuing with the example above, we can create a useGetRequest hook that will fetch any type of resource we want depending on the url that is passed, flexible to be consumed across various types of components. Many lines of code saved!
import { useState, useEffect } from "react";
import axios from "axios";
const useGetRequest = url => {
const [resources, setResources] = useState([]);
useEffect(() => {
(async resource => {
const response = await axios.get(url);
setResources(response.data);
})(url);
}, [url]);
return resources;
};
Why hooks?
There are numerous reasons why we could want to move away from classes. Classes can be confusing compared to functions (for humans and computers), they require knowledge of this (remember to bind!), and you usually end up writing the same boilerplate code. There is also usually that fear of starting with a functional component and then having to convert it into a class later. Their lifecycle methods are easy to misuse, might include unrelated logic, and are harder to test and minify. And if you choose to include reusable logic with higher order components, for example, you might end up bloating the component tree (aka “wrapper hell”).
Therefore Hooks come onto the scene to leverage functional components, allowing them to perform the same role previously only available to class-based components - a design decision more in sync with the functional nature of JavaScript.
Final Notes
According to the React team, there is no intention to deprecate classes and gradual integration is encouraged as opposed to major rewrites. These patterns can coexist since Hooks are additive and completely opt-in. Instead of rewriting code that already works, let’s make use of Hooks in our new code!
Newsletter Sign-up
Receive summaries directly in your inbox.