You already built useCounter — a hook that returns a count plus increment, decrement, and reset. It works, but it rebuilds those three helper functions from scratch on every render, so each render hands the component a new function with the same behavior but a different identity. This version fixes that: the helpers must keep the same identity (===) across renders. Stable identities matter because a child wrapped in React.memo re-renders whenever a prop reference changes — passing it a fresh onClick every render quietly defeats the memoization.
function useCounterIi(initialValue?: number): {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
};
The API is identical to useCounter. initialValue defaults to 0; reset restores that original value, not 0. The new requirement is purely about identity: across any number of renders, the increment, decrement, and reset you return must be the very same function objects.
function Stepper() {
const { count, increment, decrement, reset } = useCounterIi(10);
// Passing increment to a memoized child is safe: its identity never changes,
// so the child does not re-render just because the parent did.
return <Controls value={count} onUp={increment} onDown={decrement} onReset={reset} />;
}
// After a re-render, each helper is the SAME function object as before.
const first = result.current.increment;
rerender();
result.current.increment === first; // true
useCounter; what is graded here is that the returned helpers are stable across renders.increment and decrement still use functional updaters. setCount((c) => c + 1) reads the latest pending value, so an empty dependency array is correct and they never need to be recreated.reset is the subtle one. Restoring the initial value while keeping a stable identity means you must not depend on initialValue in a way that recreates the function when it changes.decrement has no lower bound; clamping is out of scope.You'll take the working useCounter and pin down the identity of its three helpers, so they survive re-renders as the same function objects instead of being rebuilt each time.
The original useCounter is correct: increment, decrement, and reset all do the right thing. But it defines them with plain arrow functions in the hook body, so React creates three brand-new functions every render. Same behavior, different identity. That's invisible until you hand one of those functions to a child wrapped in React.memo — which skips re-rendering only when its props are referentially equal to last time. A fresh onClick on every parent render looks like a changed prop, so the "memoized" child re-renders anyway. The job here is to keep each helper at one stable reference for the lifetime of the hook.
useCallback is React's tool for this. It memoizes a function: given the same dependency array, it returns the same function instance it returned last render instead of the freshly-created one. With an empty dependency array ([]), the function is created once on the first render and reused forever after. The catch is that the function must not close over anything that can go stale — if it reads a value that changes, an empty dependency array lies, and you'll capture the first render's value forever. So the whole exercise is arranging each helper to need zero dependencies.
The natural first move is to wrap each helper in useCallback and list whatever it reads as a dependency:
const { useState, useCallback } = require('react');
function useCounterIi(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return { count, increment, decrement, reset };
}
increment and decrement are already perfect: the functional updater (c) => c + 1 reads no outside value, so [] is honest and they're created once. The problem is reset. It reads initialValue, so to be correct you list it as a dependency — and now reset gets a new identity every time initialValue changes. For a fixed useCounterIi(10) that never happens, but a caller passing a value that updates (say from props) would see reset's reference churn, which is exactly the instability this question is about eliminating.
const { useState, useRef, useCallback } = require('react');
function useCounterIi(initialValue = 0) {
const [count, setCount] = useState(initialValue);
// Capture the starting value once, in a ref. A ref's .current persists across
// renders and isn't a "dependency" that React tracks, so reset can read it
// without listing initialValue — letting reset keep an empty dep array.
const initialRef = useRef(initialValue);
// Functional updaters read the latest pending value, not a render-time
// snapshot, so these need no dependencies and are created exactly once.
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
// reset reads the captured value from the ref, never from initialValue
// directly, so its dependency array is also empty and its identity is fixed.
const reset = useCallback(() => setCount(initialRef.current), []);
return { count, increment, decrement, reset };
}
module.exports = { useCounterIi };
The shift from the first attempt is one move: instead of reading initialValue inside reset (which forces it into the dependency array), capture that value once in a ref and read initialRef.current. useRef(initialValue) runs its initializer only on the first render, so the ref holds the original value permanently, and reset can carry an empty dependency array honestly. Now all three helpers are created on the first render and reused on every render after.
This is a genuine tradeoff worth naming: by capturing the initial value once, reset deliberately ignores later changes to initialValue — that's the price of a fully stable identity. For a counter whose initial value is a constant, that's exactly what you want. If you genuinely needed reset to track a changing initialValue, you'd accept [initialValue] and the identity churn that comes with it.
Render the hook with useCounterIi(0) and follow increment across a click and the re-render it triggers:
useState(0) makes count be 0. useRef(0) creates initialRef holding 0. Each useCallback(..., []) creates its function and caches it; call the increment instance #a. The hook returns count = 0 and the three helpers.increment(), which runs setCount((c) => c + 1). React queues the updater, computes 0 + 1, and schedules a re-render.useState now returns 1, so count is 1. Each useCallback sees the same empty dependency array as last render, so it skips creating new functions and returns the cached instances — increment is still #a, the exact same object as step 1.Because nothing in the dependency arrays ever changes, the helpers you got on render 1 are === to the ones you get on render 2, render 3, and so on. count updates; the functions don't.
initialValue as a reset dependency. useCallback(() => setCount(initialValue), [initialValue]) is correct behavior but unstable — reset gets a new identity whenever initialValue changes. Fix: capture the value once in a ref and read initialRef.current with an empty dependency array.useCallback(() => setCount(count + 1), []) freezes count at its first-render value forever, so increment stops working after one click. Fix: use the functional updater (c) => c + 1, which reads no outside value and is safe with [].const increment = () => ... without useCallback rebuilds it every render, the exact problem this question fixes. The whole point is to wrap them so the reference is stable.initialRef and still calling setCount(initialValue) inside reset reintroduces the dependency. Read from initialRef.current so the empty dependency array stays honest.useMemo for the whole object? You could wrap the returned { count, increment, decrement, reset } in useMemo, but count changes every update so the object identity must change anyway. Memoizing the individual functions is the right granularity.step option. Accepting useCounterIi(0, { step }) lets increment move by 5 or 10. Keep step out of the helpers' closures the same way — capture it in a ref — if you want the helpers to stay identity-stable.useReducer instead. Modeling the counter with useReducer gives you a dispatch whose identity React guarantees is stable, so you'd get stable actions without writing useCallback at all — a common pattern once a hook grows more than a couple of helpers.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
You already built useCounter — a hook that returns a count plus increment, decrement, and reset. It works, but it rebuilds those three helper functions from scratch on every render, so each render hands the component a new function with the same behavior but a different identity. This version fixes that: the helpers must keep the same identity (===) across renders. Stable identities matter because a child wrapped in React.memo re-renders whenever a prop reference changes — passing it a fresh onClick every render quietly defeats the memoization.
function useCounterIi(initialValue?: number): {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
};
The API is identical to useCounter. initialValue defaults to 0; reset restores that original value, not 0. The new requirement is purely about identity: across any number of renders, the increment, decrement, and reset you return must be the very same function objects.
function Stepper() {
const { count, increment, decrement, reset } = useCounterIi(10);
// Passing increment to a memoized child is safe: its identity never changes,
// so the child does not re-render just because the parent did.
return <Controls value={count} onUp={increment} onDown={decrement} onReset={reset} />;
}
// After a re-render, each helper is the SAME function object as before.
const first = result.current.increment;
rerender();
result.current.increment === first; // true
useCounter; what is graded here is that the returned helpers are stable across renders.increment and decrement still use functional updaters. setCount((c) => c + 1) reads the latest pending value, so an empty dependency array is correct and they never need to be recreated.reset is the subtle one. Restoring the initial value while keeping a stable identity means you must not depend on initialValue in a way that recreates the function when it changes.decrement has no lower bound; clamping is out of scope.You'll take the working useCounter and pin down the identity of its three helpers, so they survive re-renders as the same function objects instead of being rebuilt each time.
The original useCounter is correct: increment, decrement, and reset all do the right thing. But it defines them with plain arrow functions in the hook body, so React creates three brand-new functions every render. Same behavior, different identity. That's invisible until you hand one of those functions to a child wrapped in React.memo — which skips re-rendering only when its props are referentially equal to last time. A fresh onClick on every parent render looks like a changed prop, so the "memoized" child re-renders anyway. The job here is to keep each helper at one stable reference for the lifetime of the hook.
useCallback is React's tool for this. It memoizes a function: given the same dependency array, it returns the same function instance it returned last render instead of the freshly-created one. With an empty dependency array ([]), the function is created once on the first render and reused forever after. The catch is that the function must not close over anything that can go stale — if it reads a value that changes, an empty dependency array lies, and you'll capture the first render's value forever. So the whole exercise is arranging each helper to need zero dependencies.
The natural first move is to wrap each helper in useCallback and list whatever it reads as a dependency:
const { useState, useCallback } = require('react');
function useCounterIi(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return { count, increment, decrement, reset };
}
increment and decrement are already perfect: the functional updater (c) => c + 1 reads no outside value, so [] is honest and they're created once. The problem is reset. It reads initialValue, so to be correct you list it as a dependency — and now reset gets a new identity every time initialValue changes. For a fixed useCounterIi(10) that never happens, but a caller passing a value that updates (say from props) would see reset's reference churn, which is exactly the instability this question is about eliminating.
const { useState, useRef, useCallback } = require('react');
function useCounterIi(initialValue = 0) {
const [count, setCount] = useState(initialValue);
// Capture the starting value once, in a ref. A ref's .current persists across
// renders and isn't a "dependency" that React tracks, so reset can read it
// without listing initialValue — letting reset keep an empty dep array.
const initialRef = useRef(initialValue);
// Functional updaters read the latest pending value, not a render-time
// snapshot, so these need no dependencies and are created exactly once.
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
// reset reads the captured value from the ref, never from initialValue
// directly, so its dependency array is also empty and its identity is fixed.
const reset = useCallback(() => setCount(initialRef.current), []);
return { count, increment, decrement, reset };
}
module.exports = { useCounterIi };
The shift from the first attempt is one move: instead of reading initialValue inside reset (which forces it into the dependency array), capture that value once in a ref and read initialRef.current. useRef(initialValue) runs its initializer only on the first render, so the ref holds the original value permanently, and reset can carry an empty dependency array honestly. Now all three helpers are created on the first render and reused on every render after.
This is a genuine tradeoff worth naming: by capturing the initial value once, reset deliberately ignores later changes to initialValue — that's the price of a fully stable identity. For a counter whose initial value is a constant, that's exactly what you want. If you genuinely needed reset to track a changing initialValue, you'd accept [initialValue] and the identity churn that comes with it.
Render the hook with useCounterIi(0) and follow increment across a click and the re-render it triggers:
useState(0) makes count be 0. useRef(0) creates initialRef holding 0. Each useCallback(..., []) creates its function and caches it; call the increment instance #a. The hook returns count = 0 and the three helpers.increment(), which runs setCount((c) => c + 1). React queues the updater, computes 0 + 1, and schedules a re-render.useState now returns 1, so count is 1. Each useCallback sees the same empty dependency array as last render, so it skips creating new functions and returns the cached instances — increment is still #a, the exact same object as step 1.Because nothing in the dependency arrays ever changes, the helpers you got on render 1 are === to the ones you get on render 2, render 3, and so on. count updates; the functions don't.
initialValue as a reset dependency. useCallback(() => setCount(initialValue), [initialValue]) is correct behavior but unstable — reset gets a new identity whenever initialValue changes. Fix: capture the value once in a ref and read initialRef.current with an empty dependency array.useCallback(() => setCount(count + 1), []) freezes count at its first-render value forever, so increment stops working after one click. Fix: use the functional updater (c) => c + 1, which reads no outside value and is safe with [].const increment = () => ... without useCallback rebuilds it every render, the exact problem this question fixes. The whole point is to wrap them so the reference is stable.initialRef and still calling setCount(initialValue) inside reset reintroduces the dependency. Read from initialRef.current so the empty dependency array stays honest.useMemo for the whole object? You could wrap the returned { count, increment, decrement, reset } in useMemo, but count changes every update so the object identity must change anyway. Memoizing the individual functions is the right granularity.step option. Accepting useCounterIi(0, { step }) lets increment move by 5 or 10. Keep step out of the helpers' closures the same way — capture it in a ref — if you want the helpers to stay identity-stable.useReducer instead. Modeling the counter with useReducer gives you a dispatch whose identity React guarantees is stable, so you'd get stable actions without writing useCallback at all — a common pattern once a hook grows more than a couple of helpers.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.