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

Question: Mutation of an inner object in a state in a reducer #1623

Closed
elado opened this issue Apr 15, 2016 · 3 comments
Closed

Question: Mutation of an inner object in a state in a reducer #1623

elado opened this issue Apr 15, 2016 · 3 comments

Comments

@elado
Copy link

elado commented Apr 15, 2016

Is this a valid redux reducer?

function reducer(state={ byId: {}, ids: [] }, action) {
  switch (action.type) {
    case 'ADD':
      state = { ...state }
      state.byId[action.id] = action.value
      state.ids.push(action.id)
      return state

    default:
      return state
  }
}

The entire state is shallow cloned first, so a new object will be returned, but the inner objects stay the same and mutated.

Or do I have to do something like:

function reducer(state={ byId: {}, ids: [] }, action) {
  switch (action.type) {
    case 'ADD':
      return {
        ...state,
        byId: {
          ...state.byId,
          [action.id]: action.value
        },
        ids: [ ...state.ids, action.id ]
      }

    default:
      return state
  }
}

The side effects I see with the first approach are in DevTools LogMonitor:

  • expanding a sub node (e.g. byId) in a previous state will show the same value as the latest one even when it was something else previously
  • time travel isn't working

Is it just a bug in the LogMonitor or the second approach is the right one? If I add state = _.cloneDeep(state) before the mutations it actually works fine, proving that the LogMonitor reuses the same objects, hence the same values.

The rest of the app works fine and @connected views update correctly though.

@markerikson
Copy link
Contributor

Nope, that first example is absolutely mutating the state. The initial shallow copy creates a new object, but the new object points to the same byId and ids references as the original. So, when you do byId[action.id] = action.value and ids.push(action.id), you're mutating the original items.

The latter example is what you need to do. Obviously, that can get somewhat verbose if you're nesting things. There's several utility libraries that try to abstract that for you, and I have some of them listed over at in the Immutable Data page of my Redux libraries list.

The Redux FAQ also has a question that covers correctly updating state immutably: http://redux.js.org/docs/FAQ.html#react-not-rerendering .

@gaearon
Copy link
Contributor

gaearon commented Apr 15, 2016

The latter example is what you need to do. Obviously, that can get somewhat verbose if you're nesting things. There's several utility libraries that try to abstract that for you, and I have some of them listed over at in the Immutable Data page of my Redux libraries list.

Also reducer composition helps with this.

function byId(state = {}, action) {
  switch (action.type) {
    case 'ADD':
      return {
        ...state,
        [action.id]: action.value
      }
    default:
      return state
  }
}

function ids(state = [], action) {
  switch (action.type) {
    case 'ADD':
      return [ ...state, action.id ]
    default:
      return state
  }
}

const reducer = combineReducers({
  byId,
  ids
})

@elado
Copy link
Author

elado commented Apr 19, 2016

@gaearon @markerikson Thanks. I ended up writing a high-order-reducer that manages dynamic indexed lists (by ID)

Code: https://gist.github.com/elado/95484b754f31fcd6846c7e75de4aafe4

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