0

I'm working on my first React app, which displays a Chart.js chart based on the user's current selection in a dropdown menu. I had it at the point where it could render the respective chart as I changed the selection, but it was using hard coded data. I've since then added an Ajax call to get JSON, but the chart component was rendering before the Ajax response came in. So, I added a check to display "Loading..." while waiting for the JSON data to be set in state. My problem is that my current implementation of rendering the correct chart is conflicting with my implementation of waiting for JSON data before rendering the chart.

After researching, I've found that my problem is most similar to this post, but with multiple chart possibilities instead of one: React render Chart.js based on api response

Here is my code:

App.js

import React, { Component } from 'react';
import $ from 'jquery';
import './App.css';
import Header from './Components/Header';
import Dropdown from './Components/Dropdown';
import BarChart from './Components/BarChart';
import DoughnutChart from './Components/DoughnutChart';
import PieChart from './Components/PieChart';
import LineChart from './Components/LineChart';       
import RadarChart from './Components/RadarChart';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { 
      chartData: {}, 
      chartType: 'Bar', //default to bar chart
      isLoaded: false
    };
    this.handleVisualChange = this.handleVisualChange.bind(this);
  }

  componentDidMount() {
    this.getChartData();  
  }

  handleVisualChange(value) {
    //logging for testing
    console.log('testing value in App.js: ', value);
    //updating the state to the current selected visual
    this.setState({chartType: value});
  }

  getChartData() {
    $.ajax({
      url: 'http://localhost/fetch/getData.php',
      dataType: 'json',
      success: function(dataReturned){
        //logging to test if correct data is being received
        console.log('original data: ', dataReturned);

        //loop through array and build array of categories and array of totals
        var arrCAT = [];
        var arrTOTAL = [];
        for (var i = 0, len = dataReturned.length; i < len; i++) {
          arrCAT.push(dataReturned[i].CAT);
          arrTOTAL.push(dataReturned[i].TOTAL);
        }
        //logging to test that arrays were loaded correctly
        console.log('just categories: ', arrCAT);
        console.log('just totals: ', arrTOTAL);

        this.setState({
          chartData: {
            labels: arrCAT,
            datasets: [
              {
                label: '# of Transactions',
                data: arrTOTAL,
                backgroundColor: [
                  'rgba(255, 99, 132, 0.6)',  //red
                  'rgba(54, 162, 235, 0.6)',  //blue
                  'rgba(255, 206, 86, 0.6)',  //yellow
                  'rgba(75, 192, 192, 0.6)',  //green
                  'rgba(153, 102, 255, 0.6)', //purple
                  'rgba(255, 159, 64, 0.6)',  //orange
                  'rgba(90, 96, 104, 0.6)'    //grey
                ]
              }
            ]
          },
          isLoaded: true
        });
      }.bind(this),
    }); 
  }  

  render() {
    let chartToBeDisplayed = null;
    switch(this.state.chartType) {
      case 'Bar':
        chartToBeDisplayed = <BarChart chartData={this.state.chartData} />;
        break;
      case 'Doughnut':
        chartToBeDisplayed = <DoughnutChart chartData={this.state.chartData} />;
        break;
      case 'Pie':
        chartToBeDisplayed = <PieChart chartData={this.state.chartData} />;
        break;
      case 'Line':
        chartToBeDisplayed = <LineChart chartData={this.state.chartData} />;
        break;
      case 'Radar':
        chartToBeDisplayed = <RadarChart chartData={this.state.chartData} />;
        break;
      case 'All':
        chartToBeDisplayed = <div className="all">
                               <BarChart chartData={this.state.chartData} />
                               <DoughnutChart chartData={this.state.chartData} />
                               <PieChart chartData={this.state.chartData} />
                               <LineChart chartData={this.state.chartData} />
                               <RadarChart chartData={this.state.chartData} />
                             </div>;
        break;
      default:
        console.log('Something went wrong...');
    } 
    return (
      <div className="App">
        <Header />
        <Dropdown onVisualChange={this.handleVisualChange} />
        <div className="ChartArea">
          {this.state.isLoaded ? {chartToBeDisplayed} : <div>Loading...</div>}
        </div>
      </div>
    );
  }
}

export default App;

Dropdown.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class Dropdown extends Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  static defaultProps = {
    visuals: ['Bar', 'Line', 'Pie', 'Doughnut', 'Radar', 'All']
  }

  handleChange(event) {
    //logging for testing
    console.log('testing value in Dropdown.js: ', event.target.value);
    this.props.onVisualChange(event.target.value);
  }

  render() {
    let visualOptions = this.props.visuals.map(visual => {
      return <option key={visual} value={visual}>{visual}</option>
    });
    return (
      <div className="dropdown">
        <label>Visuals</label><br />
          <select id="soflow" ref="visual" value={this.props.value} onChange={this.handleChange}>
            {visualOptions}
          </select>
      </div>
    );
  }
}

Dropdown.propTypes = {
  onVisualChange: PropTypes.func,
  visuals: PropTypes.array,
  value: PropTypes.string
};

export default Dropdown;

BarChart.js (the other chart components are identical except for chart type)

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {Bar} from 'react-chartjs-2';

class BarChart extends Component {
  constructor(props) {
    super(props);
    this.state = {
      chartData: props.chartData
    }
  }

  static defaultProps = {
    displayTitle: true,
    displayLegend: true,
    legendPosition: 'bottom',
  }

  render() {
    return (
      <div className="chart">
        <Bar
          data={this.state.chartData}
          options={{
            title: {
            display: this.props.displayTitle,
            text: 'Transactions by Category',
            fontSize: 25
            },
            legend: {
            display: this.props.displayLegend,          
            position: this.props.legendPosition 
            }
          }}
        />
      </div>
    )   
  }
}

BarChart.propTypes = {
  chartData: PropTypes.object,
  displayTitle: PropTypes.bool,
  displayLegend: PropTypes.bool,
  legendPosition: PropTypes.string
};

export default BarChart;

Current Error:

Objects are not valid as a React child (found: object with keys {chartToBeDisplayed}). If you meant to render a collection of children, use an array instead.
    in div (at App.js:112)
    in div (at App.js:109)
    in App (at index.js:7)

In App.js, I know that {this.state.isLoaded ? {chartToBeDisplayed} : <div>Loading...</div>} is incorrect, but this was my first attempt at merging the two techniques that I had. My second attempt was to replace {chartToBeDisplayed} with a generic chart component (<Chart />) and place the switch(this.state.chartType) inside of that component's render(), like so:

Chart.js

import React, {Component} from 'react';
import BarChart from './BarChart';
import DoughnutChart from './DoughnutChart';
import PieChart from './PieChart';
import LineChart from './LineChart';
import RadarChart from './RadarChart';
import PropTypes from 'prop-types';
import {Bar, Line, Pie, Doughnut, Radar} from 'react-chartjs-2';

class Chart extends Component {
  constructor(props) {
    super(props);
    this.state = {
      chartData: props.chartData,
      chartType: props.chartType
    }
  }

  render() {
    let chartToBeDisplayed = null;
    switch(this.state.chartType) {
      case 'Bar':
        chartToBeDisplayed = <BarChart chartData={this.state.chartData} />;
        break;
      case 'Doughnut':
        chartToBeDisplayed = <DoughnutChart chartData={this.state.chartData} />;
        break;
      case 'Pie':
        chartToBeDisplayed = <PieChart chartData={this.state.chartData} />;
        break;
      case 'Line':
        chartToBeDisplayed = <LineChart chartData={this.state.chartData} />;
        break;
      case 'Radar':
        chartToBeDisplayed = <RadarChart chartData={this.state.chartData} />;
        break;
      case 'All':
        chartToBeDisplayed = <div className="all">
                               <BarChart chartData={this.state.chartData} />
                               <DoughnutChart chartData={this.state.chartData} />
                               <PieChart chartData={this.state.chartData} />
                               <LineChart chartData={this.state.chartData} />
                               <RadarChart chartData={this.state.chartData} />
                             </div>;
        break;
      default:
        console.log('Something went wrong...');
    } 
    return (
      <div className="chart">
        {chartToBeDisplayed}
      </div>
    );  
  }
}

Chart.propTypes = {
  chartData: PropTypes.object,
  chartType: PropTypes.string,
};

