70

I'm quite new to React.JS and I am in the process of experimenting by building a masonry-style layout.

I render each element to the DOM, then I need to loop over each item and apply x and y positions based on the preceding elements.

The initial model looks like this:

[
  {
    "title": "The Forrest",
    "description": "some cool text",
    "imgSmallSrc": "/img/img4-small.jpg",
    "imgAlt": "Placeholder image",
    "tags": [
        "Design",
        "Mobile",
        "Responsive"
    ],
    "date": 1367154709885,
    "podStyle": {
      "width": 253
    }
  }
]

(I've only shown one item to keep things short).

Once I complete the loop and have my x and y data I want to apply this to the podStyle object. I call setState() with the following data:

[
  {
    "podStyle": {
      "x": 0,
      "y": 0,
      "height": 146,
      "width": 253
    }
  }
]

This seems to remove all current data from the model and leave me with just the podStyle data. Am I misunderstanding how this merge works?

starball
  • 20,030
  • 7
  • 43
  • 238
DanV
  • 3,193
  • 4
  • 29
  • 43
  • 3
    Try the [immutability helpers](https://facebook.github.io/react/docs/update.html) in React's addons for a declarative way of modifying complex state objects. – Ross Allen Jul 23 '14 at 11:23

3 Answers3

84

If your state is an object:

getInitialState: function() {
  return { x: 0, y: 0 };
}

you can use setState to set individual keys on that object:

this.setState({ x: 1 }); // y still == 0

React does no intelligent merging of your state; for example, this does not work:

getInitialState: function() {
  return {
    point: { x: 0, y: 0 },
    radius: 10
  };
}

this.setState({point: {x: 1}});
// state is now == {point: {x: 1}, radius: 10} (point.y is gone)

[Edit]

As mentioned by @ssorallen, you can use the immutability helpers to get the effect you're after:

var newState = React.addons.update(this.state, {
  point: { x: {$set: 10} }
});
this.setState(newState);

See this JSFiddle for an example: http://jsfiddle.net/BinaryMuse/HW6w5/

Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
  • So I'm pretty sure I'm setting the state as per your second example, however, 'radius' in this case is getting removed. When I'm home tonight I'll have a play around and make sure I'm setting correctly. Thanks for your help Brandon! – DanV Jul 23 '14 at 08:15
  • 1
    @DanV Here is a JSFiddle to demonstrate the code in my answer. http://jsfiddle.net/BinaryMuse/HW6w5/ I also added a button to use the immutability helpers mentioned by ssorallen, which does what you want to do. – Michelle Tilley Jul 23 '14 at 15:37
  • Thanks so much for your help Brandon. This definitely sent me in the right direction. I ended up using the $merge helper as my model was slightly different and I was also applying the updates to several children items. Works nicely though. – DanV Jul 28 '14 at 07:58
23

The merging is shallow, so this.setState({point}) leaves (ed: this.state.radius) intact, but completely replaces (ed: this.state.point).

https://facebook.github.io/react/docs/state-and-lifecycle.html#state-updates-are-merged

To offer an ES7+ perspective on the answers already given, using transform-object-rest-spread instead of Object.assign():

class MyComponent extends React.Component {
    state = {
        point: { 
            x: 0, 
            y: 0,
        },
        radius: 10,
    }

    handleChange = () => {
        this.setState((prevState, props) => ({
            point: {
                // rest operator (...) expands out to:
                ...prevState.point, // x:0, y:0,
                y: 1, // overwrites old y
            },
            // radius is not overwritten by setState
        }));
    }

    render() {
        // omitted
    }
}

.babelrc (also requires transform-class-properties from babel preset stage 2)

{
    "presets": ["es2015", "stage-2", "react"],
    "plugins": ["transform-object-rest-spread"],
}

Updated 2018-04-22

As @sheljohn points out (thanks!), referring to this.state inside setState is unreliable:

Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

...

To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument

https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous

ptim
  • 14,902
  • 10
  • 83
  • 103
  • Thank you! I think it is an amazing solution. – Balázs Orbán Feb 04 '17 at 15:23
  • 2
    I am new to react, but this seems wrong; `handleChange` [should call](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous) `this.setState( (prevState, props) => ({ ... // use prevState.point }) )` instead. – Jonathan H Apr 21 '18 at 16:14
  • OMG I can't believe, it works like charm. You saved my day. – user1892203 Dec 03 '19 at 00:49
4

Something like:

getInitialState: function() {
    return {
        something: { x: 0, y: 0 },
        blah: 10
    };
}

var state = Object.assign(this.state, {
    something: Object.assign(this.state.something, { y: 50 }),
});

this.setState(state);

Would be better if it was recursive/deep rather than hard coding the tree, but I will leave that up to the reader :)

Metalstorm
  • 2,940
  • 3
  • 26
  • 22
  • The outer `Object.assign` isn't really doing anything since the `something` property you're assigning still refers to the original (although mutated) object. Also, you shouldn't mutate objects in the state directly. It may be OK since you call `setState` anyway, but it's not clean. Furthermore, you can supply partial updates to `setState` instead of a full state object. – herman Aug 01 '16 at 10:17
  • 3
    Thinking about it a little more: it is definitely NOT OK to mutate `this.state` and then call `setState` with a reference to `this.state`. This is because it will break code in `shouldComponentUpdate` that may compare the next state with the previous state (which will always be equal in this case since both are references to the same object). – herman Aug 01 '16 at 11:24