0

I am trying to implement a minimize/maximize feature in React. I have a div that serves as an information panel and by clicking on a button I would like it to toggle between maximized / minimized states. My top level app component has a boolean state field (maximizedInfo in the example below) that tracks whether the information panel is maximized or not and accordingly renders either just the panel or the full grid of my application with many other DOM elements. The below code is obviously a minified example but the main idea is that the render() method of my top-level component generates two very different DOM trees depending on the state. Unfortunately, I have discovered that my information panel component keeps getting unmounted and the constructor is called on every state change, thus losing the state the information panel component had accumulated.

What is the proper way to address that and implement this sort of functionality in React?

const React = require('react');



class InformationPanel extends React.Component {

  constructor(props) {
    console.log('InformationPanel:: constructor'); // this keeps getting called
    super(props);
  }

  render() {

    return (
      <div>
        <a id='information' class='nav-link' href="#" onClick={this.props.toggleInfoPanel}>toggle</a>
        short info
      </div>

    );
  }
}

class App extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      maximizedInfo: false
    };
    this.toggleInfoPanel = this.toggleInfoPanel.bind(this);
  }

  toggleInfoPanel() {
    this.setState({maximizedInfo: !this.state.maximizedInfo});
  }

  render() {
    if (this.state.maximizedInfo)
      return (
        <div class='container-fluid'>
          <div class='row no-gutters'>
            <div class='col-12 padding-0'>
              <InformationPanel toggleInfoPanel={this.toggleInfoPanel}/>
            </div>
          </div>
        </div>
      )
    else return (
      <div class='container-fluid'>
        <div class='row no-gutters'>
          <div class='col-10'>
            some other info that takes up more space ...
          </div>
          <div class='col-2 padding-0'>
            <InformationPanel toggleInfoPanel={this.toggleInfoPanel}/>
          </div>
        </div>
      </div>
    );
  }
}
Marcus Junius Brutus
  • 26,087
  • 41
  • 189
  • 331
  • use centralized state (f.e. redux) or use 'classic' css (`display:none`) to hide components – xadm Feb 26 '20 at 10:45

3 Answers3

1

1st of all - keep structure/tree unmodified [and more maintainable]:

render() {
  return (
    <div class='container-fluid'>
      <div class='row no-gutters'>
        {this.state.maximizedInfo && 
          <div class='col-10'>
          some other info that takes up more space ...
        </div>
        }
        <div key="infoPanel" class='col-2 padding-0'>
          <InformationPanel toggleInfoPanel={this.toggleInfoPanel}/>
        </div>
      </div>
    </div>
  );
}

Adding key prop helps in reconcilation - article

After this change <InformationPanel/> should not be rerendered. Notice that change is on parent - the place where child nodes differs. Parent not changed, props not changed, no rerendering.

2nd - above is not enough - we want size change!

I'd say that's a 'structural problem' - styling should be done inside <InformationPanel/> with required change passed as prop (f.e.):

 <InformationPanel key="infoPanel" wide={!this.state.maximizedInfo} toggleInfoPanel={this.toggleInfoPanel}/>

   // and in render in <InformationPanel/>
   <div className={`padding-0 ${this.props.wide ? 'col-12' : 'col-2'}`}>
     ...

Still use key prop!

Other options for conditional styling in this thread

xadm
  • 8,219
  • 3
  • 14
  • 25
0

xadm's answer was correct that key is essential for tree reconciliation. The thing is, I discovered that the key needs to be present in the parent components, not necessarily in the InformationPanel component. The below code works:

    if (this.state.maximizedInfo)
      return (
        <div key='a' class='container-fluid'>
          <div key='b' class='row no-gutters'>
            <div key='c' class='col-12 padding-0'>
              {informationPanel}
            </div>
          </div>
        </div>
      )
    else return (
      <div key='a' class='container-fluid'>
        <div key='b' class='row no-gutters'>
          <div class='col-10'>
            some other info that takes up more space ...
          </div>
          <div key='c' class='col-2 padding-0'>
              {informationPanel}
          </div>
        </div>
      </div>
    );
Marcus Junius Brutus
  • 26,087
  • 41
  • 189
  • 331
-1

Since you don't want to lose state in InformationPanel, you can declare it outside conditional rendering so that it won't be getting unmounted on state change. Code will look something like below:

class App extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      maximizedInfo: false
    };
    this.toggleInfoPanel = this.toggleInfoPanel.bind(this);
  }

  toggleInfoPanel() {
    this.setState({maximizedInfo: !this.state.maximizedInfo});
  }

  render() {
  const informationPanel = <InformationPanel toggleInfoPanel={this.toggleInfoPanel} />
    if (this.state.maximizedInfo)
      return (
        <div class='container-fluid'>
          <div class='row no-gutters'>
            <div class='col-12 padding-0'>
              {informationPanel}
            </div>
          </div>
        </div>
      )
    else return (
      <div class='container-fluid'>
        <div class='row no-gutters'>
          <div class='col-10'>
            some other info that takes up more space ...
          </div>
          <div class='col-2 padding-0'>
            {informationPanel}
          </div>
        </div>
      </div>
    );
  }
}
mukesh210
  • 2,792
  • 2
  • 19
  • 41