usePatchState is a hook for managing object state with partial updates (patches), similar in spirit to class components’ this.setState. It accepts a partial object or an updater function and performs a shallow merge with the current state.
function usePatchState<ObjectType extends PlainObject>(
initialState: ObjectType | (() => ObjectType),
): readonly [state: ObjectType, patch: UsePatchStateFunction<ObjectType>];-
Parameters
initialState— the complete initial state object or a lazy initializer function.
-
Returns
- Tuple
[state, patch]:state: ObjectType— current state;patch(partial | updater): void— applies a partial update (see below).
- Tuple
import { usePatchState } from '@webeach/react-hooks/usePatchState';
type Form = {
name: string;
age: number;
};
export function ProfileForm() {
const [form, patchForm] = usePatchState<Form>({ name: '', age: 0 });
return (
<form>
<input
value={form.name}
onChange={(event) => patchForm({ name: event.target.value })}
/>
<input
type="number"
value={form.age}
onChange={(event) => patchForm({ age: Number(event.target.value) })}
/>
<pre>{JSON.stringify(form, null, 2)}</pre>
</form>
);
}import { usePatchState } from '@webeach/react-hooks/usePatchState';
type CounterState = { count: number; step: number };
export function Counter() {
const [state, patch] = usePatchState<CounterState>({ count: 0, step: 1 });
const increment = () => patch((prev) => ({ count: prev.count + prev.step }));
const decrement = () => patch((prev) => ({ count: prev.count - prev.step }));
const setStep = (step: number) => patch({ step });
return (
<div>
<output>{state.count}</output>
<button onClick={increment}>+{state.step}</button>
<button onClick={decrement}>-{state.step}</button>
<button onClick={() => setStep(5)}>step = 5</button>
</div>
);
}import { usePatchState } from '@webeach/react-hooks/usePatchState';
type Settings = {
theme: {
mode: 'light' | 'dark';
accent: string;
};
tags: string[];
};
export function SettingsPanel() {
const [settings, patch] = usePatchState<Settings>(() => ({
theme: {
mode: 'light',
accent: '#09f',
},
tags: [],
}));
// Important: the patch is shallow. For nested fields create a new nested object
// (or use `usePatchDeepState` for deep merge semantics).
const setDark = () => patch((prev) => ({ theme: { ...prev.theme, mode: 'dark' } }));
const addTag = (t: string) => patch((prev) => ({ tags: [...prev.tags, t] }));
return (
<div>
<button onClick={setDark}>Dark mode</button>
<button onClick={() => addTag('react')}>+react</button>
<pre>{JSON.stringify(settings, null, 2)}</pre>
</div>
);
}-
Shallow merge
- The patch is combined with the state using a spread operation: new key–value pairs overwrite top‑level fields. Nested objects/arrays are not deeply merged — recreate them in the updater when needed.
-
Two patch forms
- You can pass a partial object (
{ field: value }) or a functional updater(prev) => ({ field: next }). The function receives the current state and must return a partial object.
- You can pass a partial object (
-
patchstability- The
patchfunction is memoized and stable between renders; safe to use in effect/callback dependency arrays.
- The
-
Lazy initialization
- If
initialStateis a function, it is called once on the first render.
- If
-
Immutability
- Do not mutate
previnside the updater. Return a new partial object and, when necessary, new nested structures.
- Do not mutate
-
Arrays
- Because merging is shallow, array fields are replaced entirely. For insertion/removal create a new array:
({ list: [...prev.list, x] }).
- Because merging is shallow, array fields are replaced entirely. For insertion/removal create a new array:
- Forms, filters, settings — when it’s convenient to keep multiple fields in one object and patch them as the user types.
- State where individual fields change often without rebuilding the entire object.
- View‑model style state where you need quick patches to independent parts.
- If the state isn’t an object (numbers, strings) — plain
useStateis simpler. - If you need complex transactional updates/invariants — consider
useReducer. - If you require deep merging across a tree — use
usePatchDeepStateinstead.
-
Expecting a deep merge
- The patch is shallow. For nested updates return a new nested object/array.
-
Mutating the previous state
- Don’t change
previn place inside the updater — always create new structures.
- Don’t change
-
Passing an event object as a patch
- Avoid
onChange={patch}— the DOM event object would be used as a patch. Wrap the handler:onChange={(event) => patch({ field: event.target.value })}.
- Avoid
-
Returning a non‑object from the updater
- The updater must return a partial object. Returning
undefinedor a primitive breaks the merge logic.
- The updater must return a partial object. Returning
-
Accidentally overwriting an entire object field
patch({ theme: { mode: 'dark' } })replaces the wholetheme. If you need to change just one field, combine with the previous value:patch((prev) => ({ theme: { ...prev.theme, mode: 'dark' } }))— or useusePatchDeepState.
Exported types
UsePatchStateFunction<ObjectType extends PlainObject = PlainObject>- Function for partially updating an object state.
- Accepts:
- Partial object:
Partial<ObjectType>. - Or functional updater:
(currentState: ObjectType) => Partial<ObjectType>.
- Partial object:
- The update is applied via shallow merge.
- Returns
void.