Hooks allow you to use React features by calling special hook functions from within function components. Let's take a deeper dive into hooks and see what advantages they bring over using class components.

Kentaro Wakayama
23 February 2022

Hooks are new React APIs added to React 16.8. They enable React functional components to use React features that were previously only available in React class components. In a nutshell, they are functions that bring the power of React class components to functional components, giving you a cleaner way to combine them.
Before Hooks, React functional and class components performed distinct functions. Functional components were only used for presentation purposes—to render data to the UI. They could only receive and render props from parent components, which were usually class components. Functional components did not keep track of an internal state and did not know the component lifecycle. Thus, they were referred to as “dumb components.”
Class components, on the other hand, track a component's internal state and enable you to perform operations during each phase by using lifecycle methods. For example, you can fetch data from an external API once a component mounts, update the state due to user interactivity, and unsubscribe from a store once a component unmounts. All of this is possible because a class component keeps track of its internal state and lifecycle. Consequently, class components were—and still are—referred to as “smart components.”
Hooks were added to solve some of the problems related to using React class components. However, some are not connected to React directly, but, rather, the way native JavaScript classes are designed. In this article, you will learn about the problems of React class components, as well as Hooks and how they solve these problems.
A React class component is a native JavaScript class, so it inherited the issues of JavaScript classes, including working with this, explicitly binding methods, verbose syntax, and more. Let's discuss each of these in detail.
Consider the code below:
import React, { Component } from "react";
class Card extends Component {
constructor(props) {
super(props);
this.state = { name: "John Doe" };
this.changeName = this.changeName.bind(this);
}
changeName() {
this.setState({ name: "Jane Doe" });
}
render() {
return (
<div>
<p>My name is {this.state.name}.</p>
<button onClick={this.changeName}>Change Name</button>
</div>
);
}
}
export default Card;
This code shows a simple class component that renders a name state to the UI and provides a button to change the name. As you see, you have to explicitly bind this, call super(props) in the base constructor, and always prefix your state or methods with this to access them.
These little details are a function of how ES6 classes are designed and are some of the common causes of bugs in React applications.
React class components have a verbose syntax that can often result in “very large components” - components with a lot of logic split across several lifecycle methods that are hard to follow.
Also, the lifecycle method API forces you to repeat related logic across different lifecycle methods throughout the component, as seen below.
import React from 'react';
class FriendProfile extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
friend: {}
}
}
componentDidMount() {
this.subscribeToFriendsStatus(this.props.id);
this.updateFriendProfile(this.props.id);
}
componentDidUpdate(prevProps) {
// compariation hell.
if(prevProps.id !== this.props.id) {
this.updateFriendProfile(this.props.id);
}
}
componentWillUnmount() {
this.unSubscribeToFriendsStatus(this.props.id);
}
subscribeToFriendsStatus () {
console.log("I have subscribled")
}
unSubscribeToFriendsStatus () {
console.log("I have unsubscribled")
}
fetchFriendData(id) {
// fetch friend logic here
}
async updateFriendProfile (id) {
this.setState({loading: true})
// fetch friend data
await this.fetchFriendData(id);
this.setState({loading: false})
}
render() {
return (<div> Hello {this.friend ? this.friend.name : "John Doe!"}</div>); // ... jsx
}
}
export default FriendProfile;
To compose React class components, you often need complicated patterns such as the Higher-Order Components (HOC) pattern that make your code difficult to read and maintain.
HOCs are often used for cross-cutting concerns like authorization, logging, and data retrieval; that is, tasks that span the entire application and would otherwise lead to repeated logic. The HOC pattern leverages higher-order functions in JavaScript—an HOC is a pure function with zero side effects.
Consider the code below:
import React, { Component } from "react";
const withLogger = (WrappedComponent) => {
return class WithLoggerHOC extends Component {
state = {
name: "John Doe",
logInfo: true
};
componentDidMound() {
// some logic
}
componentDidUdpate() {
// some logic
}
render() {
return <WrappedComponent {...this.state} {...this.props} />;
}
};
};
export default withLogger;
You can use your HOC, as seen here:
import React from "react";
import WithLogger from "/_veryfront/fs/L2FwcC9wYWdlcy9ibG9nL2FydGljbGVzL1dpdGhMb2dnZXI.js";
const Info = (props) => {
return <div>Hello, my name is {props.logInfo && props.name}</div>;
};
export default WithLogger(Info);
This is clean enough, however, issues arise when you try to use multiple HOCs: for example, WithRouter, WithAuth, WithTheme, WithLogger, etc. This can lead to codes that are difficult to read, as you can see here:
import React from "react";
import WithRouter from "/_veryfront/fs/L2FwcC9wYWdlcy9ibG9nL2FydGljbGVzL2NvbXBvbmVudHMvV2l0aFJvdXRlcg.js";
import WithAuth from "/_veryfront/fs/L2FwcC9wYWdlcy9ibG9nL2FydGljbGVzL2NvbXBvbmVudHMvV2l0aFJvdXRlcg.js";
import WithLogger from "/_veryfront/fs/L2FwcC9wYWdlcy9ibG9nL2FydGljbGVzL2NvbXBvbmVudHMvV2l0aFJvdXRlcg.js";
import WithTheme from "/_veryfront/fs/L2FwcC9wYWdlcy9ibG9nL2FydGljbGVzL2NvbXBvbmVudHMvV2l0aFJvdXRlcg.js";
const SomeComponent = (props) => {
return (
// some jsx
)
}
export default WithTheme(
WithAuth(
WithLogger(
WithRouter(SomeComponent);
)
)
);
Also, composing HOCs can result in a deeply nested structure in the React dev tool, making it difficult to follow through a component state or props—and, consequently, making it difficult for your code to debug your application.
Hooks solve all of the class-related problems listed above. They also enable you to write cleaner, leaner, and more maintainable code.
In this section, you’ll learn about the useState, useEffect, useRef, useContext, and custom Hooks in more detail.
The useState Hook gives you an easy way to use state in a functional component. It also takes one argument (the initial state) and returns an array with two values: the current state and a function to update the state. By convention, these values are stored using array destructuring.
Here is the function signature of the useState Hook:
const [state, setState] = useState(initialState);
You can rewrite the Card class component above to a functional component by using the useState Hook, as seen here:
import React, { useState } from "react";
const Card = () => {
const [name, setName] = useState("John Doe");
return (
<div>
<p>Hello, from SayName. My name is {name}</p>
<button onClick={() => setName("Jane Doe")}>Change Name</button>
</div>
);
};
export default Card;
You can see that Hooks enable you to use states without dealing with class constructors and binding this. This approach is much simpler and more straightforward than using a class component.
The useEffect Hook gives you an easier way to hook into a component lifecycle without writing redundant logic like you do in class components.
Consider the code below:
useEffect(() => {
// Mounting
return () => {
// Unmounting
}
}, [
// Updating
])
The function signature of the useEffect Hook is in the code. It takes two arguments: a function that is called after each complete render and an array.
The function passed to the useEffect Hook contains the logic that executes side effects. If you want to do a clean up, as you do with componentWillUnmount in a class component, return a function from this that is passed to the useEffect Hook.
Lastly, the array in the second argument holds a list of dependencies used for updating the component.
You can refactor your FriendProfile component to the code below:
import React, { useState, useEffect } from "react";
function FriendProfile({ id }) {
const [loading, setLoading] = useState(false);
const [friend, setFriend] = useState({});
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
updateFriendProfile(id);
subscribeToFriendsStatus(id);
return () => {
unSubscribeToFriendsStatus(id);
};
}, [id]);
const subscribeToFriendsStatus = () => {
console.log("I have subscribled");
};
const unSubscribeToFriendsStatus = () => {
console.log("I have unsubscribled");
};
const fetchFriendData = (id) => {
// fetch friend logic here
};
const updateFriendProfile = async (id) => {
setLoading(true);
// fetch friend data
await fetchFriendData(id);
setLoading(false);
};
return <div> Hello {friend ? friend.name : "John Doe!"}</div>; // ... jsx
}
export default FriendProfile;
You can share non-visual logic by creating custom Hooks. This will allow you to reuse logic across your components, consequently keeping your codes DRY.
The isMounted Hook is a good example of a custom Hook. It ensures you do not “set state” when a component is unmounted. In React, mutating the state on an unmounted component will log a warning error in the console:
// Warning: Can't perform a React state update on an unmounted component.
// This is a no-op, but it indicates a memory leak in your application.
// To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
This is because when a component is unmounted in React, it will never be mounted again. You can handle this by conditionally mutating a component’s state based on this.isMounted, as seen below:
if (this.isMounted()) { // This is bad.
this.setState({...});
}
Although this may work, the React team considers it an antipattern, and, instead, recommends that you track the value mounted status yourself.
To do this effectively, create a custom Hook that tracks the status of mounted. You can use this Hook across your components.
Consider this code:
import { useEffect, useState } from "react";
const useIsMounted = () => {
const [isMounted, setIsMouted] = useState(false);
useEffect(() => {
setIsMouted(true);
return () => setIsMouted(false);
}, []);
return isMounted;
};
export default useIsMounted;
Now you can use the isMounted component:
//...
const Dev = () => {
const isMounted = useIsMounted();
const [state, setState] = useState("");
useEffect(() => {
function someFunc() {
setTimeout(() => {
if (isMounted) setState("Lawrence Eagles");
}, 4000);
}
someFunc();
});
return (
// ... jsx blob
);
}
The useRef Hook takes an initial value and returns a ref, or reference object. This is an object with a current property that is set to the initial value of the useRef.
Here is the function signature:
const refContainer = useRef(initialValue);
The useRef Hook is used for creating refs that gives you direct access to DOM elements. This is useful when working with forms.
Consider this code:
const formComponent = () => {
const inputElem = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputElem} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Also, ref is a mutable JavaScript object that can persist on every rerender, making useRef useful for keeping the mutable value around.
This also helps when accessing state in a callback, a pattern we’ll discuss later.
The React context API gives you a way to share data across your components without prop drilling. With the useContext Hook, you can easily use the context API from a functional component.
Here is the function signature of this Hook:
const value = useContext(MyContext)
The useContext Hook takes a context object (the value returned from React.createContext) as its parameter and returns the current context value (the value of the nearest context provider component). When this provider component updates, the Hook triggers a rerender.
import React, {createContext, useContext} from "react";
const themes = {
light: {
name: "Light",
foreground: "#000000",
background: "#eeeeee"
},
dark: {
name: "Dark",
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Button />
</ThemeContext.Provider>
);
}
const Button = () => {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
{theme.name} Button
</button>
);
}
As you’ve just seen, Hooks can help solve many problems associated with React class components. Hooks are simple, composable, flexible, and extendable, which are major pros. Despite this, there are a lot of challenges, including how Hooks handle stale state, access state in an asynchronous callback, and access state synchronously.
Consider the code below:
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const lazyUpdate = () => {
setTimeout(() => {
setCount(count + 1);
}, 3000);
};
return (
<div>
<p>
<strong>You clicked {count} times</strong>
</p>
<button onClick={lazyUpdate}>Increment count</button>
</div>
);
};
export default Counter;
When you click the Increment count button n times, you expect the count state to increase n times, but this is not the case. The above example shows the stale state problem: Your count increases only once, no matter how many times you click the Increment count button.
This occurs because React fails to compute the next state value with the previous state. To solve this problem, you can pass an updater function to setState. You also can pass this function to setCount, as seen below:
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const lazyUpdate = () => {
setTimeout(() => {
setCount(count => count + 1);
}, 3000);
};
return (
<div>
<p>
<strong>You clicked {count} times</strong>
</p>
<button onClick={lazyUpdate}>Increment count</button>
</div>
);
};
export default Counter;
Consider the code below:
import React, { useState } from "react";
const AsyncCounter = () => {
const [count, setCount] = useState(0);
const openModal = () => {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={openModal}>
Increment count
</button>
</div>
);
}
export default AsyncCounter;
Here, the showModal method opens an alert box that displays the count state value after 3000 milliseconds. If you try to update the count between this time interval by clicking the Increment count button, you’ll notice that the count state value on the alert only updates once. While this is similar to the stale state problem, it demands a different solution.
In this case, you need a way to persist the mutable state value on every rerender. As discussed above, the useRef Hook is great for this. You can refactor your code to use the useRef Hook, as seen below:
import React, { useState, useRef } from "react";
const AsyncCounter = () => {
const counterRef = useRef(0);
const [count, setCount] = useState(false);
const handleIncrement = () => {
counterRef.current++;
setCount(!count);
};
function handleAlertClick() {
setTimeout(() => {
alert("You clicked on: " + counterRef.current);
}, 3000);
}
return (
<div>
<p>You clicked {counterRef.current} times</p>
<button onClick={handleIncrement}>Click me</button>
<button onClick={handleAlertClick}>Show alert</button>
</div>
);
}
export default AsyncCounter;
Since useState works asynchronously, it does not work well when you try to access the state synchronously.
Consider this code:
import React, { useState } from "react";
const SyncCounter = () => {
const [count, setCount] = useState(0);
const [currentCount, setCurrentCount] = useState("");
const increment = () => {
setCount(count + 1);
setCurrentCount(`computed count is ${count}`);
};
const decrement = () => {
setCount(count - 1);
setCurrentCount(`computed count is ${count}`);
};
return (
<div className="App">
<h1>Update Count!</h1>
<p>Count: {count}</p>
<p>{currentCount}</p>
<button type="button" onClick={increment}>
Add
</button>
<button type="button" onClick={decrement}>
Subtract
</button>
</div>
);
};
export default SyncCounter;
When you increment the state, you see that even though the currentCount state is computed with the count state, it is always behind the count state.
Passing an updater function to useState won’t solve this problem, but since you know the next value, you can calculate the next value first and update both states with it. Basically, you can refractor your methods, as seen below:
const increment = () => {
const newCount = count + 1;
setCount(newCount);
currentCount(`count is ${newCount}`);
}
const decrement = () => {
const newCount = count - 1;
setCount(newCount);
currentCount(`computed count is ${newCount}`);
}
As we discussed earlier, Hooks are a collection of specialized JavaScript functions that aim to solve the issues you may experience when working with React class components. They enable functional components to use React features only available with React classes by giving you direct and flexible access to these features. Clearly, Hooks have changed the way React components are created for the better—and are here to stay!