I’ve come to a halt making this covid19 app where I can see a list of countries on the left side of the screen with the option of adding any number of countries to the right side of the screen, which displays more covid data of the added country. I’m also kinda new to React.
Problem is, when I click the add button the added state is updated, and it displays that added country on the right side of the screen. But, when I try adding another country I get an error. I believe the error is somewhere around when I try to setState({ state }) in the addCountry method from within App.js.
In other words, the ‘added’ state is only letting itself hold no more than one array element. Help much much much appreciated. I posted all the code.
index.js
import ReactDOM from 'react-dom'; import 'bootstrap/dist/css/bootstrap.min.css'; import './index.css'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root'));
App.js
import CountryList from "./components/CountryList.js"; import Find from "./components/Find.js"; import Added from "./components/Added.js"; class App extends Component { constructor() { super(); this.state = { countries: [], inputbox: [], added: [], }; } // Arrow functions capture "this" when they are defined, while standard functions do when they are executed. // Thus, no need for the bind method. Awesome. handleChange = (e) => this.setState({ inputbox: e.target.value, }); getCountryData = async (slug) => { const resp = await fetch(`https://api.covid19api.com/live/country/${slug}`); var addedData = await resp.json(); // Api returns most days of covid, per country, that it tracks // Thus, we want the last tracked day of a country addedData = addedData[addedData.length - 1]; return addedData; }; // Add a country to the added state // Call when user clicks button associated with their desired country addCountry = async (btnId) => { const { countries, added } = this.state; var addedData = await this.getCountryData(btnId); countries.map((country) => { // If the button ID is equal to the current country in the loops' Slug if (btnId == country.Slug) { try { added.push([ { addedCountry: addedData.Country, confirmedTotal: addedData.Confirmed, deathsTotal: addedData.Deaths, recoveredTotal: addedData.Recovered, activeTotal: addedData.Active, }, ]); // (bug) IT IS PUSHING, BUT ITS NOT SETTING THE STATE! // ITS ONLY LETTING ME KEEP ONE ITEM IN THE STATE this.setState({ added }); console.log(added); } catch (error) { alert(`Sorry, country data not available for ${country.Country}`); return; } } }); }; removeCountry = (btnId) => { const { added } = this.state; added.map((added, index) => { //console.log(added[index].addedCountry); if (btnId == added[index].addedCountry) { added.splice(index, 1); this.setState({ added: added }); } else { console.log("not removed"); return; } }); }; // Mount-on lifecycle method async componentDidMount() { const resp = await fetch("https://api.covid19api.com/countries"); const countries = await resp.json(); // parsed response this.setState({ countries }); // set state to parsed response } render() { // Filter out countries depending on what state the inputbox is in const { countries, inputbox } = this.state; const filtered = countries.filter((country) => country.Country.includes(inputbox) ); return ( <div className="App Container"> <Find placeholder="Type to find a country of interest..." handleChange={this.handleChange} /> <div className="row"> <CountryList countries={filtered} addCountry={this.addCountry} /> <Added added={this.state.added} removeCountry={this.removeCountry} /> </div> </div> ); } } export default App;
Added.js import React, { Component } from "react"; import { Table, Form, Input, Button } from "reactstrap"; import AddedCountry from "./AddedCountry.js"; class Added extends Component { constructor(props) { super(props); } render() { return ( <div className="col-md-6"> <Table> <thead> <tr> <th scope="col">#</th> <th scope="col">Country</th> <th scope="col">Active</th> <th scope="col">Confirmed Total</th> <th scope="col">Recovered</th> <th scope="col">Deaths</th> <th scope="col">Action</th> </tr> </thead> {this.props.added.map((added, index) => ( <AddedCountry added={added[index]} removeCountry={this.props.removeCountry} /> ))} </Table> </div> ); } } export default Added;
AddedCountry.js import React, { Component } from "react"; import { Table, Form, Input, Button } from "reactstrap"; class AddedCountry extends Component { constructor(props) { super(props); } render() { return ( <tbody> <tr> <td></td> <td>{this.props.added.addedCountry}</td> <td>{this.props.added.activeTotal}</td> <td>{this.props.added.confirmedTotal}</td> <td>{this.props.added.recoveredTotal}</td> <td>{this.props.added.deathsTotal}</td> <td> { <Button onClick={() => this.props.removeCountry( document.getElementById(this.props.added.addedCountry).id ) } id={this.props.added.addedCountry} type="submit" color="danger" size="sm" > Remove </Button> } </td> </tr> </tbody> ); } } export default AddedCountry;
CountryList.js import React, { Component } from "react"; import { Table, Form, Input, Button } from "reactstrap"; import Country from "./Country.js"; class CountryList extends Component { constructor(props) { super(props); } render() { return ( <div className="col-md-6"> <Table> <thead> <tr> <th scope="col">#</th> <th scope="col">Country</th> <th scope="col">Actions</th> </tr> </thead> { // Each country is a component // Function will display all countries as the Map function loops through them this.props.countries.map((country) => ( <Country countries={country} addCountry={this.props.addCountry} /> )) } </Table> </div> ); } } export default CountryList;
Country.js import React, { Component } from "react"; import { Table, Form, Input, Button } from "reactstrap"; class Country extends Component { constructor(props) { super(props); } render() { return ( <tbody> <tr> <td></td> <td>{this.props.countries.Country}</td> <td> { <Button onClick={() => this.props.addCountry( document.getElementById(this.props.countries.Slug).id ) } id={this.props.countries.Slug} type="submit" color="success" size="sm" > Add </Button> } </td> </tr> </tbody> ); } } export default Country;
Find.js import React, { Component } from "react"; import { Table, Form, Input, Button } from "reactstrap"; class Find extends Component { constructor(props) { super(props); } render() { return ( <div className="Find container"> <br /> <Form> <div className="form-row"> <div className="form-group col-md-6"> <h3>Find a Country</h3> <Input type="text" className="form-control" id="country" placeholder={this.props.placeholder} onChange={this.props.handleChange} ></Input> </div> </div> </Form> </div> ); } } export default Find;
Advertisement
Answer
I haven’t pored over all that code, but focusing right where you think the issue is it is obvious you are mutating your state object by pushing directly into the added
array.
Solution
Don’t mutate state!
Since it seems you only want to add a single new “add” and only when the button’s btnId
matches a country’s slug, and the btnId
can only ever be a valid value from the mapped countries
array, I think this can be greatly simplified.
addCountry = async (btnId) => { const addedData = await this.getCountryData(btnId); if (addedData) { this.setState(prevState => ({ added: prevState.added.concat({ // <-- concat creates a new array reference addedCountry: addedData.Country, confirmedTotal: addedData.Confirmed, deathsTotal: addedData.Deaths, recoveredTotal: addedData.Recovered, activeTotal: addedData.Active, }), })); } else { alert(`Sorry, country data not available for ${country.Country}`); } };
Similarly the removeCountry
handler is mis-using the array mapping function and mutating the added
state. Array.prototype.filter
is the idiomatic way to remove an element from an array and return the new array reference.
removeCountry = (btnId) => { this.setState(prevState => ({ added: prevState.added.filter(el => el.addedCountry !== btnId), })); };
Additional Issues & Suggestions
Added.js
If you maintain the added
array as a flat array (not an array of arrays) then it’s trivial to map the values.
{this.props.added.map((added) => ( <AddedCountry key={added} added={added} removeCountry={this.props.removeCountry} /> ))}
Country.js & AddedCountry.js
I don’t see any reason to query the DOM for the button id when you are literally right there and can enclose the country slug in the onClick
callback.
<Button onClick={() => this.props.addCountry(this.props.countries.Slug)} id={this.props.countries.Slug} type="submit" color="success" size="sm" > Add </Button> <Button onClick={() => this.props.removeCountry(this.props.added.addedCountry)} id={this.props.added.addedCountry} type="submit" color="danger" size="sm" > Remove </Button>
App.js
This may or may not matter, but it is often the case to do case-insensitive search/filtering of data. This is to ensure something like “France” still matching a user’s search input of “france”.
const filtered = countries.filter((country) => country.Country.toLowerCase().includes(inputbox.toLowerCase()) );