useObjectLoading saved progress…

useObject

Build a custom React hook that manages a single object in state. useObject holds an object and hands back the current value plus three helpers: merge to apply a partial update, set to replace the whole object, and reset to snap back to the value the hook started with. The interesting part isn't the storage — it's making each change produce a new object so React notices, while merge keeps the keys you didn't touch.

Signature

function useObject<T extends object>(initialValue?: T): {
  value: T;
  merge: (patch: Partial<T>) => void;
  set: (next: T) => void;
  reset: () => void;
};

initialValue defaults to {} when omitted. merge shallow-merges patch into value, keeping every key not present in patch. set replaces the object wholesale. reset restores the original initialValue, even if the hook is later re-rendered with a different argument.

Examples

function ProfileForm() {
  const { value, merge, set, reset } = useObject({ name: 'Ada', role: 'dev' });

  return (
    <div>
      <span>{value.name}{value.role}</span>
      <button onClick={() => merge({ role: 'lead' })}>promote</button>
      <button onClick={() => set({ name: 'Grace', role: 'admiral' })}>swap</button>
      <button onClick={reset}>reset</button>
    </div>
  );
}
// merge({ role: 'lead' }) -> { name: 'Ada', role: 'lead' } (name survives)
// reset() -> { name: 'Ada', role: 'dev' }
// merge keeps untouched keys; a "merge" that replaces would drop `a`.
const { value, merge } = useObject({ a: 1, b: 2 });
merge({ b: 3 });
// value === { a: 1, b: 3 }, NOT { b: 3 }

Notes

  • merge is shallow. It copies the top-level keys of patch over the current object. Nested objects are replaced, not deep-merged — that is out of scope here.
  • Every change must be a new object. React compares the object by reference to decide whether to re-render. Editing the existing object in place reuses the same reference, and React skips the update.
  • Helpers must survive batched updates. React groups several setter calls in one event into one re-render, so merge has to be expressed in a way that stacks correctly rather than colliding on a stale snapshot.
  • reset targets the first initial value. If the component re-renders and passes a new initialValue, reset still restores the value the hook was created with. Snapshot it so it can't drift.
  • You don't need to memoize the helpers for this version — returning fresh functions each render is acceptable. Stabilizing their identity is a separate exercise.
Loading editor…