Build reusable custom hooks to extract component logic and share stateful behavior.
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}
function useDebouncedValue(value, delayMs = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(timer); // cancel if value changes again
}, [value, delayMs]);
return debounced;
}
function SearchBox() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebouncedValue(query, 400);
useEffect(() => {
if (debouncedQuery) searchApi(debouncedQuery);
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
// Good: clearly indicates it's a hook
function useToggle() { }
function useFetch(url) { }
function useLocalStorage(key) { }
// Bad: doesn't follow naming convention
function toggle() { }
function fetch() { }function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue(!value);
return [value, toggle];
}
// Usage
function Modal() {
const [isOpen, toggleOpen] = useToggle(false);
return (
<>
<button onClick={toggleOpen}>Open</button>
{isOpen && <div>Modal content</div>}
</>
);
}function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}function useOnlineStatus() {
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;
}
function useSyncedField(key, initialValue) {
const [value, setValue] = useLocalStorage(key, initialValue); // reuse an existing hook
const isOnline = useOnlineStatus(); // compose a second hook
return { value, setValue, isOnline };
}
function useToggle<T = boolean>(initial: T): [T, () => void] {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v as T), []);
return [value, toggle];
}
const [isOpen, toggleOpen] = useToggle(false); // isOpen inferred as boolean
import { renderHook, act } from '@testing-library/react';
test('useToggle flips its value', () => {
const { result } = renderHook(() => useToggle(false));
act(() => result.current[1]()); // call the toggle function
expect(result.current[0]).toBe(true);
});
function useCounter() {
const [count, setCount] = useState(0);
return [count, () => setCount(count + 1)];
}
function Component1() {
const [count, increment] = useCounter();
return <button onClick={increment}>{count}</button>;
}
function Component2() {
const [count, increment] = useCounter();
return <button onClick={increment}>{count}</button>;
}
// Each component has separate count statefunction useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth needs provider");
return context;
}Extract into a custom hook when the same stateful logic (fetching, form handling, subscriptions) appears in multiple components, or when a single component's logic grows complex enough to benefit from separation. Custom hooks let you test and reuse that logic independently of any UI.
No — hooks must be called at the top level of a function, never inside conditionals, loops, or nested functions. This rule applies inside custom hooks too, not just components.
You can't share state via a hook alone — each component calling the hook gets its own independent state instance. To share state, lift it to a common parent and pass it down, or use a context provider that the hook reads from internally.
Return an array (like useState) when consumers will typically rename the values and order is obvious; return an object (like useReducer's dispatch pattern or React Query) when there are many return values or names carry meaning. Mixing both in a single hook adds confusion.
No — a custom hook runs in the context of the component that calls it, so re-render behavior is identical to inlining the logic. The hook abstraction has no runtime overhead.