Custom hooks
A custom hook is an ordinary function whose name starts with use and which calls other hooks inside. It holds logic repeated across several components: loading data, subscribing to an event, working with localStorage. The component gets shorter, and the logic can be reused.
A data loading hook
In the blog, the post list, the post page and the comments all load data the same way: a request, loading, an error. Let us put that into a hook. Create the folder and the file src/hooks/useFetch.js:
// src/hooks/useFetch.js
import { useEffect, useState } from 'react';
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
setLoading(true);
setError(null);
fetch(url)
.then((response) => {
if (!response.ok) throw new Error(`Error ${response.status}`);
return response.json();
})
.then((json) => {
if (!ignore) setData(json);
})
.catch((err) => {
if (!ignore) setError(err.message);
})
.finally(() => {
if (!ignore) setLoading(false);
});
return () => {
ignore = true;
};
}, [url]);
return { data, loading, error };
}The hook returns an object with three values. When url changes, the request is repeated.
Using it
The PostList component from the data loading lesson shrinks to a few lines:
// src/components/PostList.jsx
import { useFetch } from '../hooks/useFetch.js';
export default function PostList() {
const { data: posts, loading, error } = useFetch(
'https://jsonplaceholder.typicode.com/posts',
);
if (loading) return <p>Loading…</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}The page behaves as before: "Loading…", then the post list. The same hook works for a single post: useFetch('https://jsonplaceholder.typicode.com/posts/1').
Each call has its own state
A hook reuses logic, not data. If two components call useFetch, each gets its own data, loading and error and its own request.
A simple hook without an effect
A hook does not have to be big. A toggle for a menu or a spoiler — src/hooks/useToggle.js:
// src/hooks/useToggle.js
import { useState } from 'react';
export function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue((current) => !current);
return [value, toggle];
}In a component: const [isOpen, toggle] = useToggle(); and a button with onClick={toggle}.
Rules of hooks
| Rule | Why |
|---|---|
| Call hooks only at the top level of a component or another hook | React tells hooks apart by call order |
| Do not call hooks in conditions, loops or handlers | The order would differ between renders |
A hook name starts with use | The linter finds hooks by it and checks the rules |
Common mistake
A hook inside a condition breaks the call order: on one render it is there, on another it is not:
// Wrong:
function PostPage({ id }) {
if (id) {
const { data } = useFetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
}
}Always call the hook, and move the condition into the hook or the markup. The linter in a Vite project reports this with the react-hooks/rules-of-hooks rule.