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!