Couch
Couch

Reputation: 307

React component not re-rendering on setState

tldr; version: parent not re-rendering after setState called...

I have a dashboard view that renders multiple components. The child components that load data work just fine. But I have another section of the dashboard where I plan to load multiple versions of the same component using the same dataset. Therefore, instead of making 6 api calls for the same dataset, I'm pulling the data in via the parent and sending it down to the children. Also I am using route parameters to signal which company to look up.

The components that contain their own api calls work great, functioning fine with the route params. It's the component where I'm making the api call in the parent where I have the issue. The parent is rendering, then the child, then the api call finishes and sets the state, but isn't reloading. So as the route parameter changes, the component shows the data of the previous click (essentially is always one behind).

I've tried changing which react events to tie it to, I've using mapping functions, made sure that "this" is the "this" that I want (it is)... no dice. I know it's updating the state correctly, and if I forceUpdate() the correct data shows in the component (but I can't do this it would force me to add forceUpdate() into a react update method which will cause it to constantly reload).

Any ideas?? Here's some stripped-down/obfuscated code:

Parent

...
class ClientDash extends Component {
    constructor(props) {
        super(props);
        this.state = { chartOptions: {}, data1: {}, data2: {}, data3: {}, data4: {}, data5: {}, data6: {}, loaded: false };
    }

    loadData(clientID) {
        var currYear = moment().year();
        var chartData = {
            labels: [(currYear-4).toString(), (currYear-3).toString(), (currYear-2).toString(), (currYear-1).toString(), currYear.toString()],
            datasets: [{
                type: 'bar',
                label: 'Cat1',
                backgroundColor: this.props.colors[0].borderColor,
                borderColor: 'white',
                borderWidth: 3,
                data: [null, null, null, null, null]
            }, {
                type: 'bar',
                label: 'Cat2',
                backgroundColor: this.props.colors[1].borderColor,
                borderColor: 'white',
                borderWidth: 3,
                data: [null, null, null, null, null]
            }, {
                type: 'bar',
                label: 'Cat3',
                backgroundColor: this.props.colors[2].borderColor,
                borderColor: 'white',
                borderWidth: 3,
                data: [null, null, null, null, null]
            }, {
                type: 'bar',
                label: 'Cat4',
                backgroundColor: this.props.colors[3].borderColor,
                borderColor: 'white',
                borderWidth: 3,
                data: [null, null, null, null, null]
            }]
        };

        var chartOptions = {
            tooltips: {
                mode: 'index',
                intersect: true
            },
            responsive: true
        };

        
        axios(apiCall)
            .then(d => {
                var data = d.data;
                var data1 = _.clone(chartData),
                    data2 = _.clone(chartData),
                    data3 = _.clone(chartData),
                    data4 = _.clone(chartData),
                    data5 = _.clone(chartData),
                    data6 = _.clone(chartData);

                
                /* some data manipulation */

                console.log('loading data');
                console.log(this);
                this.setState({ chartOptions: chartOptions, data1: data1, data2: data2, data3: data3, data4: data4, data5: data5, data6: data6, loaded: true });
                // this.forceUpdate();
            }).then()
            .catch(function (error) {
                console.log(error);
            })
    }

    componentWillUpdate(nextProps) {
        this.initComp(nextProps.params.clientID);
    }

    shouldComponentUpdate(nextProps) {
        return nextProps.params.clientID != this.props.params.clientID;
    }

    componentWillMount() {
        this.initComp(this.props.params.clientID);
    }

    render() {
        console.log('parent rendering')
// removed all unnecessary code below
        return (
            <div className="wrapper wrapper-content animated fadeIn">
                <div className="row">
                    <div className="col-lg-4">
                        <div className="ibox">
                            <div className="ibox-content">
                                <MetricsColumnChart metricsData={this.state.data1} options={this.state.chartOptions} />
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        )
    }

}

/**
 * Map the state to props.
 */
const mapStateToProps = (state) => ({
  ...state
});

/**
 * Map the actions to props.
 */
const mapDispatchToProps = (dispatch) => {
    return bindActionCreators(Actions, dispatch)
};


/**
 * Connect the component to
 * the Redux store.
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ClientDash)

Child

import React, { Component } from 'react';
import {Bar} from 'react-chartjs-2';
var _ = require('lodash');

class MetricsColumnChart extends Component {
    render() {
        console.log('child rendering')
        return(
            <Bar data={this.props.metricsData} options={this.props.options} />
        )
    }
}

export default MetricsColumnChart

Here's what the console looks like. As you can see, no re-render and the "this" is the correct "this": enter image description here

Upvotes: 2

Views: 6099

Answers (1)

Sammy I.
Sammy I.

Reputation: 2725

The problem lies in your shouldComponentUpdate. Right now, it only returns true when the props of the new clientID is different than the current clientID. So, when you run this.setState() at the end of your API call, the component does not update because you are not receiving new props.

Basically,

  1. Receive initial props
  2. componentWillMount runs
  3. Start API call (async)
  4. componentWillMount finishes
  5. Initial render
  6. Finish API call
  7. Set state (this.setState)
  8. shouldComponentUpdate? No new props, therefore no update
  9. Receive new props
  10. shouldComponentUpdate? Yes new props, therefore yes update
  11. componentWillUpdate runs
  12. API call starts (async)
  13. componentWillUpdate finishes
  14. New render (using currently available state)
  15. API call is done
  16. Set new state
  17. shouldComponentUpdate? No new props, therefore no update

There are two ways to fix this:

  1. Remove shouldComponentUpdate so that React decides when the component updates (when you run this.setState or receive new updates)
  2. Follow Tyler Sebastian's comment, and add a second argument to shouldComponentUpdate that is nextState, and compare the old and new state to determine when the component will update.

Incidentally, I see you have your API call in componentWillMount. There are some good reasons why you should use componentDidMount isntead.

Upvotes: 6

Related Questions