16

This question might fall a little on the side of a "best practice" question, but please bear with me.

Here is a portion of my state:

this.state = {
  typeElements: {
    headers: [
        {
          name: "h1",
          size: 70,
          lineHeight: 1.25,
          kearning: 0,
          marginAfter: 0
        }, {
          name: "h2",
          size: 70,
          lineHeight: 1.25,
          kearning: 0,
          marginAfter: 0
        }, {
          name: "h3",
          size: 70,
          lineHeight: 1.25,
          kearning: 0,
          marginAfter: 0
        }...

What I need to do is REPLACE the object at a given index on the headers array.

I don't know how to do that with the setState method as in this.setState(headers[1] = {obj}) - but that's obviously invalid. My current method is creating a new array and clobbering the old one like this:

_updateStyle(props) {
  let newState = Object.assign({}, this.state)
  newState.typeElements.headers[props.index] = props
  this.setState(newState)
};

For my small hacky project I guess it's OK but I feel like this is super heavy handed and would quickly lead to performance issues at any kind of scale.

halfer
  • 19,824
  • 17
  • 99
  • 186
motleydev
  • 3,327
  • 6
  • 36
  • 52

4 Answers4

19

Updated: since this answer still gets upvotes, be aware that the previous answer below is outdated with modern JavaScript and React. The "update" addon is now legacy and "immutability-helper" can be used instead.

The React docs also mention why immutability is important so avoid mutating state. For immutable updates you can use Object.assign() or spread syntax which needs to be done for every level of nesting, like in this example the nested headers object and its array elements. In this particular example we can use the array index as key so it's possible to also use the spread operator to make a shallow clone of the array and assign a new object as value at given index in the cloned array.

_updateStyle (props) {
  const { typeElements } = this.state;
  const updatedHeaders = [...typeElements.headers];
  updatedHeaders[props.index] = props;
  this.setState({
    ...this.state,
    typeElements: {
      ...typeElements,
      headers: updatedHeaders
    }
  ));
}

Another solution which doesn't require the spread syntax and is needed if we are not using the array index to find the object we want to replace, is using array.map to create a new array and returning the new object instead of the old one at given index.

  const updatedHeaders = typeElements.headers.map((obj, index) => {
    return index === props.index ? props : obj;
  });

Similar examples in the Redux docs also explain "immutable update patterns".

React has some immutability helpers for this, which is explained in the docs: https://facebook.github.io/react/docs/update.html

In your case you could use the $splice command to remove one item and add the new one at given index, for example:

_updateStyle (props) {
  this.setState(update(this.state.typeElements, 
    { $splice: [[props.index, 1, props]] }
  ));
}
Robin Venneman
  • 383
  • 1
  • 7
  • aha, I was aware of those but wasn't sure ow to implement, so it goes in the setState method? What does the "1" do? Is that the insertion point of the new object? Could I not just use props.index again for that if that is the case? – motleydev Oct 06 '16 at 07:47
  • 1
    Check out the array.splice() method https://developer.mozilla.org/nl/docs/Web/JavaScript/Reference/Global_Objects/Array/splice and you will understand... the array holds the arguments passed to the splice method, so the "1" in this example is the deleteCount. – Robin Venneman Oct 06 '16 at 08:06
5

Offering a better explanation of how to accomplish this.

  • First, find the index of the element you're replacing in the state array.
  • Second, update the element at that index
  • Third, call setState with the new collection
import update from 'immutability-helper';

// this.state = { employees: [{id: 1, name: 'Obama'}, {id: 2, name: 'Trump'}] } 

updateEmployee(employee) {
    const index = this.state.employees.findIndex((emp) => emp.id === employee.id);
    const updatedEmployees = update(this.state.employees, {$splice: [[index, 1, employee]]});  // array.splice(start, deleteCount, item1)
    this.setState({employees: updatedEmployees});
}
daino3
  • 4,386
  • 37
  • 48
1

use immutability-helper

you can find nice examples there

ChaosPredictor
  • 3,777
  • 1
  • 36
  • 46
0

Object.Assign uses shallow copy, not deep copy.

Please be aware that in your example Object.assign({}, this.state) copies only links to the nearest children of the state, i.e. to typeElements, but headers array is not copied.

There is a syntactic sugar ... in ES6 for Object.Assign, Babel has the special addon transform-object-rest-spread.

let newHeaders = { ...this.state.typeElements.headers};
newHeaders[index] = props.Something;

let newState = {...state, typeElements: { newHeaders }};
Artur A
  • 7,115
  • 57
  • 60
  • Really? It actually did copy the headers for me too. Can you elaborate? – motleydev Oct 06 '16 at 07:40
  • @motleydev The Object.assign() method only copies enumerable and __own__ properties from a source object to a target object (MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign). It means that it copies only references for the nearest objects or values for primitives (numbers, strings). You get the old array in your implementation and if you mutate it then it also will affect other places where it is in use. – Artur A Oct 06 '16 at 09:44