Search code examples
reactjsfluxreactjs-flux

How should I be recreating a stateful child component in a parent's render method?


Facebook says that I should not keep a React component in its parent's state. Instead I should be recreating the child in render method each time it is run.

What Shouldn't Go in State?

React components: Build them in render() based on underlying props and state.

Now my question is: How can I do that? Is it even possible? Isn't the state lost if I recreate a child component from scratch?

The only way I can think of that this scenario will work in, is that there's only one state object and it belongs to the root component. The rest of components will only have props and whenever they want to update some state of theirs, they need to call some parent's handler all the way up to root component, since it's the only component with an state object! And once updated, the root will give the child components back their state as props. Which I don't think it is practical at all!

[UPDATE]

Here's a sample code that I find hard not to store components in the parent's state:

http://codepen.io/mehranziadloo/pen/XdLvgq

class BlackBox extends React.Component
{
    constructor() {
        super();
        this.state = {
            counter: 0
        };
    }
    
    increment() {
        this.setState({ counter: this.state.counter+1 });
    }
    
    render() {
        return (
            <span onClick={this.increment.bind(this)} style={{
                fontSize: '24pt',
                border: '1px solid black',
                margin: 10,
                padding: 10,
            }}>
                {this.state.counter}
            </span>
        );
    }
}

class RedBox extends React.Component
{
    constructor() {
        super();
        this.state = {
            counter: 0
        };
    }
    
    increment() {
        this.setState({ counter: this.state.counter+1 });
    }
    
    render() {
        return (
            <span onClick={this.increment.bind(this)} style={{
                fontSize: '24pt',
                border: '1px solid red',
                margin: 10,
                padding: 10,
            }}>
                {this.state.counter}
            </span>
        );
    }
}

class Parent extends React.Component
{
    constructor() {
        super();
        this.state = {
            childCmps: [],
        };
    }

    addBlackBox() {
        let newState = this.state.childCmps.slice();
        newState.push(<BlackBox key={newState.length} />);
        this.setState({
            childCmps: newState
        });
    }

    addRedBox() {
        let newState = this.state.childCmps.slice();
        newState.push(<RedBox key={newState.length} />);
        this.setState({
            childCmps: newState
        });
    }

    render() {
        let me = this;

        return (
            <div>
                <button onClick={this.addBlackBox.bind(this)}>Add Black Box</button> 
                <button onClick={this.addRedBox.bind(this)}>Add Red Box</button>
                <br /><br />
                {this.state.childCmps}
            </div>
        );
    }
}

ReactDOM.render(
    <Parent />,
    document.getElementById('root')
);

Solution

  • Isn't the state lost if I recreate a child component from scratch?

    No, because React internally manages the backing instances (which hold the state) and does not replace them if two calls to render() say to render that component.

    In other words:

    ReactDOM.render(<MyComponent />, div);
    ReactDOM.render(<MyComponent />, div);
    

    This will not create MyComponent twice, but only once. It will render it twice: the first time it doesn't exist, so it creates it, and the second time it already exists, so it will update it. Any internal state that may be set between the two render passes will be preserved.

    React is optimized to allow you to simply create complete, declarative render functions, and it figures out what changes are needed to actualize the rendering.


    Update

    The example you posted is using keys on a dynamic list of children. Keys are a way to identify specific children (and where they exist), so you need to be careful not to change keys between render passes for elements that maintain state.

    Instead of storing the actual rendered components in state, such as <BlackBox key={i} />, store the necessary data to render the component, such as the component class BlackBox and a unique identifier for the key. (FYI you shouldn't use index as key, since index can change. I recommend using an always incrementing counter.)

    Here is the Parent class modified to work without storing rendered components in state (the other components can remain as is):

    class Parent extends React.Component {
        static blackCount = 0;
        static redCount = 0;
        state = {
            childCmps: [],
        };
        constructor(props, context) {
            super(props, context);
        }
    
        addBlackBox = () => {
            this.setState({
                childCmps: [...this.state.childCmps, { Component: BlackBox,  id: "black" + (++Parent.blackCount) }]
            });
        };
    
        addRedBox = () => {
            this.setState({
                childCmps: [...this.state.childCmps, { Component: RedBox, id: "red" + (++Parent.redCount) }]
            });
        };
    
        render() {
            return (
                <div>
                    <button onClick={this.addBlackBox}>Add Black Box</button> 
                    <button onClick={this.addRedBox}>Add Red Box</button>
                    <br /><br />
                    {this.state.childCmps.map(child => <child.Component key={child.id} />)}
                </div>
            );
        }
    }
    

    Example in CodePen.

    Notes:

    • I used static (aka global) props to count how many black and red boxes have been added, combined with the strings "red" and "black" to form unique keys. (You can use Parent.blackCount = 0, etc, to initialize static class properties if you don't have support for class properties.)
    • I used fat arrow function properties as event handler callbacks to ensure this is in the correct scope. (You can use this.addBlackBox = this.addBlackBox.bind(this) in the constructor if you don't have support for class properties.)
    • I moved state initialization to a class property. As you can guess, I highly recommend you make use of class properties. :)
    • I used ES6 spread with array literal initialization to append a new box and create a new array.
    • Finally, in the Parent/render() function each box component is always re-rendered using a map() of the state with dynamic component type rendering of <child.Component>.