useMethodsLoading saved progress…

useMethods

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.

Signature

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.

Examples

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} />;
}

Notes

  • The bound methods never change identity. The object, and every function on it, is the same on render 50 as on render 1 — callers hand them to 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.
  • A method still computes from the newest state. A method captured on render 1 and called an hour later transitions from the state as it is then, not the state it was created under.
  • The method names are read once, from methods(initialState). A factory that returns different keys for different states will not grow new methods later.
  • Every argument is forwarded. add(5) and setField('email', value) both have to reach the transition intact.
  • Transitions return the next state, they never mutate it. Same contract as any reducer: React compares references to decide what to re-render.
  • Falsy next states are states. 0, '', false and null all have to land.
Loading editor…