Suggest an editImprove this articleRefine the answer for “How useCallback works and why is it needed”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**useCallback** memoizes a function, returning the same reference across re-renders unless dependencies change. ```jsx const handleClick = useCallback(() => { doSomething(id); }, [id]); // recreated only when id changes ``` **When to use:** passing callbacks to `React.memo` children, or when a function is a `useEffect` dependency.Shown above the full answer for quick recall.Answer (EN)Image**useCallback** is a React hook that memoizes a function, returning the same reference across re-renders unless its dependencies change. ## Theory ### TL;DR - Each render creates new function instances; two identical arrow functions are not `===` equal - `useCallback` caches the function and returns the cached version if deps match - Main use case: callbacks passed to `React.memo` children to skip unnecessary re-renders - Without a memoized child receiving it, `useCallback` adds overhead with no render savings - Rule: profile with React DevTools Profiler first, then add `useCallback` only where it actually helps ### Quick example ```jsx import { useState, useCallback } from 'react'; function Parent() { const [count, setCount] = useState(0); const [other, setOther] = useState(0); // Without useCallback: new function every render -> Button always re-renders // const handleClick = () => setCount(c => c + 1); // With useCallback: same ref unless deps change -> Button skips re-render const handleClick = useCallback(() => { setCount(c => c + 1); }, []); // empty deps = never recreated return ( <> <Button onClick={handleClick} /> <button onClick={() => setOther(o => o + 1)}>Change other</button> </> ); } const Button = React.memo(({ onClick }) => { console.log('Button render'); // skips when handleClick ref is stable return <button onClick={onClick}>Click</button>; }); ``` When `other` changes, `Parent` re-renders but `Button` skips because `handleClick` holds the same reference. ### Why function references matter Every render call creates new function objects. Two arrow functions with identical bodies are still different references: ```js const a = () => {}; const b = () => {}; console.log(a === b); // false ``` `React.memo` does a shallow prop comparison. A new function reference triggers a child re-render even though nothing functionally changed. `useCallback` breaks that cycle by returning the cached instance when deps match. ### When to use Use it when: - Passing a callback to a `React.memo`-wrapped child (the primary scenario) - A function is listed as a dependency in `useEffect` or `useMemo` - a new reference every render causes infinite loops - Handlers on items in large memoized lists Skip it when: - No memoized child is receiving the function - Dependencies change on every render anyway (the hook recreates regardless) - It's an internal utility used only inside the component, not passed as a prop ### How React stores callbacks internally React keeps memoized callbacks in the fiber node's `memoizedState` linked list. On each re-render, it compares deps using `Object.is` per element (shallow, not deep). If all elements match, React returns the cached closure without allocating a new one. If any dep changed, the cached entry is replaced. I've seen teams add `useCallback` everywhere as a reflex, expecting a global speed boost. It doesn't work that way. The savings come from child renders you skip, not from avoiding function allocation in isolation. ### useCallback vs useMemo | | **useCallback** | **useMemo** | |---|---|---| | Returns | A function | A computed value | | Typical use | Stable callback for a child or effect dep | Result of an expensive calculation | | Example | `useCallback(() => api.save(id), [id])` | `useMemo(() => filterList(todos), [todos])` | `useCallback(fn, deps)` is equivalent to `useMemo(() => fn, deps)` internally. If you need a stable reference to pass around, use `useCallback`. If you need to cache a computed result, use `useMemo`. ### Common mistakes **1. Empty deps when the callback captures state or props** ```jsx // Wrong: user.id is always the mount-time value const save = useCallback(() => api.save(user.id), []); // Fix const save = useCallback(() => api.save(user.id), [user.id]); ``` With `[]`, the closure captures the initial `user.id` and ignores all updates. Classic stale closure bug. **2. Wrapping functions that aren't passed as props** ```jsx // Wrong: no memoized child receives this, zero benefit const formatDate = useCallback((d) => d.toISOString(), []); // Fix: just a regular function const formatDate = (d) => d.toISOString(); ``` You pay the comparison cost on every render and gain nothing. **3. Object or array literal in the deps array** ```jsx // Wrong: config is a new object every render -> tick always recreates const config = { delay }; const tick = useCallback(() => doSomething(config), [config]); // Fix: use the primitive directly const tick = useCallback(() => doSomething(delay), [delay]); ``` `Object.is` compares references, not content. A new object literal on every render always fails the comparison and defeats the memoization. **4. Disabling `react-hooks/exhaustive-deps`** This ESLint rule catches deps you forgot to include. Disabling it to silence a warning is a pattern reviewers specifically look for in code reviews - and it ships stale closures to production. **5. Stale closure in timers** ```jsx // BUG: captures delay=1000 at mount, ignores later updates const tick = useCallback(() => { setCount(c => c + 1); }, []); // missing delay in deps useEffect(() => { const id = setInterval(tick, delay); return () => clearInterval(id); }, [tick, delay]); ``` Toggling delay shows the new value in the UI but the interval fires at the original speed. Fix: add `delay` to `useCallback` deps. `tick` will recreate when delay changes, and the interval will use the correct value. ### Real-world usage - React TodoMVC: stable `onRemove` callback passed to `React.memo(Todo)` items - Redux Toolkit Query: `useLazyQuery` trigger wrapped stable for button click handlers - Material-UI DataGrid: row handlers memoized to skip re-renders across hundreds of rows - Next.js App Router: server action callbacks kept stable across client navigations React Compiler (shipping with React 19) auto-memoizes components and hooks, which may reduce the need for manual `useCallback` in projects that adopt it. ### Follow-up questions **Q:** What is the difference between `useCallback(fn, [])` and storing a function in `useRef`? **A:** `useCallback` re-runs if deps change and participates in the `exhaustive-deps` lint rule. `useRef` never re-runs and has no deps check, so it fits one-time inits like storing an interval ID, but not callbacks that close over props or state. **Q:** Does `useCallback` help if the child isn't wrapped in `React.memo`? **A:** Not for skipping renders. But if the function is a `useEffect` dependency, a stable reference prevents the effect from re-running on every render. **Q:** Why does React use shallow comparison for deps instead of deep equality? **A:** Deep comparison on large arrays or nested objects would cost more than the re-render it tries to prevent. Keep deps as primitives or stable references and push stability upstream. **Q:** Can `useCallback` hurt performance? **A:** Yes. With 1000 memoized items and no dep changes, the memoization overhead is measurable in React Profiler (around 12ms in benchmarks). If deps change frequently, the hook recreates anyway and you paid the cost for nothing. Profile before adding it. **Q (senior):** In RTK Query, why does wrapping a `useMutation` trigger in `useCallback` matter? **A:** RTK Query caches results by argument reference. An unstable trigger creates a new cache key on every render, causing cache misses and breaking optimistic updates. A stable reference keeps the cache hit and lets the optimistic flow work correctly. ## Examples ### Memoized child skips re-render ```jsx import { useState, useCallback } from 'react'; function TodoList() { const [todos, setTodos] = useState([]); const addTodo = useCallback((text) => { setTodos(t => [...t, { id: Date.now(), text, done: false }]); }, []); const toggleTodo = useCallback((id) => { setTodos(t => t.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo )); }, []); return ( <div> <AddForm onAdd={addTodo} /> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} /> ))} </div> ); } const TodoItem = React.memo(({ todo, onToggle }) => { console.log(`Render item ${todo.id}`); return ( <label> <input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} /> {todo.text} </label> ); }); ``` `toggleTodo` holds the same reference on every render. Adding a new todo re-renders `TodoList` but each existing `TodoItem` skips because `onToggle` didn't change. ### Stabilizing a useEffect dependency ```jsx function UserProfile({ userId }) { const [profile, setProfile] = useState(null); // Without useCallback: fetchProfile is new every render // -> useEffect fires every render -> infinite loop const fetchProfile = useCallback(async () => { const data = await api.getUser(userId); setProfile(data); }, [userId]); // re-fetches only when userId changes useEffect(() => { fetchProfile(); }, [fetchProfile]); return profile ? <div>{profile.name}</div> : <div>Loading...</div>; } ``` Without `useCallback`, a new `fetchProfile` on every render makes `useEffect` fire continuously. With it, the effect only runs when `userId` actually changes. ### Stale closure in a timer ```jsx function Timer() { const [count, setCount] = useState(0); const [delay, setDelay] = useState(1000); // BUG: captures delay=1000 at mount, never updates const tick = useCallback(() => { setCount(c => c + 1); }, []); // missing delay in deps useEffect(() => { const id = setInterval(tick, delay); return () => clearInterval(id); }, [tick, delay]); return ( <> <div>Count: {count} | Delay: {delay}ms</div> <button onClick={() => setDelay(d => d === 1000 ? 100 : 1000)}> Toggle speed </button> </> ); } ``` Clicking "Toggle speed" updates the UI to show `100ms` but the interval still fires every 1000ms. The closure captured the initial delay. Fix: add `delay` to `useCallback` deps. `tick` will recreate when delay changes, and the interval will use the correct value.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.