Skip to content

React holds state of no more than one array element

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;

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())
);