0

What is the correct pattern in ReactJS lifecycle (v. 16.4) to display data in child component from componentDidMount in parent component?

I have a scenario which should be simple enough, but it is not behaving the way I expect it to. I want to pass data from a parent component to the child component which in its turn transforms the data into something that can be displayed.

I have read this article https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html, and am trying to get the pattern right.

Previously I used componentWillMount to do this, and that worked fine, but as I said, it behaves strangely with componentDidMount.

My parent component gets data in its componentDidMount which updates state which is then passed to the child component:

class NewTable extends Component {
    constructor(props) {
        super(props);

        this.state = {
            dataSet: {}
        }
    }

    componentDidMount() {
        const dataSet = DataSet.getColorDataSet();

        this.setState({
            dataSet: dataSet
        });
    }

    render() {

        return (
            <div className="example">
                <h2>HTML Table</h2>
                <NewKSParser.HTMLTable dataSet={this.state.dataSet} id="myid" />
            </div>
        );
    }
}

export default NewTable;

My child component should then pick up the dataSet from props and display it:

export class HTMLTable extends Component {

    constructor(props) {
        super(props);

        this.state = {
            columns: [],
            rows: []
        }
    }

    componentDidUpdate(prevProps, prevState) {
        if (prevProps.dataSet !== this.props.dataSet) {
            this.createTableData();
        }
    }

    createTableData = () => {
        const dataSet = { ...this.props.dataSet };

        const rows = dataSet.dataSets ? dataSet.dataSets[0].rows : [];
        const metaCols = dataSet.dataSets ? dataSet.dataSets[0].metadata.columns : [];
        const columns = this.generateColumns(metaCols);

        this.setState({
            columns: columns,
            rows: rows
        });
    }

    generateColumns = (metaCols) => {
        const columns = metaCols.map((col) => {
            let obj = {};
            obj.id = col.id;
            obj.index = col.index;
            obj.title = col.title;

            return obj;
        });

        return columns;
    };


    render() {

        const { rows, columns } = this.state;

        const tableHeader = columns.map((column) => {
            return <th key={column.id}>{column.title}</th>
        });

        const tableCells = rows.map((row, idx1) => {
            return (<tr key={'row_' + idx1}>
                {row.cells.map((cellContent, idx2) => <td key={`cell_${idx1}_${idx2}`}>{cellContent}</td>)}
            </tr>);
        });

        return (
            <table id={this.props.myid} key={this.props.myid}>
                <thead>
                    <tr>
                        {tableHeader}
                    </tr>
                </thead>
                <tbody>
                    {tableCells}
                </tbody>
            </table>
        );
    }
}

As you can see, I have put that code in componentDidUpdate because I was not getting it to work at all when putting it in the componentDidMount method of the child.

It seems strange to me to put it in componentDidUpdate, and I don't know if that is correct.

The second problem I am having is that it renders fine the first time I visit the page, but if I go to another page (within react-router) and then come back, the data in the table cells is gone. That could be either because I am implementing the life cycle wrong, or because there is something wrong with my keys... I don't know.

UPDATE:

This is my routing code:

class KnowledgeSets extends Component {

    render() {

        return (
            <Router>
                <main>
                    <TopMenu />
                    <Switch>
                        <Route exact path="/" render={()=><Home {...this.props} />} />
                        <Route path="/example" render={()=><Example {...this.props} />} />
                        <Route path="/htmltablecomponent" render={()=><Table {...this.props} />} />
                        <Route path="/complexexpandable" render={()=><ComplexExpandable {...this.props} />} />
                        <Route path="/newdataviewer" render={()=><NewDataViewer {...this.props} />} />
                        <Route path="/newhtmltablecomponent" render={()=><NewTable {...this.props} />} />
                        <Route path="/search" render={()=><Search {...this.props} />} />
                    </Switch>
                </main>
            </Router>
        );
    }
}

export default KnowledgeSets;

And if I change the child component to...

componentDidMount() {

    console.log(this.props.dataSet);

    this.createTableData();
}

the logged output is {}. It never gets updated even though the sate of the parent has changed.

Winter
  • 2,407
  • 1
  • 19
  • 28
  • You are indirectly updating the state in `componentDidUpdate()` that means it will run race condition eventually ending up UI to lag and break. So avoid setting/updating state or any side effects in `componentDidUpdate()` hook – Meet Zaveri Sep 05 '18 at 09:29
  • Thanks, like I said, it seems wrong to put it there, but I don't understand what the correct way to do it is. – Winter Sep 05 '18 at 09:31
  • Yeah as I said, only standard way to do side-effects/API calls is `componentDidMount()` . In `componentDidUpdate()` you can check if props/state has been changed in respective to previous one, so that you can know and perform route change operation or just watch it! – Meet Zaveri Sep 05 '18 at 09:34
  • From my experience, I don't think `componentDidMount` would be the problem here. It should most likely be an issue with the way the parent component is mounted on the route. Could you share route code as well? – hazardous Sep 05 '18 at 09:35
  • Oh and please revert back to `componentDidMount`, your guts are telling you the truth, that's the correct way and you would never want to `setState` in `cDU` except for very specific cases. – hazardous Sep 05 '18 at 09:36
  • I have updated with the routing code. – Winter Sep 05 '18 at 09:45

3 Answers3

1

React knows that the props of your child component have changed, and will re-render the component for you. You don't need to use any lifecycle methods or state in the child. Just regenerate rows and columns from the props inside the child's render method and use them right away.

Jed Richards
  • 12,244
  • 3
  • 24
  • 35
  • 1
    Okay, so just get rid of the `componentDidMount` in the child component. Simple :) It worked like a charm. – Winter Sep 05 '18 at 11:25
1

Your parent component is ok but you have to remember that setState is async. First call to render will pass empty object. This is common issue with rendering fetched data. setState updates state and forces rerendering - second render with updated data.

Child can be aware of changing props or not. If not then prevent rendering child until data will be ready. In this case componentDidUpdate makes the child aware of prop changes. It's ok and it's ok to call this.createTableData(); in componentDidMount(not invoked on initial render) as you updated.

Note: componentDidMount will be called only once.

If you still have issues then first console.log in parent render to check if it's called twice and what is passed down. The same for child render.

You can use shouldComponentUpdate instead componentDidUpdate (and createTableData simply called from render). Preparing data is needed as optimalization method - when not all data is changed.

You can insert logging everywhere to track what, when and arguments (data) of lifecycles.

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

You should mount the child component only when your parent state is set in order to have child props initialized correctly (then your child component can use them properly in componentDidMount()).

Here's how you can render your component with an async initial state :

class NewTable extends Component {
  constructor(props) {
    super(props);
    this.state = {
      dataSet: {},
      loading: true
    };
  }

  componentDidMount() {
    const dataSet = DataSet.getColorDataSet();
    this.setState({
      dataSet,
      loading: false
    });
  }

  render() {
    if (this.state.loading) {
      return (<div> Loading </div>)
    }
    return (
      <div className="example">
        <h2>
        HTML Table
        </h2>
        <NewKSParser.HTMLTable dataSet={this.state.dataSet} id="myid" />
      </div>
    );
  }
}
Dyo
  • 4,429
  • 1
  • 15
  • 30
  • Thanks for the example. This would probably be the way to go if my child component was more complex. – Winter Sep 05 '18 at 11:26