export default Chart;

With this implementation, it would change App.js to this:

import React, { Component } from 'react';
import $ from 'jquery';
import './App.css';
import Header from './Components/Header';
import Dropdown from './Components/Dropdown';
import Chart from './Components/Chart';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { 
      chartData: {}, 
      chartType: 'Bar',
      isLoaded: false
    };
    this.handleVisualChange = this.handleVisualChange.bind(this);
  }

  componentDidMount() {
    this.getChartData();  
  }

  handleVisualChange(userSelection) {
    //logging for testing
    console.log('testing value in App.js: ', userSelection);
    //updating the state to the current selected visual
    this.setState({chartType: userSelection});
  }

  getChartData() {
    $.ajax({
      url: 'http://localhost/test/fetch.php',
      dataType: 'json',
      success: function(dataReturned){
        //logging to test if correct data is being received
        console.log('original data: ', dataReturned);

        //loop through array and build array of categories and array of totals
        var arrCAT = [];
        var arrTOTAL = [];
        for (var i = 0, len = dataReturned.length; i < len; i++) {
          arrCAT.push(dataReturned[i].CAT);
          arrTOTAL.push(dataReturned[i].TOTAL);
        }
        //logging to test that arrays were loaded correctly
        console.log('just categories: ', arrCAT);
        console.log('just totals: ', arrTOTAL);

        this.setState({
          chartData: {
            labels: arrCAT,
            datasets: [
              {
                label: '# of Transactions',
                data: arrTOTAL,
                backgroundColor: [
                  'rgba(255, 99, 132, 0.6)',  //red
                  'rgba(54, 162, 235, 0.6)',  //blue
                  'rgba(255, 206, 86, 0.6)',  //yellow
                  'rgba(75, 192, 192, 0.6)',  //green
                  'rgba(153, 102, 255, 0.6)', //purple
                  'rgba(255, 159, 64, 0.6)',  //orange
                  'rgba(90, 96, 104, 0.6)'    //grey
                ]
              }
            ]
          },
          isLoaded: true
        });
      }.bind(this),
    }); 
  }  

  render() {
    return (
      <div className="App">
        <Header />
        <Dropdown onVisualChange={this.handleVisualChange} />
        <div className="ChartArea">
          {this.state.isLoaded ? <Chart chartData={this.state.chartData} chartType={this.state.chartType} /> : <div>Loading...</div>}
        </div>
      </div>
    );
  }
}

export default App;

However, I've also been unable to get this to work. React Developer Tools allows me to see that the state is changing correctly when I change the selection in the dropdown, but it is not rendering the new chart selection. It simply stays at the default, which is BarChart.

Bottom Line: I'm unsure if I'm close to achieving this, but am missing a few minor details, or if I need to make major changes to how I render the correct chart? The switch statement implementation doesn't feel right, but it was working really well on its own. As I've stated, I'm new to working with React and would like some direction/clarification. Thank you!

Kronaka
  • 13
  • 1
  • 5

1 Answers1

0

For your first trial, try to remove curly brackets at chartToBeDisplayed.

{this.state.isLoaded ? chartToBeDisplayed : <div>Loading...</div>}

As long as the grammar of your Chart is correct, it should work.


For your second trial.

Chart.js only set state at initial mount.

Thus, when you update state on App.js and pass new props - chartType to Chart.js, you need to update the local state.

Try to add componentWillReceiveProps at Chart.js component.

class Chart extends Component {
  constructor(props) {
    ...
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.chartType !== this.state.chartType) {
      this.setState({chartType: nextProps.chartType})
    }
  }
}

Updated

For preventing misunderstand, Chart.js mentioned above means the Chart.js file posted on question.

Chen-Tai
  • 3,435
  • 3
  • 21
  • 34
  • I've implemented both solutions and they are both working nicely. However, do you have a recommendation on which implementation is better? I intend to keep developing this app into a dashboard so it will grow in size and complexity. I'm leaning towards the second implementation, but would like to hear your thoughts @Chen-Tai Hou – Kronaka Nov 28 '17 at 18:49
  • For me, second method is better, because you can keep chart api the same. It seems more easily to maintain. – Chen-Tai Nov 28 '17 at 18:56