Not as you'd expect: how React persists state across renders

12/11/2015

A few months ago we released a new version of our checkout page, one of the first Big Projects we’ve built with React. The ability to power the views by state makes it easy to reason about the various flows and has drastically improved our ability to make changes on the page.

While checking out the user is presented with a number of forms to collect their payment information, shipping address, etc, so we built a form wrapper component. This component stores the form’s values in state and makes them accessible to its parent. Not the best solution for the purpose, but it worked and is still infinitely better than how this page used to function. This form component worked well until we started to render a second form component in the same place on the page, determined by what step the user is on.

Take a step back and look at one of the most important concepts React introduced to the JavaScript community: Virtual DOM. React maintains an internal representation of the DOM, comparing old and new renders to see what has changed and only applies those differences to the browser’s DOM. This is a huge optimization because DOM access is slow and changing any value could lead to recalculating CSS, physically moving elements, repainting, and a whole host of other slow operations.

As an example, imagine there is a page heading that changes its text and color depending on what a user is doing on the page. On the first render, the element simply says Welcome but after clicking a join button it changes to Join and turns green, because everyone likes green. React notices the two things that changed and updates the existing DOM element with the new text and style.

if (isJoining) {
   return <h1 style={{color: ‘green’}}>Join</h1>

} else {
   return <h1>Welcome</h1>

}

In addition to not creating a new element, React retains the existing heading component in the virtual DOM. Not obviously, this means that any state associated with the first render is carried through to the new one. This is where we got into trouble with our form component: to us, our code said “show either the billing form or the shipping form”, but to React the code actually was “show the form with either billing properties or shipping properties”. After the user filled out their shipping address and started entering billing information, the form component still had the shipping data in its state.

There are two solutions to the issue. The first forces React to drop the existing component and create a new one. Simply adding a unique key attribute to each use of the form component forces React to treat them separately. This approach is not performant and it ignores a lot of React best practices, which leads us to the second solution: don’t store the form’s values in its state. If everything the form needs is passed to it through properties then there’s no state to accidentally leak. Changes to values can be propagated through event handlers or a Flux-like model.

 

Comments (0)

The comments to this entry are closed.