Ilja
Ilja

Reputation: 46479

How to update single value inside specific array item in redux

I have an issue where re-rendering of state causes ui issues and was suggested to only update specific value inside my reducer to reduce amount of re-rendering on a page.

this is example of my state

{
 name: "some name",
 subtitle: "some subtitle",
 contents: [
   {title: "some title", text: "some text"},
   {title: "some other title", text: "some other text"}
 ]
}

and I am currently updating it like this

case 'SOME_ACTION':
   return { ...state, contents: action.payload }

where action.payload is a whole array containing new values. But now I actually just need to update text of second item in contents array, and something like this doesn't work

case 'SOME_ACTION':
   return { ...state, contents[1].text: action.payload }

where action.payload is now a text I need for update.

Upvotes: 160

Views: 188817

Answers (13)

Buzzy
Buzzy

Reputation: 1924

If you have an object type in you state array you can replace it by map with a condition like

reducers: {
    updateProject(state, { payload }) {
      state.projects = state.projects.map(origin =>
        origin.id === payload.id
          ? payload
          : origin
      );
    }
  },

Upvotes: 0

vdegenne
vdegenne

Reputation: 13270

Note: in newer versions (@reduxjs/toolkit), Redux automatically detects changes in object, and you don't need to return a complete state :

/* reducer */
const slice = createSlice({
  name: 'yourweirdobject',
  initialState: { ... },
  reducers: {
    updateText(state, action) {
      // updating one property will cause Redux to update views
      // only depending on that property.
      state.contents[action.payload.id].text = action.payload.text
    },
    ...
  }
})

/* store */
export const store = configureStore({
  reducer: {
    yourweirdobject: slice.reducer
  }
})

This is how you should do now.

Upvotes: 2

zeusstl
zeusstl

Reputation: 1865

Immer.js (an amazing react/rn/redux friendly package) solves this very efficiently. A redux store is made up of immutable data - immer allows you to update the stored data cleanly coding as though the data were not immutable.

