How should I be recreating a stateful child compon

2019-08-13 18:35发布

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')
);

4条回答
我欲成王,谁敢阻挡
2楼-- · 2019-08-13 18:49

You only need to keep in parent's state any data necessary for rendering the children components. Typically, this is just the props you want pass, or the type of component.
In your case, this is just the color of the component "Red" or "Black".
So in parent state, an array containing Strings with value "Red" or "Black" is enough.

And everytime one of the buttons is clicked, you simply add another item to the array, and set state again. Something like this.

addRedBox() {
    let newChildList = this.state.childList.slice();
    newChildList.push("Red");
    this.setState({
        childList: newChildList
    });
}

And then in your render() function do this:

{this.state.childList.map(function(color,i) {
  if (color=="Black") {
    return <BlackBox key={i} />
  } else {
    return <RedBox key={i} />
  }
})}

On re-render, you simply pass new props (if any) to your child components, and each child component will then also re-render with the new props.
Passing new props to the child will not reset the child component. It will simply run all lifecycle methods again (including render()).

You can find a working version in this codepen.

查看更多
来,给爷笑一个
3楼-- · 2019-08-13 18:59

the component only renders if the state changes(updates), and you should keep your state simple, use props to communicate with the children components.

and when your App gets larger you can use Flux or Redux to manage your states

查看更多
疯言疯语
4楼-- · 2019-08-13 19:03

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>.
查看更多
迷人小祖宗
5楼-- · 2019-08-13 19:11

You are attempting to see an Object-Oriented approach in React. Don't. There's OO and then there's whatever it is that Facebook do.

No, you cannot store components in state, as per the documentation you quoted. You can try it but you'll find things just don't work.

Here's an example of an OO class (in pseudocode):

class Parent {
  list children

  temporarilyAbondonChildren() {
    for each child in children {
      Orphanage.leaveAtDoorStep( child )
    }

    doStuffForTenYears()

    for each child in Children {
     output child.isOk()
    }
  }
}

And here's the closest equivalent to it in React:

class Parent {
  temporarilyAbandonChildren() {
    doStuffForTenYears()
    child_properties = Orphanage.whatWouldveHappenedToMyKidHadIGivenBirthAndLeftThemForTenYears()

    children = renderImaginaryChildren( child_properties )

    for each child in children {
      output child.isOk()
    }
  }

}

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!

I agree.

查看更多
登录 后发表回答