useInputControlLoading saved progress…

useInputControl

Build a custom React hook that owns a single controlled input and the two flags a form usually wants alongside it: dirty (has the value changed from where it started?) and touched (has the user left the field at least once?). Most forms re-implement this by hand for every field. useInputControl(initialValue) packages the value, its event handlers, and both flags into one object a component can wire to an <input> in a single line.

Signature

function useInputControl(initialValue?: string): {
  value: string;                                   // current input value
  onChange: (event: { target: { value: string } }) => void; // set value from event
  onBlur: () => void;                              // mark the field touched
  dirty: boolean;                                  // value !== ORIGINAL initialValue
  touched: boolean;                                // field has been blurred once
  reset: () => void;                               // restore value, clear flags
};

initialValue defaults to ''. dirty is measured against the value the hook was first created with, not against any later argument.

Examples

function EmailField() {
  const email = useInputControl('');

  return (
    <label>
      Email
      <input value={email.value} onChange={email.onChange} onBlur={email.onBlur} />
      {email.touched && email.dirty && <span>unsaved changes</span>}
      <button onClick={email.reset}>Reset</button>
    </label>
  );
}
// typing flips dirty=true; leaving the field flips touched=true; Reset clears both.
// dirty tracks equality with the ORIGINAL initial value, both ways.
const f = useInputControl('alice');
// f.value === 'alice', f.dirty === false, f.touched === false
// onChange({ target: { value: 'bob' } })   → value 'bob',   dirty true
// onChange({ target: { value: 'alice' } }) → value 'alice', dirty false again
// onBlur()                                  → touched true
// reset()                                   → value 'alice', dirty false, touched false

Notes

  • dirty is computed, not stored. It is the live comparison value !== originalInitial. Typing away from the start makes it true; typing back to the exact original makes it false again.
  • dirty targets the FIRST initial value. If the component re-renders and calls the hook with a different initialValue argument, dirty must still compare against the value the hook was created with on its first render — snapshot it, do not read the live argument.
  • touched is one-way until reset. It starts false, becomes true the first time onBlur runs, and stays true through later changes. Only reset() clears it.
  • onChange reads event.target.value. It receives the DOM change event and pulls the new string from event.target.value, exactly like a native controlled input.
  • reset restores everything. It returns value to the original initial value and sets both dirty and touched back to their starting state.
  • Handlers should have stable identities. onChange, onBlur, and reset should keep the same reference across re-renders so they are safe to pass to memoized children or list in effect dependencies.
Loading editor…