Right way to update state in redux reducers
Asked Answered
P

3

29

I'm a newbie in redux and es6 syntax. I make my app with official redux tutorial, and with this example.

There is JS snippet below. My point - to define REQUEST_POST_BODY and RECEIVE_POST_BODY cases in posts reducer. Main difficult - to find and update right object in store.

I try to use code from example:

  return Object.assign({}, state, {
    [action.subreddit]: posts(state[action.subreddit], action)
  })

But it used simple array of posts. It's not needed to find right post by id.

Here my code:

  const initialState = {
    items: [{id:3, title: '1984', isFetching:false}, {id:6, title: 'Mouse', isFetching:false}]
  }

  // Reducer for posts store
  export default function posts(state = initialState, action) {
    switch (action.type) {
    case REQUEST_POST_BODY:
      // here I need to set post.isFetching => true
    case RECEIVE_POST_BODY:
      // here I need to set post.isFetching => false and post.body => action.body
    default:
      return state;
    }
  }

  function requestPostBody(id) {
    return {
      type: REQUEST_POST_BODY,
      id
    };
  }

  function receivePostBody(id, body_from_server) {
    return {
      type: RECEIVE_POST_BODY,
      id,
      body: body_from_server
    };
  }

  dispatch(requestPostBody(3));
  dispatch(receivePostBody(3, {id:3, body: 'blablabla'}));
Psychodynamics answered 16/3, 2016 at 9:32 Comment(1)
If the ids are always going to be unique, you might be better off making items an object instead of an array. Then you can find by id just using items[id].Alleras
M
42

With Arrays

If you'd prefer to stick with arrays, then you can write a reducer that just tackles single post objects.

export default function reducePost(post, action) {
  if(post.id !== action.id) return post;

  switch(action.type) {
  case REQUEST_POST_BODY:
    return Object.assign({}, post, { isFetching: true });
  case RECEIVE_POST_BODY:
    return Object.assign({}, post, { isFetching: false, body: action.body });
  default:
    return post;
}

Your root reducer would become:

export default function posts(state = initialState, action) {
  return state.map(post => reducePost(post, action);
}

We're just running our new reducer over each post in the list, to return an updated array of posts. In this case, the unique id will ensure that only one item will be changed.

With Objects

If each item has a unique string/number id, then you can flip your array around and use an object instead.

const initialState = {
  items: {
    3: {id:3, title: '1984', isFetching:false},
    6: {id:6, title: 'Mouse', isFetching:false}
  };
}

Then you can simplify your reducer.

switch (action.type) {
case REQUEST_POST_BODY:
  let id = action.id;
  return Object.assign({}, state, {
    [id]: Object.assign({}, state[id], { isFetching: true })
  });
case RECEIVE_POST_BODY:
  let id = action.id;
  return Object.assign({}, state, {
    [id]: Object.assign({}, state[id], {
      isFetching: false,
      body: action.body
    })
  });
default:
  return state;
}

If you're happy to experiment with some ES7 syntax too, you can enable the Object spread operator with Babel and rewrite the calls to Object.assign.

switch (action.type) {
case REQUEST_POST_BODY:
  let id = action.id;
  return {
    ...state,
    [id]: {...state[id], isFetching: true }
  };
case RECEIVE_POST_BODY:
  let id = action.id;
  return {
    ...state,
    [id]: {
      ...state[id],
      isFetching: false,
      body: action.body
    }
  };
default:
  return state;
}

If you're not so keen on using the spread syntax, then it's still possible to make Object.assign a bit more palatable.

function $set(...objects) {
  return Object.assign({}, ...objects); 
}
case RECEIVE_POST_BODY:
  let id = action.id;
  return $set(state, {
    [id]: $set(state[id], {
      isFetching: false,
      body: action.body
    })
  });
Myca answered 16/3, 2016 at 9:40 Comment(7)
Simple and nice! Thanks! So, there is a bit different store data structure, and I have to change my component render method. Code {this.props.data.map((item, id) => <PostItem /> raise error: TypeError: this.props.data.map is not a function at render What is right way to iterate new object (instead of an array) here?Psychodynamics
It should be something like this: for (let [id, item] of Object.entries(this.props.data)) {Psychodynamics
That works, or you can also iterate on the keys. Object.keys(this.props.data).map(k => <PostItem post={this.props.data[k]} />.Myca
If you're using Lodash or Underscore, functions like map and filter iterate over an object's own properties, as well as arrays, which can be extremely useful in situations like that.Fatimafatimah
There should be a comma(",") after "...state" in the case REQUEST_POST_BODY.Adjournment
Hey! I have doubt over here what does ...state mean here. I am not able to understand this .Sondra
...state means copy all the properties in state into whatever object we're currently declaring.Myca
M
1

If I understand correctly, you are having trouble getting the specific post you want.

First of all, Having your reducer also update the array and the object in it, makes it hard to read and maintain. I suggest you watch this short video explaining about reducer composition with arrays. You can simplify your code by using the technique described there.

In your case, you would a posts reducer and a post reducer, while posts reducer calls the post reducer.

As for finding the right object to work on, Dan Prince's suggestion makes it easier. Having an object map instead of an array would make it easier for you. Relevant code snippet from Dan's answer:

const initialState = {
  items: {
    3: {id:3, title: '1984', isFetching:false},
    6: {id:6, title: 'Mouse', isFetching:false}
  ];
}
Michaels answered 16/3, 2016 at 9:47 Comment(0)
O
0

I pretty much implemented Object reducers by using Object.assign, which works, but as our project has grown and we have added a bunch of dependent components it has become very inefficient and renders are very slow.

If I'd know about immer I would have used that from the start.

Essentially you use immer as follows, where the example has a layers object that looks like this:

const initialState = {
  layers: {
   'layer1': { selected: false },
   'layer2': { selected: true }
  }
}

Reducers.js (extract)

import produce from 'immer'
import has from 'lodash/has'
export const layers = (state = null, action) => {
  switch (action.type) {
    case ADD_LAYER:
      // Using `immer.produce` only for consistency 
      // clearly the whole object changes in this case.
      return produce(state, layers => {
        // Take a copy of the prebuilt layer
        var layer = Object.assign({}, action.layer)
        // Add some defaults
        if (!has(layer, 'selected')) {
          layer.selected = false
        }
        layers[action.id] = layer
      })
    case SELECT_LAYER:
      return produce(state, layers => {
        layers[action.id].selected = true
      })
    case UNSELECT_LAYER:
      return produce(state, layers => {
        layers[action.id].selected = false
      })
    default:
      return state
  }
}

Actions.js (extract)

export const addLayer = id => ({
  type: ADD_LAYER,
  id
})

export const selectLayer = id => ({
  type: SELECT_LAYER,
  id
})

export const unSelectLayer = id => ({
  type: UNSELECT_LAYER,
  id
})

References:

https://github.com/immerjs/immer

https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns

Osteoblast answered 6/8, 2019 at 5:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.