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

Pass default value as second parameter for curried producer #111

Closed
klimashkin opened this issue Feb 23, 2018 · 12 comments
Closed

Pass default value as second parameter for curried producer #111

klimashkin opened this issue Feb 23, 2018 · 12 comments

Comments

@klimashkin
Copy link

klimashkin commented Feb 23, 2018

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

@christiangenco
Copy link
Contributor

christiangenco commented Feb 27, 2018

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;
  }
})

@klimashkin
Copy link
Author

klimashkin commented Feb 27, 2018

@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

@christiangenco
Copy link
Contributor

@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;
  }
});

@pkerschbaum
Copy link
Contributor

@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;
  }
});

@pkerschbaum
Copy link
Contributor

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

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;
  }
});

@klimashkin
Copy link
Author

klimashkin commented Mar 2, 2018

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});

@christiangenco
Copy link
Contributor

@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?

@mweststrate
Copy link
Collaborator

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!

@pkerschbaum
Copy link
Contributor

@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.

@mweststrate
Copy link
Collaborator

mweststrate commented Mar 6, 2018 via email

@pkerschbaum
Copy link
Contributor

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

@christiangenco
Copy link
Contributor

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 }));

mweststrate added a commit that referenced this issue Mar 17, 2018
…rry-fn

resolve #111: allow passing initial state to curried producer function
mweststrate added a commit that referenced this issue Mar 22, 2018
…rry-fn

resolve #111: allow passing initial state to curried producer function
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants