This article covers how to convert common use cases from class components to hooks, so you can modernize your React apps.
React has been around for many years and is often chosen as a solution for creating UIs in modern applications. Throughout the years, the way we write components with React has changed greatly.
Initially, we had the createClass
method, which was later replaced by class components. In version 16.8, React released hooks that revolutionized how we write React applications, as they allowed us to write more concise and cleaner code and
provided a better pattern for creating reusable stateful logic.
Many developers shifted toward hooks and abandoned class components. However, many legacy React apps still use class components. What’s more, class components still have their use cases, such as error boundaries, as there is no hook for that.
In this article, we will cover how to convert common use cases from class components to hooks.
You can find full code examples in this GitHub repo and an interactive CodeSandbox below.
State management is one of the most common things in any React application. React renders components based on the state and props. Whenever they change, components are re-rendered, and the DOM is updated accordingly. Here is an example of a simple class component with a counter state and two methods to update it.
import { Component } from "react";
class ManagingStateClass extends Component {
state = {
counter: 0,
};
increment = () => {
this.setState(prevState => {
return {
counter: prevState.counter + 1,
};
});
};
decrement = () => {
this.setState(prevState => {
return {
counter: prevState.counter - 1,
};
});
};
render() {
return (
<div>
<h2>Managing State - Class</h2>
<div>Count: {this.state.counter}</div>
<div>
<button onClick={this.increment}>Increment</button>
<button onClick={this.decrement}>Decrement</button>
</div>
</div>
);
}
}
export default ManagingStateClass;
The hooks implementation is much more concise.
import { useState } from "react";
const ManagingStateHooks = () => {
const [counter, setCounter] = useState(0);
const increment = () => setCounter(counter => counter + 1);
const decrement = () => setCounter(counter => counter - 1);
return (
<div>
<h2>Managing State - Hooks</h2>
<div>Count: {counter}</div>
<div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
</div>
);
};
export default ManagingStateHooks;
The component is just a function that returns JSX. We use the useState
hook to manage the state. It returns an array with two values—the first one is the state and the second one is the updater function. We also have increment
and decrement
functions that utilize the setCounter
updater.
There are scenarios in which we might need to perform some kind of action whenever the state changes. In a class component, we can do that by using the componentDidUpdate
lifecycle.
import { Component } from "react";
class StateChangesClass extends Component {
state = {
counter: 0,
};
componentDidUpdate(prevProps, prevState) {
console.log("New counter", this.state.counter);
localStorage.setItem("counter", this.state.counter);
}
increment = () => {
this.setState(prevState => {
return {
counter: prevState.counter + 1,
};
});
};
decrement = () => {
this.setState(prevState => {
return {
counter: prevState.counter - 1,
};
});
};
render() {
return (
<div>
<h2>Reacting To State Changes - Class</h2>
<div>Count: {this.state.counter}</div>
<div>
<button onClick={this.increment}>Increment</button>
<button onClick={this.decrement}>Decrement</button>
</div>
</div>
);
}
}
export default StateChangesClass;
When the state changes, we save the new counter value in the local storage. We can achieve the same in a functional component by utilizing the useEffect
hook.
import { useState, useEffect } from "react";
const StateChangesHooks = () => {
const [counter, setCounter] = useState(0);
const increment = () => setCounter(counter => counter + 1);
const decrement = () => setCounter(counter => counter - 1);
useEffect(() => {
console.log("Current counter", counter);
localStorage.setItem("counter", counter);
}, [counter]);
return (
<div>
<h2>Reacting To State Changes - Hooks</h2>
<div>Count: {counter}</div>
<div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
</div>
);
};
export default StateChangesHooks;
The useEffect
hook expects two arguments—a callback function and an array of dependencies. This hook always runs at least once after the component is mounted. Then, it runs only when any of the values passed inside of the dependencies
array change. If the dependencies array passed to the useEffect
is empty, then the effect runs only once. In our example, whenever the counter
state changes, the useEffect
runs the function that saves the counter
in the local storage.
If you want to fetch some data in class components, you need to initialize an API request in the componentDidMount
lifecycle. In the code example below, we fetch and display a list of posts.
import { Component } from "react";
class FetchingDataClass extends Component {
state = {
posts: [],
};
componentDidMount() {
this.fetchPosts();
}
fetchPosts = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const data = await response.json();
this.setState({
posts: data.slice(0, 10),
});
};
render() {
return (
<div>
<h2>Fetching Data - Class</h2>
<div>
{this.state.posts.map(post => {
return <div key={post.id}>{post.title}</div>;
})}
</div>
</div>
);
}
}
export default FetchingDataClass;
With hooks, we can again use the useEffect
hook. As I mentioned previously, the useEffect
hook runs once after the component is mounted for the first time, and then any time dependencies passed change. We ensure that the useEffect
runs only once by passing an empty array as the second argument for the dependencies argument.
import { useState, useEffect } from "react";
const FetchingDataHooks = () => {
const [posts, setPosts] = useState([]);
const fetchPosts = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const data = await response.json();
setPosts(data.slice(0, 10));
};
useEffect(() => {
fetchPosts();
}, []);
return (
<div>
<h2>Fetching Data - Hooks</h2>
<div>
{posts.map(post => {
return <div key={post.id}>{post.title}</div>;
})}
</div>
</div>
);
};
export default FetchingDataHooks;
Cleaning up when a component is unmounted is quite important, as otherwise we could end up with memory leaks. For instance, in a component, we might want to listen to an event like resize
or scroll
and do something based on the
window’s size or scroll’s position. Below you can see a class component example that listens to the resize
event and then updates the state with the window’s width and height. The event listener is removed in the componentWillUnmount
lifecycle.
import { Component } from "react";
class CleanupClass extends Component {
state = {
width: window.innerWidth,
height: window.innerHeight,
};
componentDidMount() {
window.addEventListener("resize", this.updateWindowSize, {
passive: true,
});
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateWindowSize, {
passive: true,
});
}
updateWindowSize = () => {
this.setState({
width: window.innerWidth,
height: window.innerHeight,
});
};
render() {
return (
<div>
<h2>Cleanup - Class</h2>
<div>
Window Size: {this.state.width} x {this.state.height}
</div>
</div>
);
}
}
export default CleanupClass;
There is one feature of the useEffect
hook we did not cover yet. We can perform a cleanup in a component by returning a function from the callback that was passed to the useEffect
. This function is called when the component is
unmounted. As the example below shows, we first define the updateWindowSize
function and then add the resize
event listener inside of the useEffect
. Next, we return an anonymous arrow function that will remove
the listener.
import { useState, useEffect } from "react";
const CleanupHooks = () => {
const [width, setWidth] = useState(window.innerWidth);
const [height, setHeight] = useState(window.innerHeight);
useEffect(() => {
const updateWindowSize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
};
window.addEventListener("resize", updateWindowSize, {
passive: true,
});
return () => {
window.removeEventListener("resize", this.updateWindowSize, {
passive: true,
});
};
}, []);
return (
<div>
<h2>Cleanup - Hooks</h2>
<div>
Window Size: {width} x {height}
</div>
</div>
);
};
export default CleanupHooks;
React is very fast, and usually we don’t have to worry about premature optimization. However, there are cases in which it’s useful to optimize components and make sure they don’t re-render too often.
For instance, a common way of optimizing class components is by either using a PureComponent
or the shouldComponentUpdate
lifecycle hook. The example below shows two class components—a parent and child. The parent has two
stateful values—counter
and fruit
. The child component should re-render only when the fruit
value changes, so we use the shouldComponentUpdate
lifecycle to check if the fruit
prop
changed. If it’s the same, then the child component won’t re-render.
Class parent that causes a re-render
import { Component } from "react";
import PreventRerenderClass from "./PreventRerenderClass.jsx";
function randomInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const fruits = ["banana", "orange", "apple", "kiwi", "mango"];
class PreventRerenderExample extends Component {
state = {
fruit: null,
counter: 0,
};
pickFruit = () => {
const fruitIdx = randomInteger(0, fruits.length - 1);
const nextFruit = fruits[fruitIdx];
this.setState({
fruit: nextFruit,
});
};
componentDidMount() {
this.pickFruit();
}
render() {
return (
<div>
<h2>Prevent Rerender Class Example</h2>
<h3>
Current fruit: {this.state.fruit} | counter: {this.state.counter}
</h3>
<button onClick={this.pickFruit}>Pick a fruit</button>
<button
onClick={() =>
this.setState(({ counter }) => ({
counter: counter + 1,
}))
}
>
Increment
</button>
<button
onClick={() =>
this.setState(({ counter }) => ({ counter: counter - 1 }))
}
>
Decrement
</button>
<div className="section">
<PreventRerenderClass fruit={this.state.fruit} />
</div>
</div>
);
}
}
export default PreventRerenderExample;
Class child with shouldComponentUpdate
import { Component } from "react";
class PreventRerenderClass extends Component {
shouldComponentUpdate(nextProps, nextState) {
return this.props.fruit !== nextProps.fruit;
}
render() {
console.log("PreventRerenderClass rendered");
return (
<div>
<p>Fruit: {this.props.fruit}</p>
</div>
);
}
}
export default PreventRerenderClass;
With the introduction of hooks, we got a new higher-order component called memo
. It can be used to optimize the performance and prevent functional components from re-rendering. Below we have an implementation with hooks.
Hooks parent that causes a re-render
import { useState, useEffect } from "react";
import PreventRerenderHooks from "./PreventRerenderHooks.jsx";
function randomInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const fruits = ["banana", "orange", "apple", "kiwi", "mango"];
const PreventRerenderExample = () => {
const [fruit, setFruit] = useState(null);
const [counter, setCounter] = useState(0);
const pickFruit = () => {
const fruitIdx = randomInteger(0, fruits.length - 1);
const nextFruit = fruits[fruitIdx];
setFruit(nextFruit);
};
useEffect(() => {
pickFruit();
}, []);
return (
<div>
<h2>Prevent Rerender Hooks Example</h2>
<h3>
Current fruit: {fruit} | counter: {counter}
</h3>
<button onClick={pickFruit}>Pick a fruit</button>
<button onClick={() => setCounter(counter => counter + 1)}>
Increment
</button>
<button onClick={() => setCounter(counter => counter - 1)}>
Decrement
</button>
<div className="section">
<PreventRerenderHooks fruit={fruit} />
</div>
</div>
);
};
export default PreventRerenderExample;
Hooks child with memo
import { memo } from "react";
const PreventRerenderHooks = props => {
console.log("PreventRerenderHooks rendered");
return (
<div>
<p>Fruit: {props.fruit}</p>
</div>
);
};
export default memo(PreventRerenderHooks);
The PreventRerenderHooks
component is wrapped with the memo
component and only re-renders if the fruit
prop changes. Note that the memo
component performs a shallow comparison under the hood, so if you
need more control over when the wrapped component should re-render, you can provide your own function to perform the props comparison.
import { memo } from "react";
const PreventRerenderHooks = props => {
console.log("PreventRerenderHooks rendered");
return (
<div>
<p>Fruit: {props.fruit}</p>
</div>
);
};
export default memo(PreventRerenderHooks, (prevProps, nextProps) => {
return prevProps.fruit !== nextProps.fruit
});
Context API is a great tool for providing values to components at different levels in the component hierarchy. A new context can be created by using the createContext
method offered by React
. For this example, we will have two
contexts—one for the user state and the other one for the updater method.
userContext
import { createContext } from "react";
export const UserContext = createContext();
export const UserActionsContext = createContext();
Let’s start with the class component example. In the parent component, we provide the user
state and setUser
method to consumers.
Class Context Provider
import { Component, createContext } from "react";
import ContextApiClassConsumer from "./ContextApiClassConsumer.jsx";
import { UserContext, UserActionsContext } from "./userContext.js";
class ContextApiHooksProvider extends Component {
state = {
user: {
name: "Thomas Class",
},
};
setUser = user => this.setState({ user });
render() {
console.log("in render class user", this.state.user);
return (
<UserContext.Provider value={this.state.user}>
<UserActionsContext.Provider value={this.setUser}>
<ContextApiClassConsumer />
</UserActionsContext.Provider>
</UserContext.Provider>
);
}
}
export default ContextApiHooksProvider;
We can consume the context in a class component by utilizing the Context.Consumer
component that is available in every context. This component accepts a function as a child that receives context value as an argument.
Class Context Consumer
import { Component } from "react";
import { UserContext, UserActionsContext } from "./userContext.js";
class ContextApiClassConsumer extends Component {
render() {
return (
<UserContext.Consumer>
{user => (
<UserActionsContext.Consumer>
{setUser => (
<div>
<h2>ContextApiClass Consumer</h2>
<input
type="text"
value={user.name}
onChange={e =>
setUser({
name: e.target.value,
})
}
/>
</div>
)}
</UserActionsContext.Consumer>
)}
</UserContext.Consumer>
);
}
}
export default ContextApiClassConsumer;
As the example above shows, the child function of the UserContext.Consumer
component receives the user
state, and the child function of the UserActionsContext.Consumer
receives the setUser
method.
The hooks provider example is very similar but much more concise. Again, we use the UserContext.Provider
and UserActionsContext.Provider
component to provide the user
state and the setUser
method.
Hooks Context Provider
import { useState } from "react";
import ContextApiHooksConsumer from "./ContextApiHooksConsumer.jsx";
import { UserContext, UserActionsContext } from "./userContext.js";
const ContextApiHooksProvider = () => {
const [user, setUser] = useState({
name: "Thomas Hooks",
});
return (
<UserContext.Provider value={user}>
<UserActionsContext.Provider value={setUser}>
<ContextApiHooksConsumer />
</UserActionsContext.Provider>
</UserContext.Provider>
);
};
export default ContextApiHooksProvider;
Technically, in a functional component, we could consume the context in the same way as we did in the class component. However, there is a much cleaner approach with hooks, as we can utilize the useContext
hook to get access to context values.
Hooks Context Consumer
import { useContext } from "react";
import { UserContext, UserActionsContext } from "./userContext.js";
const ContextApiHooksConsumer = () => {
const user = useContext(UserContext);
const setUser = useContext(UserActionsContext);
return (
<div>
<h2>ContextApiHooks Consumer</h2>
<input
type="text"
value={user.name}
onChange={e =>
setUser({
name: e.target.value,
})
}
/>
</div>
);
};
export default ContextApiHooksConsumer;
If you would like to learn more about how to use Context API in a performant way, I have just the article for you.
There are scenarios in which we might need to store some data in a component, but we would not necessarily want to store it in the state, as the UI does not rely on this data in any way.
For instance, we might save some metadata that we would like to include later in an API request. This is very easy to achieve in a class component, as we can just assign a new property to the class.
import { Component } from "react";
class PreservingValuesClass extends Component {
state = {
counter: 0,
};
componentDidMount() {
this.valueToPreserve = Math.random();
}
showValue = () => {
alert(this.valueToPreserve);
};
increment = () => this.setState(({ counter }) => ({ counter: counter + 1 }));
render() {
console.log("PreventRerenderClass rendered");
return (
<div>
<h2>Preserving Values - Class</h2>
<p>Counter: {this.state.counter}</p>
<button onClick={this.increment}>Increment</button>
<button onClick={this.showValue}>Show value</button>
</div>
);
}
}
export default PreservingValuesClass;
In this example, when the component is mounted, we assign a dynamic random number on the valueToPreserve
property. We also have the counter increment to force a re-render and the Show value
button to show the preserved value
in an alert.
Like I said, with a class component, it’s easy, but it’s not so simple in a functional component. The reason for this is because any time a functional component re-renders, everything inside of it has to re-run. What this means is that if we have a component like this:
const MyComponent = props => {
const valueToPreserve = Math.random()
// ... other code
}
The Math.random()
method will be called on every re-render, so the first value that was created will be lost.
One way to avoid this problem would be to move the variable outside of the component. This wouldn’t work, though, because if the component was used multiple times, the value would be overridden by each of them.
Fortunately, React provides a hook that is great for this use case. We can preserve values across re-renders in functional components by utilizing the useRef
hook.
import { useState, useRef, useEffect } from "react";
const PreserveValuesHooks = props => {
const valueToPreserve = useRef(null);
const [counter, setCounter] = useState(0);
const increment = () => setCounter(counter => counter + 1);
const showValue = () => {
alert(valueToPreserve.current);
};
useEffect(() => {
valueToPreserve.current = Math.random();
}, []);
return (
<div>
<h2>Preserving Values - Class</h2>
<p>Counter: {counter}</p>
<button onClick={increment}>Increment</button>
<button onClick={showValue}>Show value</button>
</div>
);
};
export default PreserveValuesHooks;
The valueToPreserve
is a ref that initially starts with the null
value. However, it’s later changed in the useEffect
to a random number that we want to preserve.
Although we shouldn’t have to access the state and properties of a child component often, there are situations in which it can be useful—for instance, if we want to reset some of the component’s state or get access to its state. We need
to create a ref in which we can store a reference to the child component that we want to access. In a class component, we can use the createRef
method and then pass that ref to the child component.
Expose Properties Class Parent
import { Component, createRef } from "react";
import ExposePropertiesClassChild from "./ExposePropertiessClassChild";
class ExposePropertiesClassParent extends Component {
constructor(props) {
super(props);
this.childRef = createRef();
}
showValues = () => {
const counter = this.childRef.current.state.counter;
const multipliedCounter = this.childRef.current.getMultipliedCounter();
alert(`
counter: ${counter}
multipliedCounter: ${multipliedCounter}
`);
};
increment = () => this.setState(({ counter }) => ({ counter: counter + 1 }));
render() {
return (
<div>
<h2>Expose Properties - Class</h2>
<button onClick={this.showValues}>Show child values</button>
<ExposePropertiesClassChild ref={this.childRef} />
</div>
);
}
}
export default ExposePropertiesClassParent;
The showValues
method retrieves the counter
state and utilizes the getMultipliedCounter
method. Below you can see the class child component.
Expose Properties Class Child
import { Component } from "react";
class ExposePropertiesClassChild extends Component {
state = {
counter: 0,
};
getMultipliedCounter = () => {
return this.state.counter * 2;
};
increment = () => this.setState(({ counter }) => ({ counter: counter + 1 }));
render() {
return (
<div>
<p>Counter: {this.state.counter}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
export default ExposePropertiesClassChild;
To get access to the properties of the child component, we only had to create a ref in the parent component and pass it. Now, let’s have a look at how we can achieve the same thing with functional components and hooks.
Expose Properties Hooks Parent
import { useRef } from "react";
import ExposePropertiesHooksChild from "./ExposePropertiesHooksChild";
const ExposePropertiesHooksParent = props => {
const childRef = useRef(null);
const showValues = () => {
const counter = childRef.current.counter;
const multipliedCounter = childRef.current.getMultipliedCounter();
alert(`
counter: ${counter}
multipliedCounter: ${multipliedCounter}
`);
};
return (
<div>
<h2>Expose Properties - Hooks</h2>
<button onClick={showValues}>Show child values</button>
<ExposePropertiesHooksChild ref={childRef} />
</div>
);
};
export default ExposePropertiesHooksParent;
In the parent, we use the useRef
hook to store a reference to the child component. The value of the childRef
is then accessed in the showValues
function. As you can see, the implementation is quite similar to the
one in the class component.
However, we’re not done yet, as we need to expose properties from the functional component manually. We can do so by utilizing the forwardRef
and useImperativeHandle
hook.
Expose Properties Hooks Child
import { useState, useImperativeHandle, forwardRef } from "react";
const ExposePropertiesHooksChild = (props, ref) => {
const [counter, setCounter] = useState(0);
const increment = () => setCounter(counter => counter + 1);
useImperativeHandle(ref, () => {
return {
counter,
getMultipliedCounter: () => counter * 2,
};
});
return (
<div>
<p>Counter: {counter}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
export default forwardRef(ExposePropertiesHooksChild);
The forwardRef
basically forwards the ref passed from the parent to the component, while useImperativeHandle
specifies what should be accessible to the parent component.
I hope that now you have a better idea of how you can convert your class components to hooks. Before you start converting all your components, make sure you go through the official hooks documentation, as there are certain rules that need to be followed, such as the fact that hooks cannot be called conditionally.
After working with hooks for a long time, I can only say that it’s definitely worth mastering them. They provide a lot of benefits, such as more concise code and better stateful logic reusability.
Thomas Findlay is a 5-star rated mentor, full-stack developer, consultant, technical writer and the author of “React - The Road To Enterprise” and “Vue - The Road To Enterprise.” He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP and more. Thomas has worked with developers and teams from beginner to advanced and helped them build and scale their applications and products. Check out his Codementor page, and you can also find him on Twitter.