How do you create a custom hook?
Steps to create a custom hook
- Give it a name starting with
useE.g.:useLocalStorage,useDebouncedValue,useEventListener. - Write it as an ordinary function
Inside it you can call other hooks (
useState,useEffect, ...). - Follow the rules of hooks
Top level only (not in
if/for), only inside React functions. - Define a clear API
Return what the consumer needs: a value, setters, statuses (
loading,error), methods. - Specify dependencies correctly
In
useEffect/useMemo/useCallbackdo 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 matchuseState. 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 regularuseEffectis fine; caching/cancellation - throughAbortController. - Cover the "corners".
Handle errors, SSR (check for
window), and unsubscribe in cleanup. - Linting: add
eslint-plugin-react-hooksand do not ignoreexhaustive-deps. - Testing:
for unit tests use
@testing-library/react+renderHookfrom@testing-library/react-hooks/@testing-library/react.
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.