Here is the example from their documentation for redux: (Notice the produce() wrapped around the method. That's really the only change in your reducer setup.)

import produce from "immer"

// Reducer with initial state
const INITIAL_STATE = [
    /* bunch of todos */
]

const todosReducer = produce((draft, action) => {
    switch (action.type) {
        case "toggle":
            const todo = draft.find(todo => todo.id === action.id)
            todo.done = !todo.done
            break
        case "add":
            draft.push({
                id: action.id,
                title: "A new todo",
                done: false
            })
            break
        default:
            break
    }
})

(Someone else mentioned immer as a side effect of redux-toolkit, but you should use immer directly in your reducer.)

Immer installation: https://immerjs.github.io/immer/installation

Upvotes: 2

Slava  Vasylenko
Slava Vasylenko

Reputation: 1014

Pay attention to the data structure: in a project I have data like this state:{comments:{items:[{...},{...},{...},...]} and to update one item in items I do this

case actionTypes.UPDATE_COMMENT:
  const indexComment = state.comments.items.findIndex( 
    (comment) => comment.id === action.payload.data.id,
  );
  return {
    ...state,
    comments: {
      ...state.comments,
      items: state.comments.items.map((el, index) =>
        index === indexComment ? { ...el, ...action.payload.data } : el,
      ),
    },
  };

Upvotes: 0

Erfan Akhavan
Erfan Akhavan

Reputation: 355

In my case I did something like this, based on Luis's answer:

// ...State object...
userInfo = {
name: '...',
...
}

// ...Reducer's code...
case CHANGED_INFO:
return {
  ...state,
  userInfo: {
    ...state.userInfo,
    // I'm sending the arguments like this: changeInfo({ id: e.target.id, value: e.target.value }) and use them as below in reducer!
    [action.data.id]: action.data.value,
  },
};

Upvotes: 3

Ivan V.
Ivan V.

Reputation: 8081

Very late to the party but here is a generic solution that works with every index value.

  1. You create and spread a new array from the old array up to the index you want to change.

  2. Add the data you want.

  3. Create and spread a new array from the index you wanted to change to the end of the array

let index=1;// probably action.payload.id
case 'SOME_ACTION':
   return { 
       ...state, 
       contents: [
          ...state.contents.slice(0,index),
          {title: "some other title", text: "some other text"},
         ...state.contents.slice(index+1)
         ]
    }

Update:

I have made a small module to simplify the code, so you just need to call a function:

case 'SOME_ACTION':
   return {
       ...state,
       contents: insertIntoArray(state.contents,index, {title: "some title", text: "some text"})
    }

For more examples, take a look at the repository

function signature:

insertIntoArray(originalArray,insertionIndex,newData)

Edit: There is also Immer.js library which works with all kinds of values, and they can also be deeply nested.

Upvotes: 31

NearHuscarl
NearHuscarl

Reputation: 81370

This is remarkably easy in redux-toolkit, it uses Immer to help you write immutable code that looks like mutable which is more concise and easier to read.

// it looks like the state is mutated, but under the hood Immer keeps track of
// every changes and create a new state for you
state.x = newValue;

So instead of having to use spread operator in normal redux reducer

return { 
  ...state, 
  contents: state.contents.map(
      (content, i) => i === 1 ? {...content, text: action.payload}
                              : content
  )
}

You can simply reassign the local value and let Immer handle the rest for you:

state.contents[1].text = action.payload;

Live Demo

Edit 35628774/how-to-update-single-value-inside-specific-array-item-in-redux

Upvotes: 8

Yuya
Yuya

Reputation: 2311

You don't have to do everything in one line:

case 'SOME_ACTION': {
  const newState = { ...state };
  newState.contents = 
    [
      newState.contents[0],
      {title: newState.contents[1].title, text: action.payload}
    ];
  return newState
};

Upvotes: 24

Damian Czapiewski
Damian Czapiewski

Reputation: 881

I'm afraid that using map() method of an array may be expensive since entire array is to be iterated. Instead, I combine a new array that consists of three parts:

  • head - items before the modified item
  • the modified item
  • tail - items after the modified item

Here the example I've used in my code (NgRx, yet the machanism is the same for other Redux implementations):

// toggle done property: true to false, or false to true

function (state, action) {
    const todos = state.todos;
    const todoIdx = todos.findIndex(t => t.id === action.id);

    const todoObj = todos[todoIdx];
    const newTodoObj = { ...todoObj, done: !todoObj.done };

    const head = todos.slice(0, todoIdx - 1);
    const tail = todos.slice(todoIdx + 1);
    const newTodos = [...head, newTodoObj, ...tail];
}

Upvotes: 1

TazExprez
TazExprez

Reputation: 335

This is how I did it for one of my projects:

const markdownSaveActionCreator = (newMarkdownLocation, newMarkdownToSave) => ({
  type: MARKDOWN_SAVE,
  saveLocation: newMarkdownLocation,
  savedMarkdownInLocation: newMarkdownToSave  
});

const markdownSaveReducer = (state = MARKDOWN_SAVED_ARRAY_DEFAULT, action) => {
  let objTemp = {
    saveLocation: action.saveLocation, 
    savedMarkdownInLocation: action.savedMarkdownInLocation
  };

  switch(action.type) {
    case MARKDOWN_SAVE:
      return( 
        state.map(i => {
          if (i.saveLocation === objTemp.saveLocation) {
            return Object.assign({}, i, objTemp);
          }
          return i;
        })
      );
    default:
      return state;
  }
};

Upvotes: 1

Luis Rodriguez
Luis Rodriguez

Reputation: 523

I believe when you need this kinds of operations on your Redux state the spread operator is your friend and this principal applies for all children.

Let's pretend this is your state:

const state = {
    houses: {
        gryffindor: {
          points: 15
        },
        ravenclaw: {
          points: 18
        },
        hufflepuff: {
          points: 7
        },
        slytherin: {
          points: 5
        }
    }
}

And you want to add 3 points to Ravenclaw

const key = "ravenclaw";
  return {
    ...state, // copy state
    houses: {
      ...state.houses, // copy houses
      [key]: {  // update one specific house (using Computed Property syntax)
        ...state.houses[key],  // copy that specific house's properties
        points: state.houses[key].points + 3   // update its `points` property
      }
    }
  }

By using the spread operator you can update only the new state leaving everything else intact.

Example taken from this amazing article, you can find almost every possible option with great examples.

Upvotes: 8

Clarkie
Clarkie

Reputation: 7550

You could use the React Immutability helpers

import update from 'react-addons-update';

// ...    

case 'SOME_ACTION':
  return update(state, { 
    contents: { 
      1: {
        text: {$set: action.payload}
      }
    }
  });

Although I would imagine you'd probably be doing something more like this?

case 'SOME_ACTION':
  return update(state, { 
    contents: { 
      [action.id]: {
        text: {$set: action.payload}
      }
    }
  });

Upvotes: 77

Fatih Erikli
Fatih Erikli

Reputation: 3066

You can use map. Here is an example implementation:

case 'SOME_ACTION':
   return { 
       ...state, 
       contents: state.contents.map(
           (content, i) => i === 1 ? {...content, text: action.payload}
                                   : content
       )
    }

Upvotes: 241

Related Questions