useValidatedStateLoading saved progress…

useValidatedState

useValidatedState(initialState, validator) is a useState that answers one extra question: does the value it is holding right now pass? You get back the usual state and setter, plus a third slot — the validity of the current state. Every form wants this pairing, whether that is an email field and whether the address is well-formed or a password and whether it clears the rules. The interesting part is not working out the answer. It is deciding where the answer lives.

Signature

function useValidatedState<T, V>(
  initialState: T | (() => T), // seeds the state, exactly like useState
  validator: (value: T) => V,  // decides whether a value passes
): [
  T,                                       // the current state
  (next: T | ((current: T) => T)) => void, // the setter — useState's own
  V,                                       // the validity of the CURRENT state
];

Examples

A boolean validator is the common case:

const [email, setEmail, isValid] = useValidatedState('', (v) => v.includes('@'));

// isValid is false on the very first render — '' has no '@' in it.
setEmail('[email protected]'); // isValid is true on the next render

Whatever the validator returns is what you get back, so a richer answer costs the hook nothing:

const check = (v) => ({ valid: v.length >= 8, errors: v.length >= 8 ? [] : ['too short'] });

const [password, setPassword, validity] = useValidatedState('abc', check);
// validity is { valid: false, errors: ['too short'] } — on render 1, already.

Notes

  • The validity always describes the current state, on every render, the first one included. There is no frame where the two disagree.
  • One change, one render. A setState call must cost exactly one render. If your version renders twice per keystroke, something is being stored that should not be.
  • The setter is useState's setter. Functional updates, batching, lazy initial state, the bail-out when you set an unchanged value — inherit all of it by passing React's own setter straight through instead of wrapping it.
  • The validator may read more than the state. Something like (v) => v.length >= minLength closes over a prop. If minLength changes while the state sits still, the validity still has to move.
  • Falsy states are states. 0, '', false and null are values to be validated, not absences to skip.
  • Don't worry about async validators. The validator is synchronous and returns its answer directly. Checking a username against a server is a genuinely different problem — the solution's Going further covers why.
Loading editor…