useMethods(methods, initialState) turns a map of named state transitions into ready-to-call action methods over a useReducer. You pass a factory — a function that takes the current state and returns an object of transitions, each returning the next state — and you get back the state plus one bound method per name. The caller writes increment(), never dispatch({ type: 'increment' }). It is useReducer with the action-type strings and the switch deleted.
function useMethods<S, M>(
methods: (state: S) => { [K in keyof M]: (...args: any[]) => S },
initialState: S,
): [S, { [K in keyof M]: (...args: any[]) => void }];
The bound methods object, and every method on it, keeps the same identity for the life of the component.
A counter. The factory takes the state and returns four transitions; you get four methods:
const counterMethods = (state) => ({
increment: () => state + 1,
decrement: () => state - 1,
add: (n) => state + n,
reset: () => 0,
});
const [count, { increment, add, reset }] = useMethods(counterMethods, 0);
add(5); // count is 5 on the next render
increment(); // 6
reset(); // 0
In real components the factory is written inline, so the transitions can close over props:
function Cart({ taxRate }) {
const [cart, { addItem, clear }] = useMethods(
(state) => ({
addItem: (item) => ({
items: [...state.items, item],
tax: state.tax + item.price * taxRate,
}),
clear: () => ({ items: [], tax: 0 }),
}),
{ items: [], tax: 0 },
);
// addItem is the same function on every render, so ItemList never
// re-renders just because Cart did.
return <ItemList items={cart.items} onAdd={addItem} onClear={clear} />;
}
React.memo children and grab them inside mount-only effects. This has to hold even though the factory above is a brand-new function on every render.methods(initialState). A factory that returns different keys for different states will not grow new methods later.add(5) and setField('email', value) both have to reach the transition intact.0, '', false and null all have to land.