Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: useStateMachine #10

Closed
pelotom opened this issue Mar 22, 2019 · 3 comments
Closed

Suggestion: useStateMachine #10

pelotom opened this issue Mar 22, 2019 · 3 comments

Comments

@pelotom
Copy link
Contributor

pelotom commented Mar 22, 2019

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:

  • instead of switching on an action type, one simply writes "methods" which mutate the state (or return a fresh one)
  • instead of getting back a single dispatch function, one gets back an object of callbacks corresponding to the methods

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
Copy link
Contributor Author

pelotom commented Mar 26, 2019

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

@pelotom pelotom closed this as completed Mar 26, 2019
@mweststrate
Copy link
Collaborator

mweststrate commented Mar 26, 2019 via email

@zhaoyao91
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants