React Hooks Demystified: Harnessing the Full Potential of Hooks for Modern Web Apps

Nipuna Upeksha
10 min readFeb 1, 2024

👋 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.

References

  1. React Hooks Reference
  2. React Key Concepts: Consolidate your knowledge of React’s core features by Maximilian Schwarzmuller
  3. React Hooks in Action: With Suspense and Concurrent Mode by John Larsen

--

--

Nipuna Upeksha
Nipuna Upeksha

Written by Nipuna Upeksha

Software Engineer | Visiting Lecturer | AWS SAA | MSc. in Big Data Analytics