React Hooks - explained
React Hooks were introduced in React 16.8 which allows functional components to have access to state and other features like performing an after effect when a particular condition is met or specific changes occur in the state(s) without having to write class components.
In this article, I will explain how to use React Hooks (useState, useEffect, useContext, useRef, useReducer, useCallback, useMemo) and how to create your custom Hooks. Just bear in mind that you can use hooks solely for the functional component.
What is a Hook in React?
A hook is a special function that enables the use of state and several other features in functional components available in class-based components.
The rules of a Hook
Let's talk about the general rules of Hooks. It is important to know where in your code you can use hooks. You need to follow the rules to use Hooks.
- Hooks can only be called inside React function components.
- Hooks can only be called at the top level of a component.
- Hooks cannot be conditional.
React useState
Hook
Your application's status will undoubtedly change at some point. This might be any form of data present in your components, such as the value of a variable or object.
The React useState
hooks allow us to track and reflect these changes in the DOM.
Let's see how can we use useState
hook in a function component.
To use this hook, we first need to import it into our component.
import {useState} from 'react'
Next, we need to initialize our state by calling useState
. It accepts an initial state and returns two values.
- The current state
- A function that updates the state
const [name, setName] = useState('John Doe');
We are destructuring the returned values from useState
. The first value is a name
which is our current state and the second value setName
is the function that is used to update our state. Lastly, we set the initial state to John Doe
.
Our state is initialized and ready to be used anywhere in our component. For example, we can read the state by using the state variable in the rendered component.
import { useState } from 'react';
function MyComponent() {
const [name, setName] = useState('John Doe');
return (
<>
<div>
<p>My name is {name}</p>
</div>
</>
);
}
export default MyComponent;
Similarly, we can update our state by using the state updater function.
import { useState } from 'react';
function MyComponent() {
const [name, setName] = useState('John Doe');
const changeName = () => {
setName('Foo');
};
return (
<>
<div>
<p>My name is {name}</p>
<button onClick={changeName}> Click me </button>
</div>
</>
);
}
export default MyComponent;
In the code above, we update the state with a button click.
At this point you might be thinking, what can the state hold?🤔
The useState
hook can keep track of objects, arrays, strings, numbers, booleans, and any combination of these.
React useEffect
Hook
I have written a separate post on React useEffect Hook
React useContext
Hook
React useContext
hook is a way to manage states globally no matter how deep they are in the components tree.
In React, there are three basic steps to using the context:
- creating the context
- providing the context
- consuming the context.
But, before diving into details on the above steps, let's talk about what problem does useContext hook solves.
The short answer is props drilling.
The parent component in the stack that requires access to the state should hold the state.
For example, let's say we have many nested components and access to the state is required by the components at the top and bottom of the stack.
We will need to pass the state as "props" through each nested component to accomplish this without Context. This is called "prop drilling". Let's see an example.
As you can see, components 2 and 3 did not need the state, even though they had to pass the state along so that it could reach component 4.
So, the solution to this problem is to use the useContext
hook.
The main goal of using the context is to provide your components access to some global data and enable them to re-render if that global data is changed. When it comes to passing along props from parents to children, context is the key to solving the props drilling problem.
However, you should be careful before deciding to use context in your app as it adds complexity to the application and makes it hard to unit test.
A quick reminder, there are three basic steps to using the context, creating the context, providing the context, and consuming the context. Let's dive into more details about this and solve the props drilling problem.
Creating the context
To create context, you must import the built-in function createContext
and initialize it.
import { createContext } from 'react';
const Context = createContext();
Providing the context
Context.Provider
is used to provide the context to its child components, no matter how deep they are. You can use the value
prop to set the value of context.
function Component1() {
const [user, setUser] = useState("John Doe");
return (
<UserContext.Provider value={user}>
<h1>{`Hey ${user}!`}</h1>
<Component2 user={user} />
</UserContext.Provider>
);
}
Now, the user Context will be accessible to every component in this tree.
Consuming the context
To use the Context in a child component, we need to access it using the useContext Hook.
import { useContext } from "react";
function Component4() {
const user = useContext(UserContext);
return (
<>
<h1>Component 4</h1>
<h2>{`Hey ${user} again!`}</h2>
</>
);
}
The full example is below.
import { useState, createContext, useContext } from 'react';
const UserContext = createContext();
function Component1() {
const [user, setUser] = useState('John Doe');
return (
<UserContext.Provider value={user}>
<h1>{`Hey ${user}!`}</h1>
<Component2 />
</UserContext.Provider>
);
}
function Component2() {
return (
<>
<h1>Component 2</h1>
<Component3 />
</>
);
}
function Component3() {
return (
<>
<h1>Component 3</h1>
<Component4 />
</>
);
}
function Component4() {
const user = useContext(UserContext);
return (
<>
<h1>Component 4</h1>
<h2>{`Hey ${user} again!`}</h2>
</>
);
}
React useRef
Hook
useRef
Hook allows you to persist values between renders.This hook also makes it possible to access DOM nodes directly within the functional components.
UseState and useReducer in a React component can cause your component re-render every time the update methods are called.
Let's take a look at how to use the useRef()
hook to keep track of variables without causing re-renders, and how to enforce the re-rendering of React Components.
useRef
does not cause re-renders
The useState
The hook itself causes a re-render, therefore if we were to count how many times our application renders using this Hook, we would be caught in an infinite loop. The useRef
hook can be used to avoid this.
useRef(initialValue)
accepts one argument as the initial value and returns a reference. A reference is an object having a special property current
.
import { useRef } from 'react';
export default function LogButtonClicks() {
const countRef = useRef(0);
const handle = () => {
countRef.current++;
console.log(`Clicked ${countRef.current} times`);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
const countRef = useRef(0)
creates a references countRef
initialized with 0
.
CountRef.current++
is increased when the button is clicked, and the handle function is also invoked. The reference value is logged to the console.
Updating the reference value countRef.current++
does not cause component re-rendering.
As you can see, updating the reference value countRef.current++
does not cause component re-rendering, because you can see in the console that 'I rendered!' is logged just once at initial rendering and does not re-render when the reference is updated.
Now, let's see the difference between reference and state by re-using LogButtonClicks
from the previous example. We will use the useState
hook to count the number of button clicks.
import { useState } from 'react';
export default function LogButtonClicks() {
const [count, setCount] = useState(0);
const handle = () => {
const updatedCount = count + 1;
console.log(`Clicked ${updatedCount} times`);
setCount(updatedCount);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
As you can see every time the button is clicked, the message 'I rendered!' gets logged in the console causing the component to re-render.
Accessing DOM elements
Most of the time, we should let React handle all the DOM manipulation, but there might be some use cases where we want to take control of DOM manipulation. In such scenarios, we can use useRef
hook to do this without causing any issues.
For instance, let's say we want to focus on the input field when the component mounts. To make it work you’ll need to create a reference to the input, and assign the reference to the ref attribute of the input, once the component mounts call the element.focus() method on the element.
import { useRef, useEffect } from 'react';
function TextInputWithFocus() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<input ref={inputRef} type="text"/>
);
}
The functional component's function scope should either calculate the output or invoke hooks. Because of this, updating a reference and changing the state of a component shouldn't be performed inside the immediate scope of the component's function.
Either a useEffect() callback or handlers (event handlers, timer handlers, etc) must be used to change the reference.
import { useRef, useEffect } from 'react';
function MyComponent({ prop }) {
const myRef = useRef(0);
useEffect(() => {
myRef.current++; // Good!👍
setTimeout(() => {
myRef.current++; // Good!👍
}, 1000);
}, []);
const handler = () => {
myRef.current++; // Good!👍
};
myRef.current++; // Bad!👎
if (prop) {
myRef.current++; // Bad!👎
}
return <button onClick={handler}>My button</button>;
}
As we can persist useRef values between renders, we can use this hook to keep track of previous state values.
import { useState, useEffect, useRef } from "react";
function MyComponent() {
const [inputValue, setInputValue] = useState("");
const previousInputValue = useRef("");
useEffect(() => {
previousInputValue.current = inputValue;
}, [inputValue]);
return (
<>
<input type="text" value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<h2>Current Value: {inputValue}</h2>
<h2>Previous Value: {previousInputValue.current}</h2>
</>
);
}
React useReducer
Hook
The useReducer Hook is similar to the useState Hook which also allows you to manage state and re-render a component whenever that state changes. The idea behind useReducer
is it gives you a more concrete way to handle complex states. It gives you a set of actions that you can perform in your state and it's going to convert your current state to a new version of the state based on the action that you send it. If you're familiar with redux, the useReducer hook has a very similar pattern to redux.
useReducer(<reducer>, <initialState>)
The useReducer Hook accepts two arguments: reducer and initialState.
The hook then returns an array of 2 items: the current state and the dispatch function.
import { useReducer } from 'react';
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
const action = {
type: 'ActionType'
};
return (
<button onClick={() => dispatch(action)}>
Click me
</button>
);
}
Now, let's first understand what the terms initial state, action object, dispatch, and reducer mean.
Initial state: This can be a simple value the state is initialized with, but generally contain an object. For instance, in the case of user state, the initial value could be:
// initial state
const initialState = {
users: []
};
Action Object: It describes how to update the state. The property type of an action object is often a string defining the kind of state update that the reducer needs to do. For example
const action = {
type: 'ADD_USER'
};
You can add more attributes to the action object if the reducer requires it to contain a payload.
const action = {
type: 'ADD_USER',
user: {
name: 'John Doe',
email: '[email protected]'
}
};
Dispatch function: It is a special function that dispatches an action object. It is created by the useReducer hook.
const [state, dispatch] = useReducer(reducer, initialState);
You can simply call the dispatch function with the appropriate action object: dispatch(actionObject)
to update the state.
Reducer function: It is a pure function that contains your custom logic. It accepts 2 parameters: the current state and an action object. The reducer function must update the state in an immutable way, depending on the action object, and return the new state.
The following reducer function adds the user to the state.
const initialState = {
users: [],
}
const userReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_USER':
return {
...state,
users: [...state.users, action.payload],
}
default:
return state
}
}
Wiring everything
import React, { useReducer } from 'react'
const initialState = {
users: [],
}
const userReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_USER':
return {
...state,
users: [...state.users, action.payload],
}
default:
return state
}
}
function Reducer() {
const [users, dispatch] = useReducer(userReducer, initialState)
console.log(JSON.stringify(users, null, 2))
const payload = {
name: 'John Smith',
email: '[email protected]',
}
const addUser = () => {
dispatch({ type: 'ADD_USER', payload })
}
return (
<div>
<button onClick={addUser}>Add User</button>
</div>
)
}
export default Reducer
From the above code snippet, when you click the Add User
button, the function dispatches an action object of the type ADD_USER
which adds the user to the state and logs the new state in the console. You can add more complex logic like updating, and deleting a user inside the single reducer function by adding more actions.
React useCallback
Hook
Let's first discuss performance optimization before moving on. The render time must be taken into account whenever we create a React app. We can enhance our application performance if we can reduce the time taken to render the DOM. One way to achieve this will be by preventing unnecessary renders of our components. This might not have great performance improvement for a small application, but if we have a large application with many components, we can have a serious performance boost 🚀.
The React useCallback returns a memoized version of the callback function that changes only when one of the dependencies has changed.
You can think of memoization as caching, so it returns a cached version of the result (callback function) which can boost the performance.
It is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.
Why We Need a useCallback Function
One reason to use the useCallback function is to prevent unnecessary re-rendering of the components. The component should not re-render unless its props have been changed.
Let's see an example.
import React, { useState } from 'react';
import User from './User';
function CallbackHookDemo() {
const [users, setUsers] = useState([]);
const [darkMode, setDarkMode] = useState(false);
const addUser = () => {
setUsers((u) => [...u, 'New User added !!']);
};
const handleCheckboxChange = () => setDarkMode((prev) => !prev);
return (
<>
<User users={users} addUser={addUser} />
<hr />
<div className={darkMode ? 'dark-mode' : ''}>
<label htmlFor="darkMode">dark mode</label>
<input
name="darkMode"
type="checkbox"
checked={darkMode}
onChange={handleCheckboxChange}
/>
</div>
</>
);
}
export default CallbackHookDemo;
When you run this code and check the dark mode checkbox, you will notice that the User
component re-renders even when the users
do not change.
Why is this happening?🤔 The User
component should not re-render since neither the users
state nor the addUser
function are changing when the checkbox is checked.
This is due to referential equality. Functions get recreated when a component re-renders. That's why the addUser
function has changed.
To fix this, we can use the useCallback hook to prevent unnecessary re-render of the component unless necessary. We can now wrap the addUser
function inside the useCallback hook like the below code.
const addUser = useCallback(() => {
setUsers((u) => [...u, 'New User added !!']);
}, [users]);
Now, the User
component will only re-render when the users
prop changes.
React useMemo
Hook
The react useMemo hook returns a memoized value that only runs when one of its dependencies changes.
UseMemo
and useCallback
hooks have some similarities. The main difference is that UseMemo returns a memoized value whereas useCallback returns a memoized function.You might have some expensive and resource-intensive functions in your React application. If those functions are run on every re-render of the component, the application will have a massive performance issue which can be costly in either memory, time, or processing and it will also lead to poor user experience.
You can try to optimize the performance of the application by utilizing the memoization technique.
useMemo hook tries to address the following two problems.
- referential equality
- computationally expensive operations
useMemo()
is a built-in React hook that accepts 2 arguments — a function compute
that computes a result and the depedencies
array:
const memoizedResult = useMemo(compute, dependencies);
On the first render, useMemo(compute, dependencies)
invokes compute
, remembers the calculated output and returns it to the component.
In the subsequent renders, the compute
functions would not need to run again as long as the dependencies do not change. It will simply return the memoized output.
Now let's see how useMemo()
works in an example.
import React, { useState } from 'react'
const expensiveCalculation = (num) => {
console.log('Calculating...')
for (let i = 0; i < 1000000000; i++) {
num += 1
}
return num
}
function UseMemoHookExample() {
const [users, setUsers] = useState([])
const [count, setCount] = useState(0)
const calculation = expensiveCalculation(count)
const addUser = () => {
setUsers((u) => [...u, 'New User added !!'])
}
const increment = () => {
setCount((c) => c + 1)
}
return (
<>
<h2>Users:</h2>
{users.map((user, i) => {
return <p key={i}>{user}</p>
})}
<button onClick={addUser}>Add User</button>
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
<h2>Expensive Calculation</h2>
{calculation}
</div>
</>
)
}
export default UseMemoHookExample
In this example, you will notice that when you increase the count or add a user, there is a delay in execution.
Even if you only add a user, the component re-renders, and the expensive calculation function gets invoked resulting in a delayed execution.
Let's improve the performance of this component by using useMemo
hook which will memoize the result of the expensive function.
import React, { useMemo, useState } from 'react'
const expensiveCalculation = (num) => {
console.log('Calculating...')
for (let i = 0; i < 1000000000; i++) {
num += 1
}
return num
}
function UseMemoHookExample() {
const [users, setUsers] = useState([])
const [count, setCount] = useState(0)
const calculation = useMemo(() => expensiveCalculation(count), [count])
const addUser = () => {
setUsers((u) => [...u, 'New User added !!'])
}
const increment = () => {
setCount((c) => c + 1)
}
return (
<>
<h2>Users:</h2>
{users.map((user, i) => {
return <p key={i}>{user}</p>
})}
<button onClick={addUser}>Add User</button>
<hr />
<div>
Count: {count}
<button onClick={increment}>+</button>
<h2>Expensive Calculation</h2>
{calculation}
</div>
</>
)
}
export default UseMemoHookExample
Now, when the component renders for the first time, an expensive calculation function will be invoked and the result will be memoized so that when we click on add user, the function will not be invoked again as there is no change in dependency and the execution will be super fast.
React custom Hook
You can create your custom hook by using the react built-in hooks and extract your custom logic into a reusable function.
There are two rules for creating a custom hook:
- Custom hooks are prefixed with "use". For example, it could be named as
useFetch
- Custom hooks consist of built-in or other custom hooks. A custom Hook is not a custom Hook and should not begin with the prefix "use" if it does not internally utilize any hooks.
Let's create a custom hook that will fetch a random joke from the API.
We created a new file and named it useFetch.js which contains our custom logic to fetch data from the API endpoint.
In App.js
, we are importing our useFetch
Hook and utilize it like any other Hook. This is where we pass in the URL to fetch data.
Now we can reuse this custom Hook in any component to fetch data from any URL.
Conclusion
This article explored built-in react hooks and their appropriate use in the React application. If you like the article then please share it and feel free to ask any questions you may have in the comment section below.