dphilipson / typescript-fsa-reducers

Fluent syntax for defining typesafe reducers on top of typescript-fsa.
MIT License
220 stars 16 forks source link

Support for immer #30

Open karlvr opened 5 years ago

karlvr commented 5 years ago

I'm introducing immer https://github.com/mweststrate/immer to our project, in which we use your fantastic module.

We currently use this pattern:

reducer.case(actions.login.failed, (state, payload) => produce(state, draft => {
    draft.error = payload
    draft.loggingIn = false
}))

And I would love to make this simpler. Something like...

reducer.immerCase(actions.login.failed, (draft, payload) => {
    draft.error = payload
    draft.loggingIn = false
})

I'm not sure whether this idea fits in your module, as it's now specific to immer, and this library is about TypeScript and Redux...

If I'm right about the above, then I'm wondering out loud how to make this work... I think I'll start with a fork and see :-)

dphilipson commented 5 years ago

Hi Karlvr!

First of all, thank you for telling me about Immer, which I had never seen and looks like a very useful library. I will definitely try it out in my own projects.

I would very much like to find a way to support your use case without adding a dependency on a specific library like Immer. I want to avoid specifically adding methods like .immerCase because it would force this library to take a dependency on Immer even for users who don't use this method, and I think it is a good thing for small libraries like this one to take on few or no dependencies whenever possible.

One thing you could do instead is use .withHandling and write yourself a reusable Immer helper. It would allow you to write code like this:

reducer.withHandling(immerCase(actions.login.failed, (draft, payload) => {
    draft.error = payload
    draft.loggingIn = false
})

where immerCase is defined as

function immerCase<S, P>(
    actionCreator: ActionCreator<P>,
    handler: (draft: S, payload: P) => void,
): (reducer: ReducerBuilder<S>) => ReducerBuilder<S> {
    return reducer =>
        reducer.case(actionCreator, (state, payload) =>
            produce(state, draft => handler(draft, payload)),
        );
}

Does that work for what you're trying to do?

An alternative I've considered before is to refactor the code to expose a ReducerBuilder class which could be extended to add new methods, but designing a class to be extensible adds a fair bit of complexity to the code and to testing, so I've been wary about doing this.

karlvr commented 5 years ago

@dphilipson Thank you so much for your lightning fast response. I absolutely agree about dependencies etc.

My goal is to make the code simpler; I'm not sure whether the withHandling example is easier or harder than what I have now... although two =>s in a line is always a head scratcher!

Maybe if it was possible to register a handler wrapper we could make it work... but I'm not sure whether that would be able to integrate with the type system nicely.

I think in the meantime I'll make do with what I've got and see whether inspiration strikes!

ivan-aksamentov commented 5 years ago

This is tremendous, folks! Last time I was so happy is probably only when I discovered the Redux itself. 🎉

The most important thing for me is that this way of writing reducers is a decent workaround for the years-old problem of return type inference (see #20 in this repo and numerous issues in typescript and redux repos). Because draft is based on mutating properties of an object, this problem just disappears.

Here is an updated version (changes to Draft<StateType> in the parameter types) to mitigate the type error:

import produce, { Draft } from 'immer'
import { ActionCreator } from 'typescript-fsa'
import { ReducerBuilder } from 'typescript-fsa-reducers'

export default function immerCase<StateType, PayloadType>(
  actionCreator: ActionCreator<PayloadType>,
  handler: (draft: Draft<StateType>, payload: PayloadType) => void,
): (reducer: ReducerBuilder<StateType>) => ReducerBuilder<StateType> {
  return reducer =>
    reducer.case(actionCreator, (state, payload) =>
      produce(state, draft => handler(draft, payload)),
    )
}