Custom Hooks in React
Custom Hooks are a powerful React feature that allows logic to be extracted from components and reused across multiple parts of an application. A Custom Hook is simply a JavaScript function whose name starts with use and that calls one or more built-in React Hooks inside it.
They do not add any new capability to React — they are purely a convention for organizing and sharing stateful logic cleanly.
Why Create Custom Hooks?
Imagine the same logic being written in multiple components — for example, every component that needs to know the current window width needs a useEffect to listen for resize events, a useState to store the width, and cleanup logic. Writing this in every component is repetitive and hard to maintain.
A Custom Hook extracts this logic into one place. Now every component just calls useWindowWidth() and gets the value it needs — no duplication, no clutter.
Rules for Custom Hooks
- The function name must start with
use(e.g.,useWindowWidth,useFetch,useLocalStorage). This is required — React uses this naming convention to apply its Hook rules. - Custom Hooks can call other Hooks (built-in or other Custom Hooks).
- They must follow the standard Rules of Hooks: only call Hooks at the top level, not inside loops, conditions, or nested functions.
Example 1 — useWindowWidth
A Custom Hook that tracks the browser window's width:
import { useState, useEffect } 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;
}
export default useWindowWidth;
Now this Hook can be used in any component without rewriting the resize logic:
import useWindowWidth from './useWindowWidth';
function ResponsiveBanner() {
const width = useWindowWidth();
return (
<p>
{width < 768 ? "You are on a mobile screen." : "You are on a desktop screen."}
</p>
);
}
Example 2 — useFetch
A very common Custom Hook is one that handles data fetching — including loading and error states:
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error("Network response was not ok");
return res.json();
})
.then((json) => {
setData(json);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
export default useFetch;
Using this Hook in a component is clean and simple:
import useFetch from './useFetch';
function PostList() {
const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/posts");
if (loading) return <p>Loading posts...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data.slice(0, 5).map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
This component does not need to know anything about how the data fetching works — it simply calls the Hook and gets the data, loading, and error values back.
Example 3 — useToggle
A simple but very reusable Hook for toggling a boolean value:
import { useState } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
function toggle() {
setValue((prev) => !prev);
}
return [value, toggle];
}
export default useToggle;
Used in a component:
import useToggle from './useToggle';
function Accordion() {
const [isOpen, toggleOpen] = useToggle(false);
return (
<div>
<button onClick={toggleOpen}>{isOpen ? "Collapse" : "Expand"}</button>
{isOpen && <p>This content is shown when expanded.</p>}
</div>
);
}
Example 4 — useLocalStorage
A Hook that syncs state with the browser's localStorage, so values persist even after a page refresh:
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
function setValue(value) {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error("Error saving to localStorage:", error);
}
}
return [storedValue, setValue];
}
export default useLocalStorage;
Used in a component:
import useLocalStorage from './useLocalStorage';
function ThemePreference() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return (
<div>
<p>Current Theme: {theme}</p>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle Theme
</button>
</div>
);
}
The theme preference is now saved to localStorage and will persist even if the user refreshes the page.
Key Points
- Custom Hooks are JavaScript functions whose names start with
useand that call React Hooks internally. - They allow stateful logic to be extracted from components and reused across the application.
- Custom Hooks do not share state — each call to a Custom Hook creates its own independent state.
- They follow the same Rules of Hooks as built-in Hooks.
- Common examples include:
useFetch,useToggle,useWindowWidth, anduseLocalStorage.
The next topic covers React Router — the standard library for adding page navigation to React applications.
