Build a custom React hook that behaves like useState but hands back one extra helper: a reset function that snaps the value back to whatever it started as. Forms, filter panels, and wizards all need a "start over" button, and re-implementing that by hand each time is busywork. useStateWithReset(initialValue) bundles the state, its setter, and a reset into a single tuple a component can drop in with one line.
function useStateWithReset<T>(
initialValue: T
): [
value: T,
setValue: (next: T | ((prev: T) => T)) => void,
reset: () => void,
];
value and setValue behave exactly like the pair from useState. reset() restores value to the original initialValue the hook was first created with.
function NameField() {
const [name, setName, reset] = useStateWithReset('');
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={reset}>Clear</button>
</div>
);
}
// typing fills the input; Clear snaps it back to ''
// setValue works with a direct value AND a functional updater, just like useState.
const [n, setN, reset] = useStateWithReset(0);
setN(5); // n === 5
setN((c) => c + 1); // n === 6
reset(); // n === 0 (back to the original)
reset targets the first initialValue. If the component re-renders and calls the hook with a different initialValue argument, reset must still return to the value the hook was created with on its very first render, not the new argument.setValue supports both forms. It must accept a direct value (setValue('x')) and a functional updater (setValue((prev) => ...)), exactly like useState's setter.[value, setValue, reset] in that order so callers can destructure and rename freely.reset should have a stable identity. Its reference should not change between renders, so it is safe to pass to memoized children or effect dependency arrays.You'll wrap a single useState and add one helper — a reset that always snaps the value back to where it started, no matter what's happened since.
Lots of UI has a "start over" button: a filter panel you want to clear, a form field you want to wipe, a wizard step you want to abandon. The state itself is ordinary useState, but on top of the usual value and setValue you need a way to jump straight back to the beginning. useStateWithReset(initialValue) packages all three together: the current value, a setValue that behaves exactly like the one from useState, and a reset that returns the value to the original initialValue.
The hook keeps two separate memories. The live value lives in useState and changes every time setValue runs. The original starting point is captured once in a ref — a mutable box whose .current survives across renders without causing a re-render. reset is just "copy the snapshot back into the live value": it calls setValue(initialRef.current). Because the snapshot was taken on the first render and never touched again, reset always knows the true origin.
The obvious version reads initialValue directly inside reset:
const { useState } = require('react');
function useStateWithReset(initialValue) {
const [value, setValue] = useState(initialValue);
const reset = () => setValue(initialValue);
return [value, setValue, reset];
}
For a component whose initialValue never changes, this works. The trap appears when the component re-renders with a different initialValue argument — say it was created with 'a' but a later render passes 'b'. The useState(initialValue) call ignores the new argument (state is only seeded once), so value is unaffected, which is correct. But reset is a fresh closure each render, and on the 'b' render it closes over 'b'. Now reset() jumps to 'b', not the 'a' the user actually started with — a surprising snap to a value they never saw at the start.
const { useState, useRef, useCallback } = require('react');
function useStateWithReset(initialValue) {
const [value, setValue] = useState(initialValue);
// Snapshot the FIRST initialValue once. useRef(initialValue) seeds .current
// on the first render and then ignores its argument forever after, so this
// box keeps the true original even if the hook is later called with a
// different initialValue.
const initialRef = useRef(initialValue);
// useCallback with [] gives reset a stable identity across renders, and it
// reads the snapshot — never the current initialValue argument — so it always
// returns to where the hook truly started.
const reset = useCallback(() => {
setValue(initialRef.current);
}, []);
return [value, setValue, reset];
}
module.exports = { useStateWithReset };
The key shift is where reset gets its target. The naive version reads initialValue from the current render's closure, which drifts as the argument changes; the working version reads initialRef.current, a value frozen on the first render. setValue is React's own useState setter, returned untouched, so it already accepts both a direct value and a functional updater. Wrapping reset in useCallback([]) keeps its reference stable, which matters when it's passed to a memoized child or listed in an effect's dependencies.
Create the hook with useStateWithReset('a'), then re-render it later with a different argument:
initialValue = 'a'. useState('a') seeds value to 'a'. useRef('a') sets initialRef.current to 'a'. reset is created once and memoized.setValue('changed') runs. value becomes 'changed'. initialRef.current is still 'a' — refs don't change on their own.initialValue = 'b'. useState('b') ignores the new argument and keeps value at 'changed'. useRef('b') also ignores its argument and keeps initialRef.current at 'a'. reset is the same memoized function as before.reset() fires. It runs setValue(initialRef.current), which is setValue('a'). value becomes 'a' — the true original — not 'b'.Had reset read initialValue directly, step 4 would have snapped to 'b', because that was the argument on the latest render.
initialValue directly in reset. () => setValue(initialValue) ties reset to whatever argument the current render received, so a re-render with a new initial value silently changes the reset target. Fix: snapshot the first value in useRef(initialValue) and reset to initialRef.current.initialRef.current = initialValue (instead of letting useRef set it once) overwrites the snapshot with the latest argument, recreating the exact bug. Fix: pass initialValue to useRef and never reassign .current.setValue by hand. Wrapping the setter as (next) => setValue(next) works for direct values but breaks the functional-updater form unless you forward it carefully. Fix: return React's setValue as-is — it already supports both setValue(x) and setValue((prev) => ...).reset identity. Returning a fresh reset each render (no useCallback) defeats React.memo on a child that takes reset as a prop and can retrigger effects that list it as a dependency. Fix: wrap it in useCallback(fn, []).setValue that also exposes the original. A variant could return { value, setValue, reset, initialValue } so callers can show "unchanged" badges or diff against the start without managing their own ref.useState's lazy form by accepting initialValue as a function and computing it once: useState(() => init()) plus useRef seeded from the same computed value, useful when the initial value is expensive to build.resetTo(next). Adding a second helper that both updates the value and makes it the new reset target lets a form "save" a draft as the fresh baseline that future resets return to.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 behaves like useState but hands back one extra helper: a reset function that snaps the value back to whatever it started as. Forms, filter panels, and wizards all need a "start over" button, and re-implementing that by hand each time is busywork. useStateWithReset(initialValue) bundles the state, its setter, and a reset into a single tuple a component can drop in with one line.
function useStateWithReset<T>(
initialValue: T
): [
value: T,
setValue: (next: T | ((prev: T) => T)) => void,
reset: () => void,
];
value and setValue behave exactly like the pair from useState. reset() restores value to the original initialValue the hook was first created with.
function NameField() {
const [name, setName, reset] = useStateWithReset('');
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={reset}>Clear</button>
</div>
);
}
// typing fills the input; Clear snaps it back to ''
// setValue works with a direct value AND a functional updater, just like useState.
const [n, setN, reset] = useStateWithReset(0);
setN(5); // n === 5
setN((c) => c + 1); // n === 6
reset(); // n === 0 (back to the original)
reset targets the first initialValue. If the component re-renders and calls the hook with a different initialValue argument, reset must still return to the value the hook was created with on its very first render, not the new argument.setValue supports both forms. It must accept a direct value (setValue('x')) and a functional updater (setValue((prev) => ...)), exactly like useState's setter.[value, setValue, reset] in that order so callers can destructure and rename freely.reset should have a stable identity. Its reference should not change between renders, so it is safe to pass to memoized children or effect dependency arrays.You'll wrap a single useState and add one helper — a reset that always snaps the value back to where it started, no matter what's happened since.
Lots of UI has a "start over" button: a filter panel you want to clear, a form field you want to wipe, a wizard step you want to abandon. The state itself is ordinary useState, but on top of the usual value and setValue you need a way to jump straight back to the beginning. useStateWithReset(initialValue) packages all three together: the current value, a setValue that behaves exactly like the one from useState, and a reset that returns the value to the original initialValue.
The hook keeps two separate memories. The live value lives in useState and changes every time setValue runs. The original starting point is captured once in a ref — a mutable box whose .current survives across renders without causing a re-render. reset is just "copy the snapshot back into the live value": it calls setValue(initialRef.current). Because the snapshot was taken on the first render and never touched again, reset always knows the true origin.
The obvious version reads initialValue directly inside reset:
const { useState } = require('react');
function useStateWithReset(initialValue) {
const [value, setValue] = useState(initialValue);
const reset = () => setValue(initialValue);
return [value, setValue, reset];
}
For a component whose initialValue never changes, this works. The trap appears when the component re-renders with a different initialValue argument — say it was created with 'a' but a later render passes 'b'. The useState(initialValue) call ignores the new argument (state is only seeded once), so value is unaffected, which is correct. But reset is a fresh closure each render, and on the 'b' render it closes over 'b'. Now reset() jumps to 'b', not the 'a' the user actually started with — a surprising snap to a value they never saw at the start.
const { useState, useRef, useCallback } = require('react');
function useStateWithReset(initialValue) {
const [value, setValue] = useState(initialValue);
// Snapshot the FIRST initialValue once. useRef(initialValue) seeds .current
// on the first render and then ignores its argument forever after, so this
// box keeps the true original even if the hook is later called with a
// different initialValue.
const initialRef = useRef(initialValue);
// useCallback with [] gives reset a stable identity across renders, and it
// reads the snapshot — never the current initialValue argument — so it always
// returns to where the hook truly started.
const reset = useCallback(() => {
setValue(initialRef.current);
}, []);
return [value, setValue, reset];
}
module.exports = { useStateWithReset };
The key shift is where reset gets its target. The naive version reads initialValue from the current render's closure, which drifts as the argument changes; the working version reads initialRef.current, a value frozen on the first render. setValue is React's own useState setter, returned untouched, so it already accepts both a direct value and a functional updater. Wrapping reset in useCallback([]) keeps its reference stable, which matters when it's passed to a memoized child or listed in an effect's dependencies.
Create the hook with useStateWithReset('a'), then re-render it later with a different argument:
initialValue = 'a'. useState('a') seeds value to 'a'. useRef('a') sets initialRef.current to 'a'. reset is created once and memoized.setValue('changed') runs. value becomes 'changed'. initialRef.current is still 'a' — refs don't change on their own.initialValue = 'b'. useState('b') ignores the new argument and keeps value at 'changed'. useRef('b') also ignores its argument and keeps initialRef.current at 'a'. reset is the same memoized function as before.reset() fires. It runs setValue(initialRef.current), which is setValue('a'). value becomes 'a' — the true original — not 'b'.Had reset read initialValue directly, step 4 would have snapped to 'b', because that was the argument on the latest render.
initialValue directly in reset. () => setValue(initialValue) ties reset to whatever argument the current render received, so a re-render with a new initial value silently changes the reset target. Fix: snapshot the first value in useRef(initialValue) and reset to initialRef.current.initialRef.current = initialValue (instead of letting useRef set it once) overwrites the snapshot with the latest argument, recreating the exact bug. Fix: pass initialValue to useRef and never reassign .current.setValue by hand. Wrapping the setter as (next) => setValue(next) works for direct values but breaks the functional-updater form unless you forward it carefully. Fix: return React's setValue as-is — it already supports both setValue(x) and setValue((prev) => ...).reset identity. Returning a fresh reset each render (no useCallback) defeats React.memo on a child that takes reset as a prop and can retrigger effects that list it as a dependency. Fix: wrap it in useCallback(fn, []).setValue that also exposes the original. A variant could return { value, setValue, reset, initialValue } so callers can show "unchanged" badges or diff against the start without managing their own ref.useState's lazy form by accepting initialValue as a function and computing it once: useState(() => init()) plus useRef seeded from the same computed value, useful when the initial value is expensive to build.resetTo(next). Adding a second helper that both updates the value and makes it the new reset target lets a form "save" a draft as the fresh baseline that future resets return to.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.