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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal implementation of handleActions #23

Open
gillchristian opened this issue Sep 25, 2018 · 7 comments
Open

Proposal implementation of handleActions #23

gillchristian opened this issue Sep 25, 2018 · 7 comments

Comments

@gillchristian
Copy link

gillchristian commented Sep 25, 2018

Really nice solution for typing Redux! It's exactly what I've been looking for for a while! Thanks! 馃槃

Feature request proposal

Use case(s)

One thing I really like from redux-actions is handleActions. For me it's much cleaner than switch statements.

The problem is it doesn't play well with rex-tils's approach, which is much better IMHO.

I created this version of handleActions:

const handleActions = <
  State,
  Types extends string,
  Actions extends ActionsUnion<{ [T in Types]: AnyFunction }>
>(
  handler: {
    [T in Types]: (state: State, action: ActionsOfType<Actions, T>) => State
  },
  initialState: State,
) => (state = initialState, action: Actions): State =>
  handler[action.type] ? handler[action.type](state, action) : state;

Which can be used like this:

interface State {
  foo: boolean,
  bar: number,
}

enum Types {
  FOO = 'FOO',
  BAR = 'BAR',
}

const Actions = {
  foo: () => createAction(Types.FOO),
  bar: (bar: number) => createAction(Types.FOO, bar),
}

const reducer = handleActions<State, Types, Actions>(
  {
    [Types.FOO]: (state) => ({ ...state, foo: !state.foo }),
    [Types.BAR]: (state, { payload }) => ({ ...state, bar: payload, }),
  },
  { foo: true, bar: 0 },
)

Enums are not necessary, it works with constants but requires a few lines more:

...

const FOO = 'FOO'
const BAR = 'BAR'

type Types =
  | typeof FOO
  | typeof BAR

...

Props

  • Works as the switch version, type of action is inferred for each case, but much less boilerplate
  • The handler object expects all the types to be "handled" (i.e. you get a type error if one of the action types is not covered)

Cons

  • handleActions expects 3 type parameters. I tried to find a way to make it work only with State and Actions but I'm not sure is possible. Maybe I'm missing something.

If you think this is worth adding to rex-tils I can create a Pull Request.

Edit handle-actions

@gillchristian gillchristian changed the title Implementation of handleActions Proposal implementation of handleActions Sep 25, 2018
@hegelstad
Copy link

I've been trying to get this working, but with a slightly different pattern: I am at a loss, do any of you have any hints? It looks very similar to what you have going on.

const SET_TOKENS = 'SET_TOKENS';
const Actions = {
  setTokens: (tokens: ApiToken[]) => createAction(SET_TOKENS, tokens),
};
export type Actions = ActionsUnion<typeof Actions>;

I am struggling especially with the types for this part..

const handleSetTokens = (state, payload) => ({ ...state, payload });

export const reducers = new Map([[SET_TOKENS, handleSetTokens]]);
import reducers from ...
const startReducers = (action$, defaultState = []) =>
  action$.pipe(
     filter(action => reducers.has(action.type)),
     scan(
       (state, { type, ...payload }) => reducers.get(type)(state, payload),
       defaultState
     ),
     startWith(defaultState)
  );

startRoutines(action$);

@raybooysen
Copy link

raybooysen commented Jun 10, 2019

@gillchristian This doesn't work for me in Typescript because this line:
handler[action.type] ? handler[action.type](state, action) : state;

action.type is resolved to be of type any.

@gillchristian
Copy link
Author

gillchristian commented Jun 12, 2019

@raybooysen even with that any there the inference works when you use handleActions.

Check it out here: http://bit.ly/2WAWFZw

Try changing the handlers you pass to handleActions or adding more cases to the Types (line 70), also check how handleActions infers actions of each handler having a payload or not.

It'd be great if we could fix this error but I don't think it changes the result.

Also, I published what's in the playground as a separate package https://github.com/housinganywhere/safe-redux

@raybooysen
Copy link

Thanks will give it a try. Checking your code's TS, there is still the error with the handler object. How are you disabling this error?

@raybooysen
Copy link

raybooysen commented Jun 13, 2019

@gillchristian Another interesting thing. In a vague roundabout way, I wrote something similar to yours (but I use immer to mutate a draft instead of recreating the state each time). Where I got unstuck was trying to do something like this (using your pattern in the sample):

// How can we type barHandler appropriately?
const barHandler = (state, {payload}) => ({...state, bar: payload});
const reducer = handleActions<State, Types, Actions>(
  {
    [Types.FOO]: (state) => ({ ...state, foo: !state.foo }),
    [Types.BAR]: barHandler
  },
  { foo: true, bar: 0 },
)

In the sample above, it remove a large amount of code inside the handleActions, it seems a nice pattern to create the actual handlers as separate functions, in this case creating a function called barHandler.

However, because barHandler no longer knows the key it was used for, in this case Types.BAR, we lose the ability to have type-safety for the handler itself. In my example, if we refactored the payload, or even worse, changed the type of the Action, there would be no compilation error with barHandler.

We could do something like this:

const barHandler = (state, payload) => ({...state, bar: payload});
const reducer = handleActions<State, Types, Actions>(
  {
    [Types.FOO]: (state) => ({ ...state, foo: !state.foo }),
    [Types.BAR]: (state, { payload }) => barHandler(state, payload),
  },
  { foo: true, bar: 0 },
)

which would give us the type-safety, but introduces another level of function calls. I'm in two minds whether this actually matters so much, other than there just being MORE code. I think this may end up being the best of the options.

BTW, none of this is an indictment on your code, I've just been grappling with the same sort of problem trying to ensure that all our reducers pick up the changes to actions.

Fun stuff. :)

@gillchristian
Copy link
Author

gillchristian commented Jun 13, 2019

Thanks will give it a try. Checking your code's TS, there is still the error with the handler object. How are you disabling this error?

I found out about the error when you pointed it out, it's not shown in my editor but it is on the playground. Probably because of a different TS config.

Screenshot from 2019-06-13 09-45-35

Screenshot from 2019-06-13 09-46-34

BTW, none of this is an indictment on your code, I've just been grappling with the same sort of problem trying to ensure that all our reducers pick up the changes to actions.

Of course 馃帀


I've been using the handlers inline all the time, so I never had the need, but it really makes sense to extract them. Found a way to do it with ActionsOfType. It's a bit verbose on the types, so I personally prefer the handlers inline. But hey, it's possible 馃帀

type Handler<State, ActionType extends string, Actions> = 
  (s: State, a: ActionsOfType<Actions, ActionType>) => State

const barHandler: Handler<State, 'bar', Actions> =
  (s, { payload }) => ({n: s.n + payload})

handleActions<State, Types, Actions>(
    {
        foo: (s) => ({n: s.n + 1}),
        bar: barHandler,
    },
    { n: 0},
)

Updated the playground: http://bit.ly/2F1V0Gz

And I think I will add it to the library when I get some time.

EDIT: added Handler to @housinganywhere/safe-redux housinganywhere/safe-redux#4

@raybooysen
Copy link

You absolute hero! I've been battling with my implementation of Handler because I didn't know how to express the key in the actions type properly

type Handler<State, ActionType extends string, Actions> = 
  (s: State, a: ActionsOfType<Actions, ActionType>) => State

const barHandler: Handler<State, 'bar', Actions> =
  (s, { payload }) => ({n: s.n + payload})

The use of bar here is what I was missing which made my handlers not pick up the change in action shapes. I really appreciate this, ty

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