useMediatedStateLoading saved progress…

useMediatedState

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.

Signature

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.

Examples

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'

Notes

  • The mediator runs on write, not on read. Every value committed through the setter is mediated exactly once. State is never re-mediated when you read it during render.
  • The initial state is stored verbatim. It does not pass through the mediator. If you need a clean initial value, clean it yourself before passing it in.
  • Functional updaters are resolved first, then mediated. Given a function, call it with the previous state to get the raw next value, then run that through the mediator.
  • The mediator receives the previous state too. Its signature is (newValue, prevState), so it can make decisions relative to what is already stored (reject a decrease, merge, dedupe).
  • Writes must survive batching. Several setter calls in one event should each see the freshest pending state, so express the update against the previous state rather than a render-time snapshot.
Loading editor…