3

Through my previous question I learned that React preserves the state for child components automatically and that's why its documentation says:

What Shouldn't Go in State?

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

Now my question is how to do the same for components that we are just hiding?

Consider a child component that in an iteration is not going to be shown and at the same time we would like to preserve its state for the future when it is going to be brought back!

To illustrate what I exactly mean I've designed a case scenario to show it to you. In the following code you can add stateful child components. A checkbox is provided for each component so you can flag them. Finally the third button will hide the child components that are not flagged. I'm looking for a way to restore the state for the not-flagged components once they are brought back.

Executable code

class BlackBox extends React.Component
{
  constructor() {
    super();
    this.state = {
      checked: false,
      counter: 0,
    };
  }

  increment = () => {
    this.setState(Object.assign({}, this.state, { counter: this.state.counter+1 }));
  };

  switch = () => {
    this.setState(Object.assign({}, this.state, { checked: !this.state.checked }));
  };

  isChecked() {
      return this.state.checked;
  }

  render() {
    return (
      <span onClick={this.increment} title={this.props.id} style={{
        fontSize: '24pt',
        border: '1px solid black',
        margin: 10,
        padding: 10,
      }}>
        <input type="checkbox" onChange={this.switch} checked={this.state.checked} />
        {this.state.counter}
      </span>
    );
  }
}

class RedBox extends React.Component
{
  constructor() {
    super();
    this.state = {
      checked: false,
      counter: 0
    };
  }

  increment = () => {
    this.setState(Object.assign({}, this.state, { counter: this.state.counter+1 }));
  };

  switch = () => {
    this.setState(Object.assign({}, this.state, { checked: !this.state.checked }));
  };

  isChecked() {
      return this.state.checked;
  }

  render() {
    return (
      <span onClick={this.increment} title={this.props.id} style={{
        fontSize: '24pt',
        border: '1px solid red',
        margin: 10,
        padding: 10,
      }}>
        <input type="checkbox" onChange={this.switch} checked={this.state.checked} />
        {this.state.counter}
      </span>
    );
  }
}

class Parent extends React.Component {
    static blackCount = 0;
    static redCount = 0;
    state = {
      childCmps: [],
      showOnlyChecked: false,
    };
    constructor(props, context) {
      super(props, context);
    }

    addBlackBox = () => {
      this.setState(Object.assign({}, this.state, {
        childCmps: [...this.state.childCmps, { Component: BlackBox,  id: "black" + (++Parent.blackCount) }],
      }));
    };

    addRedBox = () => {
      this.setState(Object.assign({}, this.state, {
        childCmps: [...this.state.childCmps, { Component: RedBox, id: "red" + (++Parent.redCount) }],
      }));
    };

    showHide = () => {
      this.setState(Object.assign({}, this.state, {
        showOnlyChecked: !this.state.showOnlyChecked,
      }));
    };

    render() {
      let children = this.state.childCmps.map(child => <child.Component key={child.id} id={child.id} ref={child.id} />);
      return (
        <div>
          <button onClick={this.addBlackBox}>Add Black Box</button> 
          <button onClick={this.addRedBox}>Add Red Box</button>
          <button onClick={this.showHide}>Show / Hide Unchecked</button>
          <br /><br />
          {children.filter(box => !this.state.showOnlyChecked || this.refs[box.props.id].isChecked())}
        </div>
      );
    }
}

ReactDOM.render(
    <Parent />,
    document.getElementById("root")
);
Community
  • 1
  • 1
Mehran
  • 15,593
  • 27
  • 122
  • 221

2 Answers2

3

The short answer:

There is no solution that only has advantages and no drawbacks (in real life as well as in your question).
You really have only 3 options here:

  1. use parent state and manage your data (in parent component and/ or stores)
  2. use child state and hide children you do not need (so find another animation solution)
  3. use child state and accept that state is lost for children not re-rendered -

You may want to check out redux for using stores.

The long answer

If you want to

  • remove a child from the DOM (e.g. for animation purposes with ReactCSSTransitionGroup)
  • AND keep the checked/unchecked & counter info

Then you cannot keep this in child state. State is by definition bound to the lifecycle of a component. If it is removed from DOM (= unmounted), then you will loose all the state info.

So if you want to save this info, you have to move this info to the state of the parent component (for each child obviously).

You can find a working codepen here.

Some other notes about this code:

  • Your child <...Box> components now become pure = stateless components
  • child components call a method on the parent, to update checked state or counter increment. In these methods they pass their own ID
  • Better not to store entire components in state, but only the props that determine the components.
  • So the new parent state contains an array of objects, and each object has relevant prop data
  • In render of the parent: better to filter the components first, before creating an array of components to be rendered
  • In parent you need new methods (onCheck() and onClick()) that will update the specific object in the array from state. Requires some special fiddling to ensure that we do not directly mutate state here.
  • for setState(), you do not need to do an Object.assign() and pass in all of state again. If you provide an object with only the parameters that changed, react will leave the other parameters untouched.
wintvelt
  • 13,855
  • 3
  • 38
  • 43
  • I understand your reasoning but what if the child's state is complex with lots of inner states? I mean like having grandchildren and stuff? Keeping data in a global scope (like stores) makes things so hard, don't you think!? – Mehran May 25 '16 at 12:11
  • 1
    There is no solution that only has advantages and no drawbacks (in real life as well as in your question). You really have only 3 options here: a) use parent state and manage your data b) use child state and hide children you do not need (so find another animation solution), c) use child state and accept that state is lost for children not re-rendered - You could use stores at well, you may want to check out redux. – wintvelt May 25 '16 at 13:18
  • You should have posted this – Mehran May 25 '16 at 14:37
  • I would have, if you had included your points from your comments in original question (your wish to animate + considerations on grand-children and managing data in global scope) :) Updated answer nonetheless.. – wintvelt May 25 '16 at 15:12
0

rather than removing the component from the DOM entirely (as you are doing through filter, just hide it. Replace children.filter with the following:

  {children.map((box, idx) => {
      var show = !this.state.showOnlyChecked || this.refs[box.props.id].isChecked();
      return <span key={idx} style={{display: (show? 'inline-block': 'none')}}>{box}</span>;
  })}
gurch101
  • 2,064
  • 1
  • 18
  • 18
  • I thought of this hack myself but it's not equivalent of not including the tags! For instance if you are going to use animation for hiding child components, your solution won't have the same output of actually leaving out them. – Mehran May 25 '16 at 01:00