React Hooks Demystified: Harnessing the Full Potential of Hooks for Modern Web Apps
👋 Introduction
In this article, we will be looking at the most common React hooks that are essential to developing seamless applications.
🎣 What are Hooks?
Hooks are special functions that you can use with React functional components. These hooks allow you to “hook into” the React lifecycle and let you change the state and behavior of a component. It is important to remember that you can only use hooks with React functional components.
🔄 useState
This is probably the most important hook that you may use when you are developing a React application. This hook allows you to add a state
variable to your components.
Let’s assume we need to create a simple application with + and — symbols and when clicked they update the value given between <span>...</span>
tags. For that, we can simply write the following code with simple HTML tags.
import React from 'react';
const App = () => {
return (
<>
<button>-</button>
<span>0</span>
<button>+</button>
</>
);
}
export default App;
The issue with the above code is when the user clicks the buttons they won’t update the result between the <span>...</span>
tags.
To make sure the above component works properly, what we should do is use the useState
hook.
When you are using useState
hook, it returns two important parameters that you can associate with your functional component, the state
and the function that lets you interact with the state
. And also it takes an initial parameter for your initial state. If you want to change the count of the above function, you can simply output them as shown below with the initial value being 0.
const [count, setCount] = useState(0)
import React, {useState} from 'react';
const App = () => {
const [count, setCount] = useState(0);
return (
<>
<button>-</button>
<span>0</span>
<button>+</button>
</>
);
}
export default App;
Since we need to increment and decrement the count whenever we click the + or — buttons, we can create two separate functions for them and add them to onClick
events of the buttons.
import React, {useState} from 'react';
const App = () => {
const [count, setCount] = useState(0);
function incrementCount() {
setCount(count + 1);
}
function decrementCount() {
setCount(count - 1);
}
return (
<>
<button onClick={decrementCount}>-</button>
<span>{count}</span>
<button onClick={incrementCount}>+</button>
</>
);
}
export default App;
Although you may think the above code is correct, it is not the way to update a value based on the previous value. To update the value using the previous value, we need to pass an anonymous function to setCount
method.
import React, {useState} from 'react';
const App = () => {
const [count, setCount] = useState(0);
function incrementCount() {
setCount(prevCount => prevCount + 1);
}
function decrementCount() {
setCount(prevCount => prevCountcount - 1);
}
return (
<>
<button onClick={decrementCount}>-</button>
<span>{count}</span>
<button onClick={incrementCount}>+</button>
</>
);
}
export default App;
This is how you can use the useState
hook in your React program. But, there are a few additional things that you know about useState
hook.
The first thing is you don’t necessarily have to set the initial value using a hardcoded value. You can use an anonymous function to set the value. For example, you can set the initial value for count
like this.
...
const [count, setCount] = useState(()=>{
console.log('run function');
return 0;
});
...
Secondly, if you wish to update multiple values passed as a JSON object, then you have to use the spread operator (…) to make sure only the value you need to update is updated and others are not changed. For example, check the below code. We need to only change the operation
and value
variable whenever we do an increment or decrement operation, but we don't want to change the user
variable.
...
const [count, setCount] = useState({
value:0,
operation:"increment",
user:"rachel"
});
function incrementCount(){
setCount(prevCount => {
return {
...prevCount,
operation:"increment",
value: prevCount.value + 1
}
});
}
function decrementCount(){
setCount(prevCount => {
return {
...prevCount,
operation:"decrement",
value: prevCount.value - 1
}
});
}
...
Finally, rather than using complex objects like the above, you can define multiple useState
hooks to change the state of the variables as shown below.
...
const [count, setCount] = useState(0);
const [operation, setOperation] = useState("increment");
const [user, setUser] = useState("rachel);
...
🧪 useEffect
useEffect
is another important hook that every developer should have in their arsenal. It allows you to perform side effects on your components. Some examples of side effects are,
- Fetching data
- Directly updating the DOM
- Using timers
useEffect
hook accepts two arguments. The first one is a function where you can tell what side effects you need to perform, and the other one is an optional dependency array. If you add any dependencies to this array, the useEffect
hook performs side effects whenever the value of one of those dependencies is changed.
Now let’s look at a simple example.
import React, {useState, useEffect} from 'react';
const App = () => {
const [resourceType, setResourceType] = useState("posts");
useEffect(()=>{
console.log("rendering");
});
return (
<>
<div>
<button onClick=(()=>setResourceType("posts"))>posts</button>
<button onClick=(()=>setResourceType("comments"))>comments</button>
<button onClick=(()=>setResourceType("albums"))>albums</button>
</div>
<h1>{resourceType}</h1>
</>
);
}
export default App;
As you can see, we are using the useEffect
without using the second parameter, therefore, whenever we click on a button, the state of the functional component changes and useEffect
hook performs a side effect, in our case, it prints out rendering...
So what will happen if we introduce an empty dependency array as the second argument as shown below?
import React, {useState, useEffect} from 'react';
const App = () => {
const [resourceType, setResourceType] = useState("posts");
useEffect(()=>{
console.log("rendering");
},[]);
return (
<>
<div>
<button onClick=(()=>setResourceType("posts"))>posts</button>
<button onClick=(()=>setResourceType("comments"))>comments</button>
<button onClick=(()=>setResourceType("albums"))>albums</button>
</div>
<h1>{resourceType}</h1>
</>
);
}
export default App;
Now, since it does not have any dependency to rely on whenever a button is clicked, it will run only once at the functional component initialization. Therefore, you will only see one rendering...
statement printed out. This is very useful when you are fetching some data from an API because we don't want to run the API again and again.
Now, let’s see what will happen if we introduce a dependency to the dependency array.
import React, {useState, useEffect} from 'react';
const App = () => {
const [resourceType, setResourceType] = useState("posts");
const [items, setItems] = useState([]);
useEffect(()=>{
fetch(`https://jsonplaceholder.typicode.com/${resourceType}`)
.then(res => res.json())
.then(json => setItems(json));
}, [resourceType]);
return (
<>
<div>
<button onClick=(()=>setResourceType("posts"))>posts</button>
<button onClick=(()=>setResourceType("comments"))>comments</button>
<button onClick=(()=>setResourceType("albums"))>albums</button>
</div>
<h1>{resourceType}</h1>
{items.map((item)=>{
<em>{JSON.stringify(item)}</em>
})}
</>
);
}
export default App;
Now, whenever we click on the buttons we will get a random list of outputs of posts, comments, or albums since the useEffect
hook's side effects depend on the resourceType
dependency.
The final note on useEffect
is that you can use a clean-up function inside useEffect
hook so that, you can clean up any created timers or objects which will degrade the performance of your application. If you use the clean-up function with useEffect
it will run every time before the useEffect
hook is executed.
...
useEffect(()=>{
console.log("resource changed");
return () => {
console.log("clean up function executed");
}
},[resourceType])
...
🧠 useMemo
The useMemo
hook returns a memoized value. You can think of memoization as caching a value so that it does not need to be recalculated. Since useMemo
hook only runs when one of its dependencies is updated, it can improve the performance of your application.
Let’s look at a simple use case of useMemo
hook.
import React, {useMemo} from 'react';
const App = ({value}) => {
const factorial = useMemo(()=>{
console.log("calculating");
calculateFactorial(value);
}, [value]);
function calculateFactorial(num) {
if (num === 0){
return 1;
} else {
return num * calculateFactorial( num - 1 );
}
}
}
export default App;
Here, useMemo
remembers the factorial
value based on the value
. Therefore, if the value
does not change, it just gives back the last factorial
value. But if the value
changes, it recalculates the factorial
value. This optimizes your application since high-cost functions are not run without checking the memoized value.
⚙️ useCallback
useCallback
hook is similar to useMemo
hook. But instead of memoizing the values, it is used to memoize an expensive function. A usage of useCallback
is shown below.
import React, {useState, useEffect} from 'react';
const List = ({getItems}) => {
const [items, setItems] = useState([]);
useEffect(()=>{
setItems(getItems())
console.log("updating...");
},[getItems]);
return items.map(item=><div key={item}>{item}</div>);
}
const App = () => {
const [number, setNumber] = useState(1);
const [dark, setDark] = useState(false);
const getItems = () => {
return [number, number + 1, number + 2];
}
const theme = {
backgroundColor: dark ? "#333" : "#fff",
color: dark ? "#fff" : "#333"
}
return (
<div style={theme}>
<input type="number" value={number} onChange={e=>setNumber(e.target.value))}/>
<button onClick={()=>setDark(prevDark=>!prevDark)}>Toggle theme</button>
<List getItems={getItems}/>
</div>
);
}
export default App;
Now if we change the number
in the input field, you will be able to see updating...
message in the developer tools. But, when you try to change the theme
you will also notice, updating...
message in the developer tools. This is happening because whenever you are updating either the number
or the theme
you are re-rendering the App
component, and generating a new set of values (identical, but fresh) for getItems
method. And due to that, you are seeing the updating...
method despite you change the number
or the theme
. To avoid this behavior, what we should do is memoize the getItems()
function. To do that, we can use the useCallback
hook.
import React, {useState, useEffect, useCallback} from 'react';
const List = ({getItems}) => {
const [items, setItems] = useState([]);
useEffect(()=>{
setItems(getItems())
console.log("updating...");
},[getItems]);
return items.map(item=><div key={item}>{item}</div>);
}
const App = () => {
const [number, setNumber] = useState(1);
const [dark, setDark] = useState(false);
const getItems = useCallback(() => {
return [number, number + 1, number + 2];
},[number])
const theme = {
backgroundColor: dark ? "#333" : "#fff",
color: dark ? "#fff" : "#333"
}
return (
<div style={theme}>
<input type="number" value={number} onChange={e=>setNumber(e.target.value))}/>
<button onClick={()=>setDark(prevDark=>!prevDark)}>Toggle theme</button>
<List getItems={getItems}/>
</div>
);
}
export default App;
📌 useRef
The useRef
hook allows you to persist values between renders. It can be used as a way to store a value that does not cause a re-render when updated. Also, it can be used to access a DOM element directly. Let's check out how you can useRef
. Since it allows you to persist values between renders, it can be used for a task like getting the render count.
import React, {useState, useEffect, useRef} from 'react';
const App = () => {
const [name, setName] = useState('');
const renderCount = useRef(0);
useEffect(()=>{
renderCount.current = renderCount.current + 1;
});
return (
<>
<input value={name} onChange={e=>setName(e.target.value)}/>
<div>Render count:{renderCount.current}</div>
</>
);
}
export default App;
Since useRef
acts as a secret storage that holds the values that are not needed for rendering, you should use this hook when you need to keep values that should stay consistent between the components.
Another frequent example of using useRef
is to focus inputs. The code snippet you can use for that is also shared here.
import { useRef } from 'react';
const App => () {
const inputRef = useRef();
function focusInput() {
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
export default App;
🧩 useReducer
This hook is similar to the useState
hook but it is more advanced than the useState
hook. Let's look at an example to get to know more about this hook.
import React, {useState} from 'react';
const App = () => {
const [count, setCount] = useState(0);
function increment() {
setCount(prevCount => prevCount + 1));
}
function decrement() {
setCount(prevCount => prevCount - 1));
}
return (
<>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</>
);
}
export default App;
As you learned in useState
hook, you know the above code works correctly and gives you the necessary outputs.
Now, let’s check how we can implement the same code with useReducer
hook. useReducer
hook takes two arguments and they are a reducer function and an initial state. This is more helpful when the useState
hook isn't enough.
Also, the useReducer
function interacts with a dispatch function which helps you to apply the rules you have defined in the reducer function. We can get the state and the dispatch function from the useReducer
hook like this.
const [state, dispatch] = useReducer(countReducer, {count: 0});
import React, {useReducer} from 'react';
const ACTIONS = {
INCREMENT:'increment',
DECREMENT:'decrement'
};
function countReducer(state, action){
switch (action.type){
case ACTIONS.INCREMENT:
return {count: state.count + 1};
case ACTIONS.DECREMENT:
return {count: state.count - 1};
default:
throw new Error(`The action type ${action.type} is incorrect`);
}
}
const App = () => {
const [state, dispatch] = useState(countReducer, {count: 0});
function increment() {
dispatch({type: ACTIONS.INCREMENT});
}
function decrement(){
dispatch({type: ACTIONS.DECREMENT});
}
return (
<>
<button onClick={decrement}>-</button>
<span>{state.count}</span>
<button onClick={increment}>+</button>
</>
);
}
export default App;
As you can see useReducer
is more complex than useState
. Therefore, you should use it when you have complex state logic.
🤝 useContext
useContext
hook allows you to share data with components easily rather than passing the data with props. If you pass data with props it can cause performance issues and it can also increase the code complexity. Therefore, this hook is very useful if you are seeing unwanted code complexity due to props drilling or passing the same data through multiple props.
// ClassContextComponent component
import React, {Component} from 'react';
import {ThemeContext} from './App';
export default class ClassContextComponent extends Component{
themeStyles(dark){
return {
backgroundColor: dark? '#333' : '#CCC';
color: dark ? '#CCC' : '#333';
}
}
render() {
return (
<ThemeContext.Consumer>
{darkTheme => {
return <div style={this.themeStyles(darkTheme)}>Class Theme</div>
}}
</ThemeContext.Consumer>
);
}
}
// FunctionContextComponent component
import React, {useContext} from 'react';
import {ThemeContext} from './App';
const FunctionContextComponent = () => {
const darkTheme = useContext(ThemeContext);
const themeStyles = {
backgroundColor: darkTheme? '#333' : '#CCC';
color: darkTheme ? '#CCC' : '#333';
}
return (
<div style={themeStyles}>Function Theme</div>
);
}
export default FunctionContextComponent;
// App component
import React, { useState } from 'react';
import FunctionContextComponent from './FunctionContextComponent';
import ClassContextComponent from './ClassContextComponent';
export const ThemeContext = React.createContext();
const App = () => {
const [darkTheme, setDarkTheme] = useState(true);
function toggleTheme(){
setDarkTheme(prevDarkTheme => !prevDarkTheme);
}
return (
<>
<ThemeContext.Provider value={darkTheme}>
<button onClick={toggleTheme}>Toggle Theme</button>
<FunctionalContextComponent/>
<ClassContextComponent/>
</ThemeContext.Provider>
</>
);
}
export default App;
You can see that using the context is different in class-based components and functional components. By using useContext
hook, we can simplify the code and avoid props drilling.