Last updated: April 13, 2025
Table of Contents
1. Introduction to React Hooks
React Hooks, introduced in React 16.8, revolutionized how developers write React components. They allow you to use state and other React features in functional components, eliminating the need for class components in many cases. Hooks make components cleaner, more reusable, and easier to test. This article provides a deep dive into the most essential Hooks: useState
, useEffect
, useContext
, and explores how to create your own custom Hooks.
2. The `useState` Hook
The useState
Hook is the most fundamental hook. It allows you to add state variables to functional components.
2.1 Managing Simple State
useState
takes an initial state value as an argument and returns an array containing two elements: the current state value and a function to update that value.
import React, { useState } from 'react';
function Counter() {
// Declare a state variable 'count' initialized to 0
// 'setCount' is the function to update 'count'
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Counter;
In this example, useState(0)
initializes the count
state to 0
. Clicking the button calls setCount
, passing the new value (count + 1
), which triggers a re-render with the updated count.
2.2 Functional Updates
If the new state depends on the previous state, it's safer to pass a function to the state updater. This function receives the previous state and returns the new state. This prevents potential issues with stale state closures.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const incrementThreeTimes = () => {
// Using functional updates ensures each increment uses the latest state
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementThreeTimes}>+3</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
export default Counter;
Using setCount(prevCount => prevCount + 1)
guarantees that each update builds upon the correct previous value, even when updates are batched together.
3. The `useEffect` Hook
The useEffect
Hook lets you perform side effects in functional components. Common side effects include data fetching, setting up subscriptions, and manually changing the DOM.
3.1 Handling Side Effects
useEffect
runs after every render by default. You pass it a function (the "effect") that contains the imperative code.
import React, { useState, useEffect } from 'react';
function DocumentTitleUpdater() {
const [count, setCount] = useState(0);
// useEffect runs after every render
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
console.log('Document title updated!');
}); // No dependency array: runs after every render
return (
<div>
<p>You clicked {count} times (Check the document title!) setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default DocumentTitleUpdater;
This effect updates the document's title whenever the count
state changes (triggering a re-render).
3.2 Cleanup & Dependency Array
Often, effects require cleanup, like unsubscribing from a service or clearing a timer. You can return a function from your effect, which React will run before the component unmounts or before the effect runs again.
To control when an effect runs, you provide a dependency array as the second argument to useEffect
. The effect only re-runs if values in the array have changed since the last render. An empty array []
means the effect runs only once after the initial render.
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Start interval
const intervalId = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
console.log('Timer effect started');
// Cleanup function: runs on unmount or before effect re-runs
return () => {
clearInterval(intervalId);
console.log('Timer effect cleaned up');
};
}, []); // Empty dependency array: effect runs only once on mount
return <div>Timer: {seconds}s</div>;
}
export default Timer;
In this Timer example, the effect sets up an interval on mount and clears it on unmount, thanks to the cleanup function and the empty dependency array []
.
If the effect depends on props or state, include them in the dependency array to ensure the effect re-runs when they change. For example useEffect(() => { /* ... */ }, [propA, stateB]);
.
4. The `useContext` Hook
The useContext
Hook provides a way to pass data through the component tree without having to pass props down manually at every level (prop drilling).
4.1 Avoiding Prop Drilling
Imagine having a theme (e.g., 'light' or 'dark') needed by many components deep in the tree. Passing it down via props is cumbersome. Context provides a cleaner solution.
4.2 Creating and Providing Context
First, create a Context object using React.createContext
. Then, use the Context.Provider
component higher up the tree to wrap components that need access to the context value. Finally, components that need the data use the useContext
hook.
import React, { useState, useContext, createContext } from 'react';
// 1. Create Context
const ThemeContext = createContext('light'); // Default value
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
// 2. Provide Context Value
return (
<ThemeContext.Provider value={theme}>
<div className={`app ${theme}`}>
<h1>Context Example</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
<Toolbar />
</div>
</ThemeContext.Provider>
);
}
function Toolbar() {
// Toolbar doesn't need the theme itself, but contains components that do
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
// 3. Consume Context Value
const theme = useContext(ThemeContext);
return (
<button className={`button ${theme}`}>
I am a {theme} themed button
</button>
);
}
export default App;
/* Add basic CSS for themes:
.app.light { background-color: #fff; color: #333; }
.app.dark { background-color: #333; color: #fff; }
.button.light { background-color: #eee; color: #333; border: 1px solid #ccc; }
.button.dark { background-color: #555; color: #fff; border: 1px solid #777; }
*/
useContext(ThemeContext)
finds the nearest ThemeContext.Provider
above it in the tree and returns its value
prop.
5. Building Custom Hooks
Custom Hooks are a powerful mechanism for code reuse in React. They allow you to extract component logic into reusable functions.
5.1 Why Create Custom Hooks?
If you find yourself writing the same stateful logic or side effects in multiple components (e.g., fetching data, subscribing to events, form handling), you can extract that logic into a custom Hook. This promotes cleaner components and better organization.
A custom Hook is simply a JavaScript function whose name starts with "use"
and that can call other Hooks (like useState
or useEffect
).
5.2 Example: A `useFetch` Hook
Let's create a simple custom Hook to handle fetching data from an API.
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Prevent race conditions if URL changes quickly
const abortController = new AbortController();
const signal = abortController.signal;
setLoading(true);
setError(null);
fetch(url, { signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(fetchedData => {
setData(fetchedData);
setError(null);
})
.catch(fetchError => {
if (fetchError.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(fetchError);
setData(null);
}
})
.finally(() => {
// Only set loading to false if the request wasn't aborted
if (!signal.aborted) {
setLoading(false);
}
});
// Cleanup function to abort fetch if component unmounts or URL changes
return () => {
abortController.abort();
};
}, [url]); // Re-run effect if the URL changes
return { data, loading, error };
}
export default useFetch;
Now, you can use this Hook in any component:
import React from 'react';
import useFetch from './useFetch'; // Assuming useFetch is in a separate file
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error fetching data: {error.message}</p>;
if (!user) return null;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Website: {user.website}</p>
</div>
);
}
export default UserProfile;
The component UserProfile
is now much simpler, focusing only on rendering the UI based on the state provided by the useFetch
Hook.
6. Conclusion
React Hooks provide a more direct API to the React concepts you already know: state, lifecycle, context, and refs. They enable writing simpler, more powerful functional components and facilitate logic reuse through custom Hooks. Mastering useState
for state management, useEffect
for side effects, useContext
for accessing shared data, and creating custom Hooks is essential for modern React development.
7. Additional Resources
Related Articles
- Frontend State Management Approaches
- JavaScript Framework Comparison
- Introduction to TypeScript
- JavaScript: var vs let vs const Explained
- REST vs GraphQL: A Comparison