1. Introduction 👋
Custom Hooks are one of React's most powerful patterns for code reuse. They let you extract component logic into reusable, testable functions without changing the underlying behavior. This tutorial covers everything from writing your first custom Hook to advanced patterns like combining them with Context, reducers, and TS.
Information
2. What are Custom Hooks? 🤔
A custom Hook is simply a JavaScript function whose name starts with use, and which calls other Hooks internally to encapsulate reusable, stateful logic.
Code Snippet
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue((v) => !v);
return [value, toggle];
}
// Usage
function Modal() {
const [isOpen, toggleOpen] = useToggle(false);
return <button onClick={toggleOpen}>{isOpen ? "Close" : "Open"}</button>;
}3. Why Create Custom Hooks? 💡
- Code Reuse: Extract logic used across multiple components into one place.
- Readability: Hide complex implementation details behind a simple, descriptive function name.
- Testability: Logic can be tested in isolation, separate from rendering concerns.
- Separation of Concerns: Keep components focused on rendering, while Hooks manage behavior.
4. Rules for Custom Hooks ⚖️
Custom Hooks must follow the same Rules of Hooks as built-in Hooks, since they're built entirely out of them.
- Only call Hooks at the top level — never inside loops, conditions, or nested functions.
- Only call Hooks from React function components or other custom Hooks.
- The function name must start with use so React and linting tools recognize it as a Hook.
Danger
5. Naming Custom Hooks 🏷️
- Always prefix with use: useAuth, useFetch, useLocalStorage.
- Choose names that describe what the Hook provides, not how it's implemented.
- Use camelCase consistently, matching the convention of built-in Hooks.
Tip
6. Creating Your First Custom Hook 🎬
useCounter.js
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount((c) => c + 1);
const decrement = () => setCount((c) => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// Usage
function Counter() {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}7. Reusing State Logic 🔄
Custom Hooks excel at extracting stateful logic that would otherwise be duplicated across several components.
Code Snippet
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Reused across many components
function ThemeSwitcher() {
const [theme, setTheme] = useLocalStorage("theme", "light");
}
function LanguageSelector() {
const [language, setLanguage] = useLocalStorage("language", "en");
}8. Reusing Side Effects ⚡
Code Snippet
function useDocumentTitle(title) {
useEffect(() => {
const previousTitle = document.title;
document.title = title;
return () => {
document.title = previousTitle;
};
}, [title]);
}
// Usage
function ProductPage({ product }) {
useDocumentTitle(`${product.name} | My Store`);
}Tip
9. Combining Multiple Hooks 🧩
Code Snippet
function useAuthenticatedUser() {
const { token } = useAuth();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!token) return;
fetchUser(token).then((data) => {
setUser(data);
setIsLoading(false);
});
}, [token]);
return { user, isLoading };
}Note
10. Sharing Business Logic 📊
Beyond simple UI state, custom Hooks are excellent for encapsulating domain-specific logic — validation rules, calculations, or workflows unique to your application.
Code Snippet
function useShoppingCart() {
const [items, setItems] = useState([]);
const addItem = (item) => setItems((prev) => [...prev, item]);
const removeItem = (id) => setItems((prev) => prev.filter((i) => i.id !== id));
const total = items.reduce((sum, item) => sum + item.price, 0);
return { items, addItem, removeItem, total };
}11. Returning Values 📤
A custom Hook can return a single value, when only one piece of information is needed by the consumer.
Code Snippet
function useIsOnline() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const goOnline = () => setIsOnline(true);
const goOffline = () => setIsOnline(false);
window.addEventListener("online", goOnline);
window.addEventListener("offline", goOffline);
return () => {
window.removeEventListener("online", goOnline);
window.removeEventListener("offline", goOffline);
};
}, []);
return isOnline;
}12. Returning Functions 🔧
Code Snippet
function useClipboard() {
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
}
return copyToClipboard;
}
// Usage
function ShareButton({ url }) {
const copy = useClipboard();
return <button onClick={() => copy(url)}>Copy Link</button>;
}13. Returning Objects 📦
When a Hook returns several related values, an object with named properties is often clearer than a positional array — especially as the number of returned values grows.
Code Snippet
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
function handleChange(e) {
setValues({ ...values, [e.target.name]: e.target.value });
}
return { values, errors, handleChange, setErrors };
}
// Usage — order doesn't matter, names are self-documenting
const { values, handleChange } = useForm({ email: "" });14. Returning Arrays 📚
For Hooks with two closely related return values (mirroring useState's pattern), an array lets consumers freely rename the destructured variables.
Code Snippet
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue((v) => !v);
return [value, toggle];
}
// Consumers can rename freely, just like useState
const [isOpen, toggleOpen] = useToggle();
const [isVisible, toggleVisible] = useToggle(true);Tip
15. Passing Arguments 📥
Code Snippet
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
fetch(url, options)
.then((res) => res.json())
.then(setData)
.catch(setError)
.finally(() => setIsLoading(false));
}, [url]);
return { data, error, isLoading };
}
// Usage
const { data, isLoading } = useFetch("/api/products");16. Using Multiple Custom Hooks 🔗
Code Snippet
function ProfilePage({ userId }) {
const { user, isLoading } = useFetch(`/api/users/${userId}`);
const [theme] = useLocalStorage("theme", "light");
const isOnline = useIsOnline();
if (isLoading) return <Spinner />;
return (
<div className={theme}>
<h1>{user.name}</h1>
<p>{isOnline ? "🟢 Online" : "🔴 Offline"}</p>
</div>
);
}17. Composing Custom Hooks 🧬
Custom Hooks can call other custom Hooks, allowing complex behavior to be built from smaller, well-tested pieces — the same principle as composing components.
Code Snippet
function useDebouncedSearch(query, delay = 300) {
const debouncedQuery = useDebounce(query, delay);
const { data, isLoading } = useFetch(`/api/search?q=${debouncedQuery}`);
return { results: data, isLoading };
}Best Practice
18. Error Handling in Custom Hooks ⚠️
Code Snippet
function useFetch(url) {
const [state, setState] = useState({ data: null, error: null, isLoading: true });
useEffect(() => {
let ignore = false;
setState((s) => ({ ...s, isLoading: true, error: null }));
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
return res.json();
})
.then((data) => {
if (!ignore) setState({ data, error: null, isLoading: false });
})
.catch((error) => {
if (!ignore) setState({ data: null, error, isLoading: false });
});
return () => { ignore = true; };
}, [url]);
return state;
}Important
19. Async Custom Hooks ⏳
Custom Hooks can't be async functions themselves (since Hooks must run synchronously during render), but they commonly wrap internal async logic inside useEffect or event handlers.
Code Snippet
function useAsyncAction() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
async function run(action) {
setIsPending(true);
setError(null);
try {
await action();
} catch (err) {
setError(err);
} finally {
setIsPending(false);
}
}
return { run, isPending, error };
}20. Custom Hooks with Context 🌐
Wrapping a useContext call inside a custom Hook creates a clean, safer API — including a helpful error if used outside its Provider.
Code Snippet
const AuthContext = createContext(null);
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
// Usage — clean and safe
function Profile() {
const { user, logout } = useAuth();
}21. Custom Hooks with Reducers 🏗️
Code Snippet
function todosReducer(state, action) {
switch (action.type) {
case "add": return [...state, action.payload];
case "remove": return state.filter((t) => t.id !== action.payload);
default: return state;
}
}
function useTodos() {
const [todos, dispatch] = useReducer(todosReducer, []);
const addTodo = (text) => dispatch({ type: "add", payload: { id: crypto.randomUUID(), text } });
const removeTodo = (id) => dispatch({ type: "remove", payload: id });
return { todos, addTodo, removeTodo };
}Tip
22. Custom Hooks with TypeScript 🔷
Code Snippet
interface UseFetchResult<T> {
data: T | null;
error: Error | null;
isLoading: boolean;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json: T) => setData(json))
.catch(setError)
.finally(() => setIsLoading(false));
}, [url]);
return { data, error, isLoading };
}
// Usage with explicit type
const { data } = useFetch<Product[]>("/api/products");Tip
23. Testing Custom Hooks 🧪
Custom Hooks can be tested in isolation using utilities like @testing-library/react's renderHook, without needing to render a full component.
Code Snippet
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
test('increments the counter', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Best Practice
24. Performance Considerations ⚡
- Each component calling a custom Hook gets its own independent state — no state is shared automatically.
- Memoize returned functions with useCallback if consumers may pass them to memo-wrapped components.
- Avoid creating new objects/arrays on every call if the Hook is used inside performance-sensitive components.
Note
25. Organizing Custom Hooks 🗂️
Tip
26. Common Custom Hook Patterns 🎨
Code Snippet
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debounced;
}Code Snippet
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}Code Snippet
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}27. Best Practices 🌟
- Always prefix custom Hook names with use.
- Keep each Hook focused on one clear responsibility.
- Return objects for many values, arrays for two tightly-coupled values.
- Handle loading and error states explicitly in async Hooks.
- Compose smaller Hooks together rather than writing one large, monolithic Hook.
- Test Hooks independently using tools like renderHook.
28. Common Mistakes 🚫
- Forgetting the use prefix, breaking linting and the Rules of Hooks enforcement.
- Assuming custom Hooks share state between components — each call is fully independent.
- Building overly generic, do-everything Hooks instead of composing smaller, focused ones.
- Not handling error states in data-fetching Hooks.
- Calling Hooks conditionally inside a custom Hook, violating the Rules of Hooks just like in components.
Danger
29. Frequently Asked Questions ❓
Question
Answer
Question
Answer
Question
Answer
30. Summary 📝
Custom Hooks unlock React's full potential for logic reuse, letting you package stateful behavior — from simple toggles to complex data-fetching and authentication flows — into clean, testable, composable functions. Mastering this pattern is often the difference between a codebase full of duplicated logic and one that stays clean as it scales.