Build a custom React hook that owns a single boolean and hands back a function to flip it. Toggles are everywhere — a dark-mode switch, a "show more" panel, a modal that's open or closed, a checkbox. useToggle(initialValue) returns a tuple [value, toggle]: read value to render, call toggle() to invert it. The interesting part isn't the boolean; it's wiring toggle so it flips correctly even when called twice in a row, accepts an explicit target, survives being attached straight to an onClick, and keeps the same function identity across renders.
function useToggle(initialValue?: boolean): [boolean, (next?: boolean) => void];
initialValue defaults to false. Call toggle() with no argument to flip; call toggle(true) or toggle(false) to set it explicitly. If toggle is handed a non-boolean (like a click event), it ignores the argument and flips.
function Panel() {
const [open, toggle] = useToggle(false);
return (
<div>
<button onClick={toggle}>{open ? 'Hide' : 'Show'}</button>
{open && <p>Now you can see me.</p>}
</div>
);
}
// starts hidden; clicking the button flips open ↔ closed each time
// no-arg flips; a boolean sets explicitly; a non-boolean still flips
const [value, toggle] = useToggle(false);
toggle(); // value === true (flipped)
toggle(false); // value === false (set explicitly)
toggle(true); // value === true (set explicitly)
toggle(true); // value === true (idempotent — set, not flip)
toggle({ type: 'click' }); // value === false (non-boolean → flip)
toggle() inverts the current value, but toggle(true) / toggle(false) force a specific value regardless of what it was.toggle is passed straight to onClick, React calls it with an event object. Guard with typeof next === 'boolean' so the truthy event does not get read as true.toggle must keep a stable identity. It should be the same function reference on every render, not a fresh one each time, so it can be a safe dependency and not break memoized children.toggle() twice in a single update should land back on the original value — this is what separates a correct implementation from the stale-closure trap.You'll wrap a single boolean in useState and return it next to one small function that flips it — written so the flip is correct under batching and the function never changes identity.
Half the controls in any interface are just on or off: a sidebar that's open or closed, a password field that's masked or shown, a dark-mode switch. Each one holds a boolean and needs a single action to invert it. useToggle packages that: it owns the boolean with useState and returns [value, toggle], where toggle() flips the value, toggle(true) or toggle(false) sets it outright, and toggle handed to an onClick still just flips even though React calls it with an event object.
A custom hook is a function that calls other hooks. useToggle calls useState for the boolean, then returns that value alongside one function that changes it. The component reads value to decide what to render and calls toggle to change it. Two things have to be true about toggle: it must compute the next boolean from the latest value (not a frozen snapshot), and it must be the same function object on every render so it's a stable prop and dependency.
The obvious move is to read value and set it to its opposite:
const { useState } = require('react');
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(!value);
return [value, toggle];
}
module.exports = { useToggle };
For a single click this works. Two problems hide underneath. First, value is a snapshot captured when the hook rendered — frozen at, say, false. If a handler calls toggle() twice in one event, both closures read that same frozen false and both call setValue(true); React batches them, and you end on true instead of back at false. Second, a brand-new toggle function is created on every render, so its identity churns — pass it to a memo-wrapped child and the memoization breaks. This version also can't honor toggle(true) / toggle(false): it ignores its argument entirely.
const { useState, useCallback } = require('react');
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
// useCallback with an empty dependency array returns the SAME function on
// every render, so `toggle` keeps a stable identity. The functional updater
// (v) => ... reads the value React is about to apply, so flips stack
// correctly even when two land in one batch. We only treat `next` as an
// explicit target when it is actually a boolean — a click event (an object)
// falls through to the flip instead of being coerced to true.
const toggle = useCallback((next) => {
setValue((v) => (typeof next === 'boolean' ? next : !v));
}, []);
return [value, toggle];
}
module.exports = { useToggle };
Two shifts from the naive version. The updater is now a function ((v) => ...) instead of a value, so each call computes from the freshest pending state rather than a stale snapshot — two flips in one batch now cancel out. And toggle is wrapped in useCallback with an empty dependency array, so React hands back the identical function reference on every render. The typeof next === 'boolean' guard is what lets one function serve both jobs: a real boolean sets the value explicitly, while anything else — including the event object React passes to an onClick handler — falls through to a plain flip.
Start with useToggle(false), so value is false. Now trace three uses of the returned toggle:
toggle() once. No argument, so next is undefined. setValue((v) => typeof undefined === 'boolean' ? undefined : !v) runs the updater with the pending value false, producing !false → true. The screen re-renders with true.toggle() twice in one event. Both calls queue the same (v) => !v updater. React calls the first with the pending false → true, then the second with true → false. The batch resolves to false — right back where it started. The naive setValue(!value) would have seen false both times and landed on true.toggle({ type: 'click' }) from an onClick. next is the event object. typeof next === 'boolean' is false, so the updater takes the !v branch and flips — the truthy object never gets mistaken for true. Had we written setValue(next), the value would have become the event object itself.Because toggle came from useCallback([]), every one of these calls used the exact same function reference — the identity never changed across the re-renders.
setValue(!value) captures the value from the render that created the function, so two flips in one event both read the same snapshot and you flip only once. Fix: use the functional form setValue((v) => !v), which receives the freshest pending value.toggle to onClick calls it with an event object; setValue(next) would store that object, and next ? true : false would always force true. Fix: guard with typeof next === 'boolean' so only a real boolean sets explicitly and everything else flips.toggle every render. Defining toggle as a plain arrow in the hook body makes a new function each render, defeating React.memo on children and tripping exhaustive-deps warnings. Fix: wrap it in useCallback with an empty dependency array — the functional updater needs no dependencies, so the function never has to change.value in the dependency array. useCallback(..., [value]) rebuilds toggle on every change, throwing away the stable identity you wanted. Because the updater reads v from React rather than closing over value, the dependency array can stay empty.useToggle variant could also return { setTrue, setFalse } (each a useCallback) so callers can write onClick={setTrue} without remembering which boolean a bare toggle lands on.useCycle(['sm', 'md', 'lg']), where calling the returned function advances to the next item and wraps around — same functional-updater and stable-identity pattern, applied to an index instead of a boolean.useCallback([], ...), but the updater reads its input from React's pending state, not from a captured variable — so there's nothing stale to capture, and the function can safely live forever.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build a custom React hook that owns a single boolean and hands back a function to flip it. Toggles are everywhere — a dark-mode switch, a "show more" panel, a modal that's open or closed, a checkbox. useToggle(initialValue) returns a tuple [value, toggle]: read value to render, call toggle() to invert it. The interesting part isn't the boolean; it's wiring toggle so it flips correctly even when called twice in a row, accepts an explicit target, survives being attached straight to an onClick, and keeps the same function identity across renders.
function useToggle(initialValue?: boolean): [boolean, (next?: boolean) => void];
initialValue defaults to false. Call toggle() with no argument to flip; call toggle(true) or toggle(false) to set it explicitly. If toggle is handed a non-boolean (like a click event), it ignores the argument and flips.
function Panel() {
const [open, toggle] = useToggle(false);
return (
<div>
<button onClick={toggle}>{open ? 'Hide' : 'Show'}</button>
{open && <p>Now you can see me.</p>}
</div>
);
}
// starts hidden; clicking the button flips open ↔ closed each time
// no-arg flips; a boolean sets explicitly; a non-boolean still flips
const [value, toggle] = useToggle(false);
toggle(); // value === true (flipped)
toggle(false); // value === false (set explicitly)
toggle(true); // value === true (set explicitly)
toggle(true); // value === true (idempotent — set, not flip)
toggle({ type: 'click' }); // value === false (non-boolean → flip)
toggle() inverts the current value, but toggle(true) / toggle(false) force a specific value regardless of what it was.toggle is passed straight to onClick, React calls it with an event object. Guard with typeof next === 'boolean' so the truthy event does not get read as true.toggle must keep a stable identity. It should be the same function reference on every render, not a fresh one each time, so it can be a safe dependency and not break memoized children.toggle() twice in a single update should land back on the original value — this is what separates a correct implementation from the stale-closure trap.You'll wrap a single boolean in useState and return it next to one small function that flips it — written so the flip is correct under batching and the function never changes identity.
Half the controls in any interface are just on or off: a sidebar that's open or closed, a password field that's masked or shown, a dark-mode switch. Each one holds a boolean and needs a single action to invert it. useToggle packages that: it owns the boolean with useState and returns [value, toggle], where toggle() flips the value, toggle(true) or toggle(false) sets it outright, and toggle handed to an onClick still just flips even though React calls it with an event object.
A custom hook is a function that calls other hooks. useToggle calls useState for the boolean, then returns that value alongside one function that changes it. The component reads value to decide what to render and calls toggle to change it. Two things have to be true about toggle: it must compute the next boolean from the latest value (not a frozen snapshot), and it must be the same function object on every render so it's a stable prop and dependency.
The obvious move is to read value and set it to its opposite:
const { useState } = require('react');
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(!value);
return [value, toggle];
}
module.exports = { useToggle };
For a single click this works. Two problems hide underneath. First, value is a snapshot captured when the hook rendered — frozen at, say, false. If a handler calls toggle() twice in one event, both closures read that same frozen false and both call setValue(true); React batches them, and you end on true instead of back at false. Second, a brand-new toggle function is created on every render, so its identity churns — pass it to a memo-wrapped child and the memoization breaks. This version also can't honor toggle(true) / toggle(false): it ignores its argument entirely.
const { useState, useCallback } = require('react');
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
// useCallback with an empty dependency array returns the SAME function on
// every render, so `toggle` keeps a stable identity. The functional updater
// (v) => ... reads the value React is about to apply, so flips stack
// correctly even when two land in one batch. We only treat `next` as an
// explicit target when it is actually a boolean — a click event (an object)
// falls through to the flip instead of being coerced to true.
const toggle = useCallback((next) => {
setValue((v) => (typeof next === 'boolean' ? next : !v));
}, []);
return [value, toggle];
}
module.exports = { useToggle };
Two shifts from the naive version. The updater is now a function ((v) => ...) instead of a value, so each call computes from the freshest pending state rather than a stale snapshot — two flips in one batch now cancel out. And toggle is wrapped in useCallback with an empty dependency array, so React hands back the identical function reference on every render. The typeof next === 'boolean' guard is what lets one function serve both jobs: a real boolean sets the value explicitly, while anything else — including the event object React passes to an onClick handler — falls through to a plain flip.
Start with useToggle(false), so value is false. Now trace three uses of the returned toggle:
toggle() once. No argument, so next is undefined. setValue((v) => typeof undefined === 'boolean' ? undefined : !v) runs the updater with the pending value false, producing !false → true. The screen re-renders with true.toggle() twice in one event. Both calls queue the same (v) => !v updater. React calls the first with the pending false → true, then the second with true → false. The batch resolves to false — right back where it started. The naive setValue(!value) would have seen false both times and landed on true.toggle({ type: 'click' }) from an onClick. next is the event object. typeof next === 'boolean' is false, so the updater takes the !v branch and flips — the truthy object never gets mistaken for true. Had we written setValue(next), the value would have become the event object itself.Because toggle came from useCallback([]), every one of these calls used the exact same function reference — the identity never changed across the re-renders.
setValue(!value) captures the value from the render that created the function, so two flips in one event both read the same snapshot and you flip only once. Fix: use the functional form setValue((v) => !v), which receives the freshest pending value.toggle to onClick calls it with an event object; setValue(next) would store that object, and next ? true : false would always force true. Fix: guard with typeof next === 'boolean' so only a real boolean sets explicitly and everything else flips.toggle every render. Defining toggle as a plain arrow in the hook body makes a new function each render, defeating React.memo on children and tripping exhaustive-deps warnings. Fix: wrap it in useCallback with an empty dependency array — the functional updater needs no dependencies, so the function never has to change.value in the dependency array. useCallback(..., [value]) rebuilds toggle on every change, throwing away the stable identity you wanted. Because the updater reads v from React rather than closing over value, the dependency array can stay empty.useToggle variant could also return { setTrue, setFalse } (each a useCallback) so callers can write onClick={setTrue} without remembering which boolean a bare toggle lands on.useCycle(['sm', 'md', 'lg']), where calling the returned function advances to the next item and wraps around — same functional-updater and stable-identity pattern, applied to an index instead of a boolean.useCallback([], ...), but the updater reads its input from React's pending state, not from a captured variable — so there's nothing stale to capture, and the function can safely live forever.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.