React Hooks are one of the most important concepts in modern React development.
They allow functional components to use state, manage side effects, access context, reference DOM elements, optimize performance, and reuse stateful logic.
Before Hooks were introduced, many React features required class components. Hooks changed that by making powerful React capabilities available directly inside function components.
In this guide, we'll explore the most important React Hooks, understand when to use them, avoid common mistakes, and build practical examples.
By the end, you'll understand not only how Hooks work, but also how to choose the right Hook for a particular problem.
1. What Are React Hooks?
Hooks are special functions provided by React that let function components use React features.
For example:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Here, useState allows a function component to remember a value between renders.
Without Hooks, function components were primarily used for presenting UI based on props.
With Hooks, function components can manage much more complex behavior.
Why Were Hooks Introduced?
Hooks help solve several problems:
- Reusing stateful logic between components.
- Avoiding large and complicated class components.
- Organizing related logic together.
- Managing component state and lifecycle behavior.
- Making React code easier to understand and maintain.
The most important idea is that Hooks let you organize component logic around features rather than lifecycle methods.
2. Rules of Hooks
Before learning individual Hooks, you must understand two important rules.
Rule 1: Only Call Hooks at the Top Level
Never call Hooks inside:
- Conditions.
- Loops.
- Nested functions.
- Event handlers.
-
try/catchblocks.
Incorrect:
function Profile({ isLoggedIn }) {
if (isLoggedIn) {
const [name, setName] = useState("Alex");
}
return <h1>Profile</h1>;
}
Correct:
function Profile({ isLoggedIn }) {
const [name, setName] = useState("Alex");
if (!isLoggedIn) {
return <p>Please log in.</p>;
}
return <h1>Hello, {name}</h1>;
}
Why?
React relies on the order of Hook calls to associate Hook state with the correct component.
If Hooks are called conditionally, their order can change between renders, causing bugs.
Rule 2: Only Call Hooks from React Functions
Hooks should be called from:
- React function components.
- Custom Hooks.
Incorrect:
function calculateSomething() {
const [value, setValue] = useState(0);
}
Correct:
function useCounter() {
const [value, setValue] = useState(0);
return { value, setValue };
}
Custom Hooks are discussed later in this article.
3. useState — Managing Component State
useState is probably the Hook you'll use most frequently.
It allows a component to store information that can change over time.
Basic Syntax
const [state, setState] = useState(initialValue);
It returns an array containing:
- The current state value.
- A function for updating that value.
Example:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>
Increase
</button>
<button onClick={() => setCount(count - 1)}>
Decrease
</button>
</div>
);
}
When setCount is called, React schedules an update and renders the component with the new state.
Updating State Based on Previous State
Sometimes you need to calculate the next state using the previous state.
In this situation, use the functional updater form.
setCount(previousCount => previousCount + 1);
For example:
function Counter() {
const [count, setCount] = useState(0);
function increaseThreeTimes() {
setCount(value => value + 1);
setCount(value => value + 1);
setCount(value => value + 1);
}
return (
<div>
<h2>{count}</h2>
<button onClick={increaseThreeTimes}>
Increase by 3
</button>
</div>
);
}
Why use this approach?
State values are snapshots associated with a particular render. Multiple updates that depend on the previous value should use updater functions to avoid calculating from an outdated snapshot.
Managing Objects with useState
You can also store objects in state.
function UserProfile() {
const [user, setUser] = useState({
name: "Alex",
age: 25,
city: "Berlin"
});
function updateAge() {
setUser(previousUser => ({
...previousUser,
age: previousUser.age + 1
}));
}
return (
<div>
<h2>{user.name}</h2>
<p>Age: {user.age}</p>
<p>City: {user.city}</p>
<button onClick={updateAge}>
Increase Age
</button>
</div>
);
}
Notice the use of the spread operator:
{
...previousUser,
age: previousUser.age + 1
}
React state should be treated as immutable.
Instead of modifying the existing object, create a new object with the updated properties.
Managing Arrays with useState
function TodoList() {
const [todos, setTodos] = useState([
"Learn React",
"Practice JavaScript"
]);
function addTodo() {
setTodos(previousTodos => [
...previousTodos,
"Build a project"
]);
}
return (
<div>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
<button onClick={addTodo}>
Add Todo
</button>
</div>
);
}
For lists that can be reordered, inserted into, or deleted from, use stable unique IDs instead of array indexes as keys.
4. useEffect — Synchronizing with External Systems
useEffect is one of the most misunderstood React Hooks.
It allows a component to synchronize with systems outside React.
Examples include:
- Fetching data from an API.
- Subscribing to browser events.
- Connecting to a WebSocket.
- Starting and cleaning up timers.
- Synchronizing with third-party libraries.
- Updating certain external browser APIs.
Basic Syntax
useEffect(() => {
// Synchronization logic
return () => {
// Cleanup logic
};
}, [dependencies]);
The dependency array determines when React needs to re-synchronize the Effect.
Example 1: An Effect After Every Commit
import { useEffect, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Component committed.");
});
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
There is no dependency array.
The Effect runs after every commit of this component.
Example 2: An Effect with an Empty Dependency Array
useEffect(() => {
console.log("Effect setup");
}, []);
This Effect has no reactive dependencies.
It runs when the component mounts, and its cleanup runs when the component unmounts.
In development, React Strict Mode may perform an additional setup-and-cleanup cycle to help reveal bugs. Therefore, don't assume an Effect's setup always runs exactly once in every environment.
Example 3: An Effect with Dependencies
function UserProfile({ userId }) {
useEffect(() => {
console.log("User ID changed:", userId);
}, [userId]);
return <h2>User: {userId}</h2>;
}
The Effect runs after the initial commit and again when userId changes.
Understanding Dependency Arrays
| Dependency array | When the Effect runs |
|---|---|
| No array | After every commit |
[] |
After initial mount |
[value] |
After mount and when value changes |
React compares dependency values using Object.is.
Every reactive value used by an Effect should be included in its dependency array.
Don't remove dependencies just to force an Effect to run less frequently. Instead, restructure the code so that the Effect genuinely depends only on the values it needs.
5. useEffect Cleanup
Some Effects create resources that must be released.
For example:
- Event listeners.
- Timers.
- WebSocket connections.
- Subscriptions.
React lets you return a cleanup function from an Effect.
Example:
import { useEffect } from "react";
function WindowWidth() {
useEffect(() => {
function handleResize() {
console.log(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <p>Resize the browser window.</p>;
}
The cleanup function runs:
- Before the Effect runs again with changed dependencies.
- When the component unmounts.
This prevents old subscriptions and event listeners from remaining active.
Practical Example: A Timer
import { useEffect, useState } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(previous => previous + 1);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return <h2>Seconds: {seconds}</h2>;
}
The cleanup function stops the interval when the component is removed.
6. Fetching API Data with useEffect
A common use case for Effects is synchronizing a component with data from an API.
Let's build a simple example.
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchUsers() {
try {
setLoading(true);
setError(null);
const response = await fetch(
"https://jsonplaceholder.typicode.com/users",
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
setUsers(data);
} catch (error) {
if (error.name !== "AbortError") {
setError(error.message);
}
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
fetchUsers();
return () => {
controller.abort();
};
}, []);
if (loading) {
return <p>Loading users...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
);
}
This example demonstrates several important concepts:
- Loading state.
- Error handling.
- API requests.
- Effect cleanup.
- Request cancellation.
- Rendering dynamic data.
Important: useEffect is not automatically the best solution for every data-fetching requirement. Larger applications often benefit from framework data-loading features or dedicated libraries that handle caching, request deduplication, retries, and server rendering.
A Common useEffect Mistake
Avoid using Effects to calculate values that can be calculated directly during rendering.
Unnecessary:
function Product({ price, quantity }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(price * quantity);
}, [price, quantity]);
return <p>Total: {total}</p>;
}
Better:
function Product({ price, quantity }) {
const total = price * quantity;
return <p>Total: {total}</p>;
}
Why?
The total is derived from existing props. It doesn't need separate state or an Effect.
Use Effects for synchronization with external systems, not as a general mechanism for responding to every change in React data.
7. useRef — Referencing DOM Elements and Storing Mutable Values
useRef creates an object that persists between renders.
Its .current property can be changed without triggering a re-render.
Basic Syntax
const myRef = useRef(initialValue);
Example 1: Accessing a DOM Element
import { useRef } from "react";
function SearchBox() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>
Focus Input
</button>
</div>
);
}
Let's understand what happens:
-
useRef(null)creates a reference object. - The object is assigned to the input using the
refattribute. - After React attaches the DOM element,
inputRef.currentpoints to it. - Calling
.focus()focuses the input.
Example 2: Storing a Previous Value
import { useEffect, useRef } from "react";
function Counter({ count }) {
const previousCount = useRef(count);
useEffect(() => {
previousCount.current = count;
}, [count]);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {previousCount.current}</p>
</div>
);
}
The ref stores a value between renders without causing another render when it changes.
useState vs useRef
| Feature | useState | useRef |
|---|---|---|
| Stores values | Yes | Yes |
| Persists between renders | Yes | Yes |
| Updating triggers render | Yes | No |
| Common use | UI state | DOM references and mutable values |
Remember: If changing a value should update the UI, state is usually the right choice. If the value must persist but doesn't need to trigger rendering, a ref may be appropriate.
8. useContext — Sharing Data Across Components
Sometimes multiple components need access to the same information.
For example:
- Current user.
- Theme settings.
- Language preferences.
- Application configuration.
Passing props through many intermediate components can become inconvenient. This is often called prop drilling.
React Context provides a way to share values across a component tree.
Step 1: Create a Context
import { createContext } from "react";
export const ThemeContext = createContext("light");
Step 2: Provide the Context Value
import { ThemeContext } from "./ThemeContext";
function App() {
return (
<ThemeContext.Provider value="dark">
<Dashboard />
</ThemeContext.Provider>
);
}
Step 3: Consume the Context
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";
function Dashboard() {
const theme = useContext(ThemeContext);
return (
<div>
<h1>Current theme: {theme}</h1>
</div>
);
}
Now the Dashboard component can access the theme without receiving it through props.
Context with State
Context can also share state.
import {
createContext,
useContext,
useState
} from "react";
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
function login(username) {
setUser({ username });
}
function logout() {
setUser(null);
}
return (
<AuthContext.Provider
value={{ user, login, logout }}
>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const context = useContext(AuthContext);
if (context === null) {
throw new Error(
"useAuth must be used inside AuthProvider"
);
}
return context;
}
A component can now consume the shared authentication state:
function Profile() {
const { user, logout } = useAuth();
if (!user) {
return <p>Please log in.</p>;
}
return (
<div>
<h2>Welcome, {user.username}</h2>
<button onClick={logout}>Logout</button>
</div>
);
}
Wrap the application with the provider:
function App() {
return (
<AuthProvider>
<Profile />
</AuthProvider>
);
}
Context is useful for sharing values across a component tree, but it isn't automatically a replacement for every state-management solution.
For frequently changing, complex application state, consider whether a dedicated state-management approach is more appropriate.
9. useReducer — Managing Complex State Logic
useReducer is an alternative to useState.
It is particularly useful when:
- State has multiple related properties.
- Many actions can change the state.
- Update logic is becoming complicated.
- You want to centralize state transitions.
Basic Syntax
const [state, dispatch] = useReducer(reducer, initialState);
The reducer receives the current state and an action, then returns the next state.
Example: Counter with useReducer
import { useReducer } from "react";
const initialState = {
count: 0
};
function reducer(state, action) {
switch (action.type) {
case "increment":
return {
...state,
count: state.count + 1
};
case "decrement":
return {
...state,
count: state.count - 1
};
case "reset":
return initialState;
default:
throw new Error("Unknown action");
}
}
function Counter() {
const [state, dispatch] = useReducer(
reducer,
initialState
);
return (
<div>
<h2>{state.count}</h2>
<button
onClick={() => dispatch({ type: "increment" })}
>
Increase
</button>
<button
onClick={() => dispatch({ type: "decrement" })}
>
Decrease
</button>
<button
onClick={() => dispatch({ type: "reset" })}
>
Reset
</button>
</div>
);
}
How Does It Work?
When a user clicks the Increase button:
- The event handler calls
dispatch. - React sends the action to the reducer.
- The reducer calculates the next state.
- React renders the component with the updated state.
useState vs useReducer
Use useState when state updates are relatively simple.
Use useReducer when state transitions involve multiple actions or more centralized logic.
Neither is universally better. Choose based on the complexity of the state.
10. useMemo — Optimizing Expensive Calculations
useMemo allows React to cache the result of a calculation between renders.
It is useful when a calculation is expensive and its inputs don't change on every render.
Basic Syntax
const cachedValue = useMemo(() => {
return expensiveCalculation();
}, [dependencies]);
Example
import { useMemo, useState } from "react";
function ProductList({ products }) {
const [search, setSearch] = useState("");
const filteredProducts = useMemo(() => {
console.log("Filtering products...");
return products.filter(product =>
product.name
.toLowerCase()
.includes(search.toLowerCase())
);
}, [products, search]);
return (
<div>
<input
value={search}
onChange={event => setSearch(event.target.value)}
placeholder="Search products..."
/>
<ul>
{filteredProducts.map(product => (
<li key={product.id}>
{product.name}
</li>
))}
</ul>
</div>
);
}
The filtered result can be reused while both products and search remain unchanged.
Important Notes
useMemo is a performance optimization, not a requirement for correct code.
Don't wrap every calculation in useMemo.
For simple operations, normal JavaScript is usually clearer:
const fullName = firstName + " " + lastName;
There is no reason to memoize every small calculation.
Also, React may discard memoized values in certain situations. Your code should remain correct even if a calculation runs again.
11. useCallback — Caching Function References
useCallback lets React cache a function definition between renders.
It is useful when function identity matters, particularly when passing callbacks to memoized child components or using functions as dependencies.
Basic Syntax
const cachedFunction = useCallback(() => {
// Function logic
}, [dependencies]);
Example
import { useCallback, useState } from "react";
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []);
return (
<div>
<h2>{count}</h2>
<button onClick={() => setCount(count + 1)}>
Increase Count
</button>
<Child onClick={handleClick} />
</div>
);
}
function Child({ onClick }) {
console.log("Child rendered");
return (
<button onClick={onClick}>
Child Button
</button>
);
}
In this example, handleClick keeps the same function identity between renders.
However, this alone doesn't prevent Child from rendering.
To skip some child renders when its props remain unchanged, you can use memo:
import { memo } from "react";
const Child = memo(function Child({ onClick }) {
console.log("Child rendered");
return (
<button onClick={onClick}>
Child Button
</button>
);
});
Now React can skip rendering the child when its props are unchanged.
useMemo vs useCallback
| Hook | What it caches |
|---|---|
| useMemo | A calculated value |
| useCallback | A function definition |
A useful mental model:
useMemo(() => calculateValue(), [dependency]);
useCallback(() => handleAction(), [dependency]);
Both are optimization tools. Use them when there is a measurable or meaningful reason, not automatically.
In projects using React Compiler, some manual memoization may be handled automatically, depending on the project's configuration and supported features.
12. useId — Generating Unique IDs for Accessibility
useId generates unique IDs that are useful for connecting related accessibility elements.
For example, an input and its label.
import { useId } from "react";
function EmailInput() {
const id = useId();
return (
<div>
<label htmlFor={id}>
Email Address
</label>
<input
id={id}
type="email"
/>
</div>
);
}
React generates an ID that can connect the label to the input.
This is particularly useful when a reusable component appears multiple times.
Important: Don't use useId to generate list keys.
List keys should come from stable data identifiers.
13. useTransition — Keeping the UI Responsive
Sometimes an update triggers expensive rendering work.
You may want an interaction, such as typing into an input, to remain responsive while another update happens in the background.
useTransition allows you to mark certain state updates as non-urgent.
Example
import {
useState,
useTransition
} from "react";
function SearchPage({ items }) {
const [query, setQuery] = useState("");
const [filteredQuery, setFilteredQuery] = useState("");
const [isPending, startTransition] = useTransition();
function handleChange(event) {
const value = event.target.value;
setQuery(value);
startTransition(() => {
setFilteredQuery(value);
});
}
const filteredItems = items.filter(item =>
item.toLowerCase().includes(filteredQuery.toLowerCase())
);
return (
<div>
<input
value={query}
onChange={handleChange}
placeholder="Search..."
/>
{isPending && <p>Updating results...</p>}
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
The input update is urgent, while the filtering-related state update is marked as a transition.
Important Concepts
useTransition returns:
-
isPending: indicates whether a transition is pending. -
startTransition: marks state updates as transitions.
A transition is not a replacement for debouncing, and it doesn't automatically make expensive JavaScript calculations faster.
It helps React prioritize rendering work.
14. useDeferredValue — Deferring Updates
useDeferredValue lets a component defer updating part of its UI.
It is useful when a value changes frequently and rendering everything based on that value is expensive.
Example
import {
useDeferredValue,
useState
} from "react";
function Search({ items }) {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const filteredItems = items.filter(item =>
item.toLowerCase().includes(
deferredQuery.toLowerCase()
)
);
return (
<div>
<input
value={query}
onChange={event => setQuery(event.target.value)}
placeholder="Search..."
/>
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
The input can update immediately while the result-rendering portion can temporarily use an older value.
This can improve responsiveness when the deferred part is expensive to render.
useTransition vs useDeferredValue
| Hook | Main purpose |
|---|---|
| useTransition | Mark state updates as non-urgent |
| useDeferredValue | Defer updating a value used by rendering |
Use useTransition when you control the state update.
Use useDeferredValue when you receive a value and want to defer how your component uses it.
15. Custom Hooks — Reusing Stateful Logic
One of the most powerful features of Hooks is the ability to create your own Hooks.
A Custom Hook is a JavaScript function whose name starts with use and that can call other Hooks.
Custom Hooks help you extract reusable behavior from components.
Example: useWindowWidth
Imagine several components need to know the browser window width.
Instead of repeating the same Effect everywhere, create a Custom Hook.
import {
useEffect,
useState
} from "react";
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return width;
}
Now any component can use it:
function ResponsiveLayout() {
const width = useWindowWidth();
return (
<div>
<h2>Window Width: {width}px</h2>
{width < 768 ? (
<p>Mobile Layout</p>
) : (
<p>Desktop Layout</p>
)}
</div>
);
}
Example: useLocalStorage
A reusable Hook can synchronize a state value with browser storage.
import {
useEffect,
useState
} from "react";
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const storedValue = localStorage.getItem(key);
return storedValue !== null
? JSON.parse(storedValue)
: initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
Usage:
function Settings() {
const [theme, setTheme] = useLocalStorage(
"theme",
"light"
);
return (
<div>
<h2>Current Theme: {theme}</h2>
<button
onClick={() =>
setTheme(theme === "light" ? "dark" : "light")
}
>
Toggle Theme
</button>
</div>
);
}
This is a simplified browser-only implementation. In server-rendered applications, browser APIs such as localStorage must be accessed only in an appropriate client-side environment. Production implementations should also consider invalid stored JSON and storage errors.
Important: Custom Hooks Share Logic, Not State
Consider two components using the same Hook:
function ComponentA() {
const [count, setCount] = useCounter();
}
function ComponentB() {
const [count, setCount] = useCounter();
}
Each component gets its own independent state.
Custom Hooks share the logic for managing state, not the actual state instance.
If components need to share the same state, use a shared parent, Context, or an appropriate state-management solution.
16. Other Useful React Hooks
The Hooks covered so far are enough for many everyday applications, but React provides additional Hooks for more specialized situations.
useLayoutEffect
useLayoutEffect is similar to useEffect, but it runs after DOM mutations and before the browser paints.
It is useful when you need to measure or synchronously adjust layout before the user sees the result.
import {
useLayoutEffect,
useRef,
useState
} from "react";
function Tooltip() {
const tooltipRef = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
if (tooltipRef.current) {
setHeight(
tooltipRef.current.getBoundingClientRect().height
);
}
}, []);
return (
<div>
<div ref={tooltipRef}>
Tooltip content
</div>
<p>Tooltip height: {height}px</p>
</div>
);
}
Use useLayoutEffect only when necessary. It can block browser painting, so useEffect is generally preferable for ordinary synchronization.
useImperativeHandle
useImperativeHandle allows a component to customize the value exposed through a ref.
It is useful when a parent needs access to a limited imperative API rather than the entire child implementation.
import {
useImperativeHandle,
useRef
} from "react";
function CustomInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = "";
}
}), []);
return <input ref={inputRef} />;
}
This example uses the React 19 ref-as-prop API. In React 18 and earlier, forwardRef is generally used to receive a ref in a function component.
useSyncExternalStore
This Hook is designed for subscribing to external stores in a way that works correctly with React's rendering model.
It is especially useful for library authors and integrations with external state systems.
import { useSyncExternalStore } from "react";
function subscribe(callback) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
function NetworkStatus() {
const isOnline = useSyncExternalStore(
subscribe,
getSnapshot
);
return (
<p>
{isOnline ? "Online" : "Offline"}
</p>
);
}
For server-rendered applications, you may also need to provide a server snapshot function.
17. React 19: The use API Is Different from Traditional Hooks
React 19 introduced the use API, which can read resources such as Promises and Context.
Despite its name, use is not an ordinary Hook.
For example, unlike traditional Hooks, use can be called inside conditions and loops in supported situations.
import { use } from "react";
function Message({ messagePromise }) {
const message = use(messagePromise);
return <p>{message}</p>;
}
When the Promise is pending, React can suspend rendering. The component should be placed within an appropriate Suspense boundary.
import { Suspense } from "react";
function App({ messagePromise }) {
return (
<Suspense fallback={<p>Loading...</p>}>
<Message messagePromise={messagePromise} />
</Suspense>
);
}
This API has specific rules and should not be confused with traditional Hooks such as useState and useEffect.
18. Practical Project: Building a Custom Counter Dashboard
Let's combine several Hooks into a small application.
Our dashboard will include:
- Counter state with
useReducer. - Theme management with
useState. - A document title synchronization Effect.
- A ref for focusing an input.
- A memoized calculation.
- A Custom Hook for reusable counter logic.
Step 1: Create the Custom Hook
import { useReducer } from "react";
const initialState = {
count: 0
};
function counterReducer(state, action) {
switch (action.type) {
case "increment":
return {
count: state.count + 1
};
case "decrement":
return {
count: state.count - 1
};
case "reset":
return initialState;
default:
return state;
}
}
export function useCounter() {
const [state, dispatch] = useReducer(
counterReducer,
initialState
);
return {
count: state.count,
increment: () => dispatch({ type: "increment" }),
decrement: () => dispatch({ type: "decrement" }),
reset: () => dispatch({ type: "reset" })
};
}
Step 2: Build the Dashboard
import {
useEffect,
useMemo,
useRef,
useState
} from "react";
import { useCounter } from "./useCounter";
function Dashboard() {
const {
count,
increment,
decrement,
reset
} = useCounter();
const [theme, setTheme] = useState("light");
const [name, setName] = useState("");
const inputRef = useRef(null);
const squaredCount = useMemo(() => {
return count * count;
}, [count]);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
function focusInput() {
inputRef.current.focus();
}
return (
<div
style={{
padding: "30px",
background: theme === "dark" ? "#222" : "#fff",
color: theme === "dark" ? "#fff" : "#222"
}}
>
<h1>React Hooks Dashboard</h1>
<h2>Counter: {count}</h2>
<p>Squared Value: {squaredCount}</p>
<button onClick={increment}>
Increase
</button>
<button onClick={decrement}>
Decrease
</button>
<button onClick={reset}>
Reset
</button>
<hr />
<h2>Theme Settings</h2>
<button
onClick={() =>
setTheme(theme === "light" ? "dark" : "light")
}
>
Toggle Theme
</button>
<hr />
<h2>User Settings</h2>
<input
ref={inputRef}
value={name}
onChange={event => setName(event.target.value)}
placeholder="Enter your name"
/>
<button onClick={focusInput}>
Focus Input
</button>
<p>
Hello, {name || "Guest"}!
</p>
</div>
);
}
export default Dashboard;
This small project demonstrates how different Hooks can work together in a single component.
Each Hook has a specific responsibility:
| Hook | Responsibility |
|---|---|
| useReducer | Counter state management |
| useState | Theme and name state |
| useEffect | Synchronizing the document title |
| useRef | Accessing the input element |
| useMemo | Caching a calculation |
| useCounter | Reusing counter logic |
Notice that useMemo isn't essential for this tiny calculation. It's included to demonstrate the API, not because squaring a number requires optimization.
19. Common React Hooks Mistakes
Let's review some mistakes developers frequently make.
Mistake 1: Calling Hooks Conditionally
Incorrect:
if (isLoggedIn) {
const [user, setUser] = useState(null);
}
Correct:
const [user, setUser] = useState(null);
if (!isLoggedIn) {
return <p>Please log in.</p>;
}
Mistake 2: Forgetting Effect Dependencies
Incorrect:
useEffect(() => {
console.log(userId);
}, []);
If userId changes, this Effect won't re-synchronize.
Correct:
useEffect(() => {
console.log(userId);
}, [userId]);
Mistake 3: Mutating State Directly
Incorrect:
user.name = "John";
setUser(user);
Correct:
setUser(previousUser => ({
...previousUser,
name: "John"
}));
Mistake 4: Using useEffect for Derived State
Unnecessary:
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(price * quantity);
}, [price, quantity]);
Better:
const total = price * quantity;
Mistake 5: Using useRef Instead of State for UI
If a value must trigger a UI update, use state.
Changing ref.current doesn't cause React to render again.
Mistake 6: Overusing useMemo and useCallback
Not every value or function needs to be memoized.
Start with simple code. Optimize when you have a reason.
Mistake 7: Forgetting Effect Cleanup
If an Effect creates a timer or subscription, make sure it releases that resource when appropriate.
Mistake 8: Treating Custom Hooks as Shared State
Two components calling the same Custom Hook normally receive separate state instances.
Use Context or another shared state solution when the actual state must be shared.
20. Which Hook Should You Use?
Here's a practical reference table.
| Situation | Hook |
|---|---|
| Store component state | useState |
| Manage complex state transitions | useReducer |
| Synchronize with external systems | useEffect |
| Access a DOM element | useRef |
| Store mutable values without rendering | useRef |
| Share values through a component tree | useContext |
| Cache expensive calculations | useMemo |
| Cache function references | useCallback |
| Generate accessibility IDs | useId |
| Mark updates as non-urgent | useTransition |
| Defer a changing value | useDeferredValue |
| Reuse stateful logic | Custom Hooks |
| Measure layout before paint | useLayoutEffect |
| Expose a custom imperative ref API | useImperativeHandle |
| Subscribe to external stores | useSyncExternalStore |
21. Final Thoughts
React Hooks transformed how developers build React applications.
They made functional components powerful enough to manage state, synchronize with external systems, share logic, and handle complex application behavior.
The most important Hooks to master first are:
useStateuseEffectuseRefuseContextuseReducer- Custom Hooks
Once you're comfortable with these, learning performance-related and specialized Hooks becomes much easier.
Don't try to use every Hook in every component.
Good React development is about choosing the right tool for the problem, keeping components understandable, and writing code that behaves correctly.
The best way to master Hooks is to build real applications and experiment with them.
What's Next?
In Part 4 of the React Mastery Series, we'll explore:
React Forms & API Integration — Building Real-World Interactive Applications
We'll cover:
- Controlled and uncontrolled components.
- Handling form submissions.
- Form validation.
- Managing complex forms.
- Fetching data from APIs.
- Loading and error states.
- Async JavaScript in React.
- Building a practical application that communicates with an API.
If you're following this series, keep practicing the examples and try building your own small project using multiple Hooks.
Happy coding! 🚀
Top comments (0)