Skip to content

This repository demonstrates the problem of stale state reads in React and a possible solution for it.

Notifications You must be signed in to change notification settings

teamious/example-react-premature-state-reads

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

README

This repo contains sample code and a description of how to deal with multiple components competing to update the same state object in your hierarchy at the same time (such as in componentDidMount).

To demonstrate the problem, consider the following object:

const values = [
    'Andrew',
    'Brooke',
]

values represents a piece of the state object contained in our root component. Now, for each index in the values array we want to be represented by a Node component. The Node component is fairly simple, it should render a UI for the value:

interface NodeProps {
    value: string;
}

class Node extends React.Component<NodeProps, {}> {
    render() {
        return (
            <div>
                {this.props.value}
            </div>
        )
    }
}

In addition to rendering the value, each Node should modify it's own value (by adding the value to itself) after mounting and then notify the parent via a function named onChange.

componentDidMount() {
    this.props.onChange(this.props.value + this.props.value);
}

The Node class will end up looking like this:

interface NodeProps {
    value: string;
    onChange: (value: string) => void;
}

class Node extends React.Component<NodeProps, {}> {
    componentDidMount() {
        this.props.onChange(this.props.value + this.props.value);
    }

    render() {
        return (
            <div>
                {this.props.value}
            </div>
        )
    }
}

Now, in our root component we can write the logic to render a Node to handle each value in the state. We also

interface AppState {
    values: string[]
}

class App extends React.Component<{}, AppState> {
    constructor() {
        super();
        this.state = {
            values: ['Andrew', 'Brooke'],
        }
    }
    render() {
        return (
            <div>
              {this.state.values.map(value => <Node value={value}/>)}
            </div>
        );
    }
}

However, the App class isn't complete yet. It needs to pass an onChange event to the Node so that it can be notified when the Node propagates its new value in it's componentDidMount lifecycle hook. We can implement an onChange function and use it in our render function:

    onChange(index: number, value: string) {
        const values = this.state.values.slice();
        values[index] = value;
        this.setState({values})
    }

    render() {
        return (
            <div>
                {this.state.values.map((value, index) => (
                    <Node value={value} onChange={this.onChange.bind(this, index))}/>
                )}
            </div>
        );
    }

Now, right now you may think everything goes and works fine. In your HTML, you would expect to see:

AndrewAndrew
BrookeBrook

But actually you will see this:

Andrew
BrookeBrooke

This is the problem of the premature state reads. If you've read about React, you should know that the setState is not synchronous. Basically, React will batch the state changes in order to be more efficient. The problem this presents is that you cannot expect your state change to be read immediately.

In the example above, when the first node calls onChange the state at that point in time is ['Andrew', 'Brooke']. After calling setState you might think that this.state.values looks like this: ['AndrewAndrew', 'Brooke']. Then when the second Node calls onChange, it will use use the new state object ['AndrewAndrew', 'Brooke']' and update it to ['AndrewAndrew', 'BrookeBrooke']. This won't happen because the second onChangeperforms a premature state read and gets back['Andrew', 'Brooke']and returns['Andrew', 'BrookeBrooke']. When the second Node calls setState, it essentially overwrites the first Node's onChange` result.

This is a problem that we encountered developing react-dynamic-formbuilder. The problem is that our FormInput component (think of it is being similar to App class above) receives multiple requests to change our value object after each component mounts. In our case, each change request operates on a value object that lives in props. After each change request, the context of FormInput is notified via an onChange event. The problem is that the context of FormInput does not immediately pass the props down before FormInput services the next change request. This results in a stale read of props and results in overwriting prior change requests.

In order to solve this problem, a solution that was proposed was to copy props into state and work directly on the state object. However, at first glance, this solution proved to pose the same problem. The state object would be stale (or read prematurely) when attempting to service multiple synchronous change requests.

After doing research on the setState API, we found an alternative usage. Basically, you can substitute a function for the value you might normally pass into setState (as of React v15.16.1 and possibly earlier);

this.setState({
    values: ['Andrew', 'Brooke']
})

// can also be written as this

this.setState(() => {
    return {
        values: ['Andrew', 'Brooke'],
    }
})

Nothing special, right? But that's not all. The function you pass into setState can access the previous state object before it has been used to render the component.

this.setState((prevState) => {
    const values = prevState.values.slice()
    values[i] = newValue
    return {values}
})

Using this approach, your state change requests can build ontop of prior changes and avoid the stale read problem described above.

Demo

You can see the demo by running:

npm install
npm start

and then navigate to localhost:8080.

About

This repository demonstrates the problem of stale state reads in React and a possible solution for it.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published