Immer: Pass default value as second parameter for curried producer

Created on 23 Feb 2018  Â·  12Comments  Â·  Source: immerjs/immer

In redux all reducers should have default value, since redux tree is bootstrapped with @@redix/INIT action.

To workaround that, using curried producer as reducer, we have to write condition to check for undefined value of the draft. And we can't use argument's default value (draft = {count: 0}, action), because in this case we don't know if we should return it or able to mutate proxy.

const counter = produce((draft, action) => {  
  if (draft === undefined) {
    return {count: 0};
  }

  switch (action.type) {
    case 'INCREMENT':
      return draft.count += 1;
    case 'DECREMENT':
      return draft.count -= 1;
  }
});

But writing such check in all producers seems a little overwhelming. What if we could specify default value as a second parameter of producer, as we do it in array.reduce method?

const counter = produce((draft, action) => {  
  switch (action.type) {
    case 'INCREMENT':
      return draft.count += 1;
    case 'DECREMENT':
      return draft.count -= 1;
  }
}, {count: 0});

Following up #105

good first issue help wanted

All 12 comments

I think the solution here should be to use ES6 default function parameters:

const initialState = {count: 0};
const counter = produce((draft = initialState, action) => {  
  switch (action.type) {
    case 'INCREMENT':
      return draft.count += 1;
    case 'DECREMENT':
      return draft.count -= 1;
  }
});

That super doesn't work though, and I'm having trouble figuring out why.

Edit

Oh I think I understand. Because produce has some javascript stateful voodoo to figure out what changes you made to draft, providing a default value for draft in a curried function is breaking the voodoo.

This bug could possibly be fixed if produce allowed you to return draft at the end?

The following code works currently, though it's a little more verbose:

const initialState = {count: 0};
const counter = (state = initialState, action) => produce(state, draft => {
  switch (action.type) {
    case 'INCREMENT':
      return draft.count += 1;
    case 'DECREMENT':
      return draft.count -= 1;
  }
})

@christiangenco Right, it works in case of creating producer on each reducer call (which seems like overhead), but in case of curried function you can't do it in short way, so this issue proposes solution for that.

This bug could possibly be fixed if produce allowed you to return draft at the end?

If draft could be returned from producer filled with default value (draft = {count: 0}, action) issue would be solved indeed.
But for me it doesn't seem best approach, because in that case I could have draft value that is actually not a Proxy (when default is used), but in other cases I would deal with Proxy. I think having the same type always is less confusing and less error prone, that's why probably setting default value as second parameter would be better, because it will be passed to producer as Proxy as well

@klimashkin Oh dude you can totally just return the draft:

It is not needed to return anything from a producer, as Immer will return the (finalized) version of the draft anyway. However, it allowed to just return draft.

I think then the best pattern becomes:

const initialState = {count: 0};
const counter = produce((draft = initialState, action) => {  
  switch (action.type) {
    case 'INCREMENT':
      return draft.count += 1;
    case 'DECREMENT':
      return draft.count -= 1;
    default: 
      return draft;
  }
});

@christiangenco
I also think that your proposed solution is currently the best pattern, but I would like to add a subtle modification:

Because of #103 the following aspect was added to immer:

It is also allowed to return abitrarily other data from the producer function. But only if you didn't modify the draft.

The return statement

return draft.count += 1;

...does not only modify the draft, but also return the result of the operation, and therefore would throw an error.

So the best current pattern would be:

const initialState = {count: 0};
const counter = produce((draft = initialState, action) => {  
  switch (action.type) {
    case 'INCREMENT':
      draft.count += 1;
    case 'DECREMENT':
      draft.count -= 1;
    default: 
      return draft;
  }
});

Ah damn it, now there is a problem with fall-through :sweat_smile:.

So maybe something like this:

const initialState = {count: 0};
const counter = produce((draft = initialState, action) => {  
  switch (action.type) {
    case 'INCREMENT':
      draft.count += 1;
      return;
    case 'DECREMENT':
      draft.count -= 1;
      return;
    default: 
      return draft;
  }
});

Right, I tried to point out that you need to know type of the draft if it's a proxy or pure object to decide what to do - return it or just mutate.

If pass default value as a second parameter to producer, we enforce proxy type and stop worrying about different type.
So we can write just

const counter = produce((draft, action) => {  
  if (action.type === 'INCREMENT') {
     draft.count += 1;
  } else if (action.type === 'DECREMENT') {
     draft.count -= 1;
  }
}, {count: 0});

@klimashkin's code is way more compact. The extra returns from @pkerschbaum's example - while it seems are the best practices currently - feel very boilerplatey.

What's the argument against passing a default value as a second parameter to producer?

I think @klimashkin proposed pattern is a good idea. Anybody willing to create a PR?

Preferably update the TS / FLow types as well :)

Also, make sure in the readme that the difference between produce(fn, initialState) and produce(initialState, fn) is very clear!

@mweststrate
I would really like to implement that enhancement and create an PR!

Only thing I don't know yet is how we can differentiate the two produce function calls, because if a function is used for the state argument, produce(baseState, fn) and produce(fn, initialState) cannot be distinguished, neither by count of arguments nor by the types of the passed arguments.

Or do I overlook something?

Alternative would be to export another function which is explicitly designed for this enhancement.

Immer only works with a base state that is either an array or an object, so
throwing on a base state that is a function (or a primitive) is perfectly
fine imho

Op di 6 mrt. 2018 om 19:57 schreef P Kerschbaum notifications@github.com:

@mweststrate https://github.com/mweststrate
I would really like to implement that enhancement and create an PR!

Only thing I don't know yet is how we can differentiate the two produce
function calls, because if a function is used for the state argument, produce(baseState,
fn) and produce(fn, initialState) cannot be distinguished, neither by
count of arguments nor by the types of the passed arguments.

Or do I overlook something?

Alternative would be to export another function which is explicitly
designed for this enhancement.

—
You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
https://github.com/mweststrate/immer/issues/111#issuecomment-370888519,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABvGhMH_pp6fhSkm_K5gQd4pf1BWJexnks5tbtuQgaJpZM4SRaQA
.

@mweststrate
I see, I created the PR #121 to implement this feature!
I've also added the typings and adapted the readme.

I took another approach to solving this for myself: a helper function for making redux reducers and actions.

It uses immer under the hood, and lets you do things like this:

const initialState = {
  photos: { large: { url: "" } }
};
const { reducer, actions } = createReducerActions(
  {
    setLargePhotoUrl: (state, { payload: { url } }) => {
      // mutate the state!
      state.photos.large.url = url;
      // don't return anything
    }
  },
  initialState,
  { mutable: true }
);

actions. setLargePhotoUrl is an action creator, and reducer is an immutable reducer, so you can do:

const newState = reducer(initialState, actions.setLargePhotoUrl({ url }));
Was this page helpful?
0 / 5 - 0 ratings