immerjs / use-immer

Use immer to drive state with a React hooks
MIT License
4.04k stars 92 forks source link

Suggestion: useStateMachine #10

Closed pelotom closed 5 years ago

pelotom commented 5 years ago

Hi, I wanted to share a simple hook I've built on top of use-immer that I've found very useful, and ask if there's any interest in including it in the library. I call it useStateMachine, and (adapting the example from your README), one uses it like this:

interface State {
  count: number;
}

const initialState: State = { count: 0 };

const transitions = (state: State) => ({
  reset() {
    return initialState;
  },
  increment() {
    state.count++;
  },
  decrement() {
    state.count--;
  },
});

function Counter() {

  const {
   count,
   reset,
   increment,
   decrement
  } = useStateMachine(initialState, transitions);

  return (
    <>
      Count: {count}
      <button onClick={reset}>Reset</button>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </>
  );
}

The API bears an obvious resemblance to that of useImmerReducer/useReducer, but with some important differences:

I find this pattern much nicer to use than standard reducers for a few reasons:

  1. writing methods is nicer than putting everything into a big switch statement and having to break/return from each case
  2. the returned callbacks can be passed directly as props to children (rather than wrapping with arg => dispatch({ type: 'foo', arg }) -- blech!), and they are properly memoized!
  3. in TypeScript, one doesn't need to make an Action union type and keep it up to date; everything is inferred from the method types

The hook is very simple; here's the code for it:

import { useCallback, useMemo } from 'react';
import { useImmerReducer } from 'use-immer';

export type Transitions<
  S extends object = any,
  R extends Record<string, (...args: any[]) => S | void> = any
> = (s: S) => R;

export type StateFor<T extends Transitions> = T extends Transitions<infer S> ? S : never;

export type ActionFor<T extends Transitions> = T extends Transitions<any, infer R>
  ? { [T in keyof R]: { type: T; payload: Parameters<R[T]> } }[keyof R]
  : never;

export type CallbacksFor<T extends Transitions> = {
  [K in ActionFor<T>['type']]: (...payload: ActionByType<ActionFor<T>, K>['payload']) => void
};

export type ActionByType<A, K> = A extends { type: infer K2 } ? (K extends K2 ? A : never) : never;

export default function useStateMachine<T extends Transitions>(
  initialState: StateFor<T>,
  transitions: T,
): StateFor<T> & CallbacksFor<T> {
  const reducer = useCallback(
    (state: StateFor<T>, action: ActionFor<T>) =>
      transitions(state)[action.type](...action.payload),
    [transitions],
  );
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  const actionTypes: ActionFor<T>['type'][] = Object.keys(transitions(initialState));
  const callbacks = useMemo(
    () =>
      actionTypes.reduce(
        (accum, type) => {
          accum[type] = (...payload) => dispatch({ type, payload } as ActionFor<T>);
          return accum;
        },
        {} as CallbacksFor<T>,
      ),
    actionTypes,
  );
  return { ...state, ...callbacks };
}

If you think this would make a good addition to the library I'd be happy to open a PR.

pelotom commented 5 years ago

I went ahead and made a separate library for this: https://github.com/pelotom/use-state-methods

mweststrate commented 5 years ago

Cool! If you ping me in a tweet I'll RT

On Tue, Mar 26, 2019 at 6:22 AM Tom Crockett notifications@github.com wrote:

I went ahead and made a separate library for this: https://github.com/pelotom/use-state-methods

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/mweststrate/use-immer/issues/10#issuecomment-476479764, or mute the thread https://github.com/notifications/unsubscribe-auth/ABvGhKw_Iwb6ArPa3HWMp9grP8wn2if6ks5vaa6KgaJpZM4cCoo2 .

zhaoyao91 commented 4 years ago

@pelotom nice libray, would it be integrated into use-immer?