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.
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.
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 }
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.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.