Hooks are a powerful feature introduced in React v16.8 that allow you to use state and other React features without writing a class. They enable functional components to manage state, handle side effects, and more—making your code simpler and easier to understand. For example, you can manage a component’s state using the useState Hook, replacing the need for class-based components.
Hooks bring flexibility and cleaner code to modern React development, unlocking new possibilities for functional programming within React applications.
Why Hooks ?
- Need to understand how
thiskeyword works in JS for classes. - Need to remember to bind event handlers in class components.
- Classes don’t minify well for hot reloading.
- Hard to reuse stateful component logic (HOC and render props are not a option always).
- Class code are harder to follow.
- There is a need to share stateful logic in a better way.
- Data fetching and subscribing to events are not organized in one place. Eg: Data fetching in componentDidMount and componentDidUpdate Eg: Event listeners in componentDidMount and componentWillUnmount
- Because of stateful logic, its hard to split components to smaller ones.
- Hooks doesn’t work inside classes.
- Completely opt in.
- 100% backward compatible since Classes won’t be removed from react.
Table of contents
Open Table of contents
Rules of Hooks
- Only call hooks at the top level i.e don’t call hooks inside loops, conditions, or nested functions.
- Only call hooks from react functions i.e call them from within react functional components and not just any regular javascript function.
useState hook
useState is a hook which allows to add react state to functional component. In classes, the state is always an object, but in hooks it can string, boolean, array or objects.
const [message, setMessage] = useState("Hello, world!");
The useState hook returns an array with 2 elements. The first element is the current value of the state, and the second element is a state setter function.
- If the new state value depends on the previous state value, pass a function to the setter function which have access to previous state value.
- When dealing with objects or arrays, use spread operator to spread the state variable and then call the setter function.
Example
import React, { useState } from "react";
function ExampleComponent() {
// String state
const [message, setMessage] = useState("Hello, world!");
// Array state
const [list, setList] = useState(["apple", "banana", "orange"]);
// Object state
const [person, setPerson] = useState({
name: "John Doe",
age: 25,
profession: "Developer",
});
const updateName = () => {
setPerson(prevPerson => ({
...prevPerson,
name: "Jane Smith",
}));
};
const addItem = () => {
setList(prevList => [...prevList, "grape"]);
};
return (
<div>
<p>{message}</p>
<button onClick={() => setMessage("Welcome!")}>Change Message</button>
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
<button onClick={addItem}>Add Item</button>
<p>Name: {person.name}</p>
<p>Age: {person.age}</p>
<p>Profession: {person.profession}</p>
<button onClick={updateName}>Update Name</button>
</div>
);
}
export default ExampleComponent;
useEffect hook
The useEffect hook allows to perform side effects in functional components. It is a close replacement for componentDidMount, componentDidUpdate and componentWillUnmount [[Life Cycle Methods]]. It runs on every render of the component.
useEffect(() => {
// Side effect code
console.log("Component rendered");
// Cleanup function
return () => {
console.log("Component unmounted");
};
}, []); // Empty dependencies array, effect runs only once
The useEffect function takes two arguments: a callback function and an optional dependencies array.
- The callback function is the main body of the effect and is executed after the component renders. It can contain any code that performs the desired side effect.
- This function may also return a cleanup function, which will be invoked before the effect runs again or when the component unmounts.
- The dependencies array is an optional second argument. It is used to specify the values that the effect depends on. If any of the values in the dependencies array change, the effect will re-run.
- If the dependencies array is omitted, the effect runs after every render. If the dependencies array is an empty array (
[]), the effect runs only once, similar tocomponentDidMount. - It’s common to have multiple
useEffecthooks in a component, each responsible for a specific side effect or set of dependencies.
Example
import React, { useState, useEffect } from "react";
function ExampleComponent() {
const [data, setData] = useState(null);
const [id, setId] = useState(1);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
const response = await fetch(`https://api.example.com/data/${id}`);
const data = await response.json();
setData(data);
setIsLoading(false);
} catch (error) {
console.error(`Error fetching data for ID ${id}:`, error);
setIsLoading(false);
}
};
fetchData();
return () => {
// Cancel any ongoing API request or perform cleanup if needed
};
}, [id]);
useEffect(() => {
const handleScroll = () => {
// DOM manipulation code here
};
if (data) {
window.addEventListener("scroll", handleScroll);
} else {
window.removeEventListener("scroll", handleScroll);
}
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [data]);
return (
<div>
<select value={id} onChange={e => setId(Number(e.target.value))}>
<option value={1}>ID 1</option>
<option value={2}>ID 2</option>
<option value={3}>ID 3</option>
</select>
{isLoading ? (
<p>Loading data...</p>
) : (
data && (
<ul>
<li>ID: {data.id}</li>
<li>Name: {data.name}</li>
<li>Description: {data.description}</li>
</ul>
)
)}
</div>
);
}
export default ExampleComponent;
useContext hook
The Context API in React provides a way to share data between components without having to pass props manually through each level of the component tree. It eliminates the need for prop drilling, which can become cumbersome and make the code harder to maintain as the application grows.
The useContext hook is a hook provided by React that allows functional components to consume values from a Context API.
Example
import React, { createContext, useContext } from "react";
// Create a context
const ThemeContext = createContext();
// A component that uses the context
function ThemeButton() {
const theme = useContext(ThemeContext);
return <button style={{ background: theme.primary }}>Click me!</button>;
}
// A component that provides the context value
function App() {
const theme = {
primary: "blue",
secondary: "gray",
};
return (
<ThemeContext.Provider value={theme}>
<div>
<h1>Welcome to My App</h1>
<ThemeButton />
</div>
</ThemeContext.Provider>
);
}
export default App;
useReducer hook
useReducer is a hook used for state management. It is an alternative to useState hook. In fact useState hook is built using primitive useReducer hook.
const [state, dispatch] = useReducer(reducer, initialState);
The useReducer hook takes two arguments: the reducer function and the initial state. The reducer function is responsible for handling state updates based on dispatched actions, while the initial state sets the initial value of the state.
Example
import React, { useReducer } from "react";
// Reducer function
const reducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
case "RESET":
return { count: 0 };
default:
return state;
}
};
// Component using useReducer
function Counter() {
const initialState = { count: 0 };
const [state, dispatch] = useReducer(reducer, initialState);
const increment = () => {
dispatch({ type: "INCREMENT" });
};
const decrement = () => {
dispatch({ type: "DECREMENT" });
};
const reset = () => {
dispatch({ type: "RESET" });
};
return (
<div>
<p>Count: {state.count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default Counter;
Instead of using [[Redux]], we can utilize the useReducer and useContext hooks provided by React for state management. useReducer allows you to handle state updates using a reducer function and dispatched actions, similar to [[Redux]]. useContext enables sharing and consuming data across components using the Context API. By combining these hooks, you can manage state efficiently without the need for [[Redux]], simplifying your codebase for smaller to medium-sized applications.
Example for state management
import React, { useEffect, useReducer } from "react";
// Reducer function
const reducer = (state, action) => {
switch (action.type) {
case "FETCH_INIT":
return { ...state, isLoading: true, error: null };
case "FETCH_SUCCESS":
return { ...state, isLoading: false, error: null, data: action.payload };
case "FETCH_ERROR":
return { ...state, isLoading: false, error: action.payload };
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
// Component using useReducer for data fetching
function DataFetcher() {
const initialState = {
isLoading: false,
error: null,
data: null,
};
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const fetchData = async () => {
dispatch({ type: "FETCH_INIT" });
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
dispatch({ type: "FETCH_SUCCESS", payload: data });
} catch (error) {
dispatch({ type: "FETCH_ERROR", payload: error.message });
}
};
fetchData();
}, []);
if (state.isLoading) {
return <p>Loading data...</p>;
}
if (state.error) {
return <p>Error: {state.error}</p>;
}
if (state.data) {
return (
<div>
<h2>Data</h2>
<ul>
{state.data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
return null;
}
export default DataFetcher;
While useReducer and useContext can be used for state management, they may not be the optimal choice for highly complex state transitions. [[Redux]], with its built-in middleware and powerful ecosystem, provides better tools for handling complex state management scenarios. It’s important to carefully evaluate the complexity of your state transitions and consider whether Redux would be more suitable for your specific use case.
useCallback hook
The useCallback hook is used in React to memoize functions and optimize performance. It is particularly useful when passing callbacks to child components that rely on reference equality to prevent unnecessary re-rendering.
useCallback is a hook that will return a memoized version of the callback function that only changes if one of the dependencies has changed.
const memoizedCallback = useCallback(() => {
// Callback logic
}, []); // [] means that the memoized callback will remain the same across re-renders.
The useCallback hook takes two arguments: the callback function and a dependency array. It returns a memoized version of the callback function that only changes if any of the dependencies in the array change. By doing so, it helps prevent unnecessary re-creation of the callback function on each re-render.
Example
import React, { useState, useCallback } from "react";
function ParentComponent() {
const [name, setName] = useState("");
const handleNameChange = useCallback(event => {
setName(event.target.value);
}, []);
return (
<div>
<input type="text" value={name} onChange={handleNameChange} />
<ChildComponent onSave={handleSave} />
</div>
);
}
function ChildComponent({ onSave }) {
// Rest of the component logic
return <button onClick={onSave}>Save</button>;
}
export default ParentComponent;
By using useCallback to memoize the function, we ensure that the ChildComponent won’t re-render unnecessarily when other state or props change in the ParentComponent.
useMemo hook
The useMemo hook in React is used to memoize a value and optimize performance. It allows you to memoize expensive computations or calculations, ensuring that they are only re-evaluated when the dependencies change.
const result = useMemo(() => {
// Expensive computation based on 'a' and 'b'
// Return the computed result
}, [a, b]);
The useMemo hook takes two arguments: a function that computes the value and an array of dependencies. It returns the memoized value, which is recalculated only if any of the dependencies in the array change.
Example
import React, { useState, useMemo } from "react";
function FibonacciCalculator({ n }) {
const calculateFibonacci = num => {
if (num <= 1) {
return num;
}
return calculateFibonacci(num - 1) + calculateFibonacci(num - 2);
};
const fibonacciResult = useMemo(() => calculateFibonacci(n), [n]);
return (
<div>
<p>
Fibonacci of {n} is: {fibonacciResult}
</p>
</div>
);
}
function App() {
const [number, setNumber] = useState(10);
const handleNumberChange = event => {
setNumber(parseInt(event.target.value));
};
return (
<div>
<input type="number" value={number} onChange={handleNumberChange} />
<FibonacciCalculator n={number} />
</div>
);
}
export default App;
By utilizing useMemo, we memoize the result of the Fibonacci calculation based on the input n. The memoized value is recalculated only when n changes, ensuring that the calculation is not repeated unnecessarily.
It’s important to note that useMemo should be used judiciously, as overusing it can lead to unnecessary complexity and potential performance issues. It’s best suited for cases where the calculation is expensive and the value is needed in multiple places within the component or its descendants.
useRef hook
The useRef hook in React is used to create a mutable value that persists across renders. Unlike state variables, changes to a ref value don’t trigger a re-render of the component. It provides a way to maintain mutable data or references to DOM elements.
const ref = useRef("initial value");
The useRef hook accepts an optional initial value as its input parameter. This initial value can be any JavaScript value, such as a primitive value, an object, or null.
Example
import React, { useRef, useState, useEffect } from "react";
function Timer() {
const intervalRef = useRef(null);
const [count, setCount] = useState(0);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, []);
return (
<div>
<p>Timer: {count}</p>
<button onClick={() => clearInterval(intervalRef.current)}>Stop</button>
</div>
);
}
export default Timer;
In this example, we have a Timer component that uses useRef to create a reference to the interval timer. The count state is used to keep track of the elapsed time.
Using useRef in this example allows us to maintain a reference to the interval timer across renders and update the count without triggering re-renders. It provides a way to manage side effects, such as starting and stopping timers, in a clean and efficient manner.
Also note that in a class component, you can directly store the interval ID as an instance variable, making it easily accessible throughout the component’s methods. This allows you to easily pause the timer using clearInterval(this.intervalId). In functional components, on the other hand, using useRef is the only option to create a mutable reference.
Custom hooks
Custom hooks are reusable functions in React that allow you to extract and reuse stateful logic across multiple components. Custom hooks enable you to create your own abstractions and encapsulate complex logic, making your code more modular and easier to maintain.
Rules of custom hooks
- Custom hooks should be named with the word “use” at the beginning.
- Aim to create custom hooks that can be reused across different components or projects.
- Hooks should only be called at the top level of custom hooks or functional components, avoiding conditional or looped calls.
- Custom hooks have the ability to manage state and perform side effects using built-in hooks.
- Properly handle dependencies by including them in the dependency array of hooks like useEffect.
Example
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Error fetching data");
}
const data = await response.json();
setData(data);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, isLoading, error };
}
function MyComponent() {
const { data, isLoading, error } = useFetch("https://api.example.com/data");
if (isLoading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return (
<div>
{data && (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
);
}
By using the useFetch custom hook, you can handle data fetching and state management in an elegant and reusable way. The returned parameters allow you to access the fetched data, loading status, and error information, enabling you to render the appropriate UI based on the current state of the data fetch.
Conclusion
Hooks have transformed the way we write React components, providing a cleaner, more intuitive approach to managing state and other React features. By eliminating the need for class components, they simplify code while enabling powerful functionality within functional components.