Skip to main content

How do you create a custom hook?

Steps to create a custom hook

  1. Give it a name starting with use E.g.: useLocalStorage, useDebouncedValue, useEventListener.
  2. Write it as an ordinary function Inside it you can call other hooks (useState, useEffect, ...).
  3. Follow the rules of hooks Top level only (not in if/for), only inside React functions.
  4. Define a clear API Return what the consumer needs: a value, setters, statuses (loading, error), methods.
  5. Specify dependencies correctly In useEffect/useMemo/useCallback do not forget the full list of dependencies.

Template 1: a basic state hook with a side effect ("Hello, custom hook!")

useCounter.ts

javascript
import { useCallback, useState } from "react"; export function useCounter(initial = 0, step = 1) { const [count, setCount] = useState(initial); const inc = useCallback(() => setCount(c => c + step), [step]); const dec = useCallback(() => setCount(c => c - step), [step]); const reset = useCallback(() => setCount(initial), [initial]); return { count, inc, dec, reset }; }

Usage

javascript
function Counter() { const { count, inc, dec, reset } = useCounter(10, 2); return ( <> <p>{count}</p> <button onClick={dec}>-</button> <button onClick={inc}>+</button> <button onClick={reset}>Reset</button> </> ); }

Template 2: a "practical" one - local storage (persist)

useLocalStorage.ts

javascript
import { useEffect, useState } from "react"; export function useLocalStorage<T>(key: string, initial: T) { const [value, setValue] = useState<T>(() => { try { const raw = localStorage.getItem(key); return raw != null ? JSON.parse(raw) as T : initial; } catch { return initial; } }); useEffect(() => { try { localStorage.setItem(key, JSON.stringify(value)); } catch { // ignore quota/private mode } }, [key, value]); return [value, setValue] as const; }

Usage

javascript
const [theme, setTheme] = useLocalStorage<'light'|'dark'>('theme', 'light');

Template 3: input optimization - debouncing a value

useDebouncedValue.ts

javascript
import { useEffect, useState } from "react"; export function useDebouncedValue<T>(value: T, delay = 300) { const [debounced, setDebounced] = useState(value); useEffect(() => { const id = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(id); }, [value, delay]); return debounced; }

Usage (lag-free search)

javascript
function Search({ items }: { items: string[] }) { const [q, setQ] = useState(""); const dq = useDebouncedValue(q, 350); const filtered = useMemo(() => items.filter(i => i.includes(dq)), [items, dq]); return ( <> <input value={q} onChange={e => setQ(e.target.value)} /> <ul>{filtered.map((x, i) => <li key={i}>{x}</li>)}</ul> </> ); }

Patterns and tips

  • Return a tuple or an object. A tuple ([value, setValue]) - if the semantics match useState. An object - if you return many fields/methods.
  • Stabilize functions (useCallback) and computations (useMemo) if the hook exposes them and that affects the consumer's re-renders.
  • Do not make network requests in useLayoutEffect - a regular useEffect is fine; caching/cancellation - through AbortController.
  • Cover the "corners". Handle errors, SSR (check for window), and unsubscribe in cleanup.
  • Linting: add eslint-plugin-react-hooks and do not ignore exhaustive-deps.
  • Testing: for unit tests use @testing-library/react + renderHook from @testing-library/react-hooks/@testing-library/react.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.