Build a custom React hook that behaves like useState, with one twist: every value on its way into state first passes through a mediator function you supply. The mediator is the single chokepoint where you sanitize, clamp, or normalize a write — strip non-digits from a phone field, cap a number at a ceiling, uppercase a code. Instead of scattering that cleanup across every place that calls the setter, you define it once and the hook applies it to every write automatically.
function useMediatedState<T>(
mediator: (newValue: any, prevState: T) => T,
initialState: T
): [T, (value: any) => void];
The setter accepts either a plain value or a functional updater (prev) => next. The functional updater is resolved against the previous state first, and the result is then passed through mediator. The initialState is stored as-is and is not mediated.
function PhoneField() {
const digitsOnly = (v) => String(v).replace(/\D/g, '');
const [phone, setPhone] = useMediatedState(digitsOnly, '');
return (
<input
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
);
}
// Typing "(415) 555-0199" stores "4155550199" — the mediator runs on every keystroke.
// A clamping mediator caps every committed value at 10.
const clampTo10 = (n) => Math.min(n, 10);
const [n, setN] = useMediatedState(clampTo10, 0);
setN(5); // n === 5
setN(42); // n === 10 (clamped on the way in)
// A functional updater is resolved first, then mediated.
const stripLetters = (v) => String(v).replace(/\D/g, '');
const [code, setCode] = useMediatedState(stripLetters, '12');
setCode((prev) => prev + 'x4'); // prev '12' + 'x4' = '12x4' → mediated to '124'
(newValue, prevState), so it can make decisions relative to what is already stored (reject a decrease, merge, dedupe).