wand3r / teasy-redux

8 stars 0 forks source link

[Question] How to create action without payload #1

Open lkostrowski opened 5 years ago

lkostrowski commented 5 years ago

Hi, I'm trying teasy-redux and so far it's really great DX. However I find documentation quite incomplete.

Can you show example how to create action without payload with working types?

wand3r commented 5 years ago

Hi,

By default, action creators create actions without payload. All magic happens in payload function, so if we don't use it, we will get a flat action object.

const actions = createActions({
  increment: (by: number) => ({ by }),
}
actions.increment(1) // { type: 'increment', by: 1 } 

We could also create a small helper function similar to payload to avoid "dynamic" action creators:

const of = <T>() => (x: T) => x

const actions = createActions({
  increment: (by: number) => ({ by }),
  decrement: of<{ by: number }>(),
})

actions.decrement({ by: 1 })  // { type: 'decrement', by: 1 }

It might be worth adding this to the library itself. Now when I think about it, those "payload" creating functions like of and payload may be the biggest strength of this library.

I completely agree that the current documentation is very poor. Hopefully, May will bring more free time :)

wand3r commented 5 years ago

As a side note. Regarding DX, one thing that we notice while using this library is that it is better to define reducers using actionCreator.type field:

const reducer = createReducer<{ counter: number }, typeof actions>(
  {
    [actions.decrement.type]: (state, { by }) => {
      return {
        ...state,
        by: state.counter - by,
      }
    },
    [actions.increment.type]: (state, { by }) => {
      return {
        ...state,
        by: state.counter + by,
      }
    },
  },
  { counter: 0 },
)

It requires a little more typing but thanks to that we get much better refactoring and navigation support from TypeScript. We can easily find all reducers where given action is handled and easily rename it which saves us a lot of time.

lkostrowski commented 5 years ago

Well, for me the main reason of using libs like this is to actually avoid writing extra boilerplate. Not sure if I understand you example. When I need is an action without payload at all Example:

export const actions = createActions({
    fetchStuff: ... // Just create {type: 'fetchStuff'}
});

So there is nothing to map here. I hacked it by fetchStuff: (x: any) => x.

Another thing is that my fetchStuff action is not meant to be handled by reducer. I just handle it in my saga/middleware. However TS throws error if I don't implement it in reducer... What do you think about it? Of course i handle it by mapping [actions.fetchStuff.type]: state => state, but I think this is worth to talk about.

About you example with [actions.decrement.type] - I actually agree but for different reason. It's very easy to accidentally have action name conflicts. I think about some optional 2nd param to createActions for namespace. For example

createActions(..., 'products') which will generate actions like products/FETCH. You can check how this is done for example in Rematch

wand3r commented 5 years ago

Sorry, I misunderstood you about action without payload.

const of = <T>() => (x: T) => x

const actions = createActions({
  increment: () => {},
  decrement: of<void>(),
})

actions.increment() // { type: 'increment' } 
actions.decrement() // { type: 'decrement' } 

Is this what you need? The second option seems to be more readable, empty function () => {} looks strange.

I think about some optional 2nd param to createActions for namespace. For example createActions(..., 'products') which will generate actions like products/FETCH. You can check how this is done for example in Rematch

I think it is impossible for now at least. I talked about it in my presentation here https://gitpitch.com/wand3r/ts-redux-meetup#/5/3. In short, we can't have actions types as string literals and namespaces together. The problem is that we can't combine two string literals together (action type + namespace) and create a new one from that. Or I'm missing something obvious :P I will check how Rematch is handling this.

Another thing is that my fetchStuff action is not meant to be handled by reducer. I just handle it in my saga/middleware. However TS throws error if I don't implement it in reducer... What do you think about it? Of course i handle it by mapping [actions.fetchStuff.type]: state => state, but I think this is worth to talk about.

I haven't thought about that. The idea was to help users don't miss handling an action :P But your use case may also be quite common. I'm not sure how potential solution that combines both approaches could look like. I definitely don't want to lose current behaviour but there also should be an easy way to omit some actions. It requires more thoughts.