0

I'm making an React application that involves fetching an API. The code is below, but basically I'm fetching the data from the API and then mapping over it to present information about each character. It works with the class component, but I tried to change it to a function component and it no longer works. There is no error, it simply doesn't display anything.

From using console.log(), I know that I'm successfully getting the information into the "characters" variable from the API when using the function component, and from comparing the two applications and using console.log() on "this.state.characters" (class) and "characters" (function), I'm pretty sure that getting the exact same data, an array of like 660 of the characters from the show where each character's information is inside of an object. When I tried to add paragraph tags with {characters[0].name} inside of the function component, I got the error "TypeError: Cannot read property 'name' of undefined".

I don't know if it's because I messed up something stupid or because I don't understand something about some detail about the difference between class and function components, either is obviously very possible. Thank you for any help.

Here is the code from the class component:

import React, { Component } from 'react';

export class Body extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
          characters: [],
          nameInput: '',
          locationInput: '',
          loading: true
        };
    };

async componentDidMount() {
    let url = 'https://rickandmortyapi.com/api/character/';
    let array = [];
    for (let i = 1; i < 34; i++) {
        let response = await fetch(url);
        let data = await response.json();
        for (let j = 0; j < 20; j++) {
        array.push(data.results[j]);
        }
        url = data.info.next;
    }
    this.setState({characters: array, loading: false}, () => console.log(this.state.characters));
}

readInput = (e) => {
    this.setState({nameInput: e.target.value});
    console.log(this.state.nameInput);
}

readLocationInput = (e) => {
    this.setState({locationInput: e.target.value});
    console.log(this.state.locationInput);
}
render() {
    return (
        <div className="all">
          <h4>Search by name:</h4>
          <input onChange={this.readInput} />
          <h4>Search by location:</h4>
          <input onChange={this.readLocationInput} />
          <br />
          <div className="row m-1">
          
          {this.state.loading ? 'Loading can take a few seconds. Your Rick and Morty experience will be ready soon!' : this.state.characters.filter((item) => {
            if (this.state.nameInput == "") {
              return item;
            } else {
              if (item.name.toLowerCase().includes(this.state.nameInput.toLowerCase())) {
                return item;
              }
            }
          }).filter((item) => {
            if (this.state.locationInput == "") {
              return item;
            } else {
              if (item.location.name.toLowerCase().includes(this.state.locationInput.toLowerCase())) {
                return item;
              }
            }
          }).map((item, id) => {
            return (
            <>
                <div className="col-md-4 border border-dark rounded" id="square">
                <h2>{item.name}</h2>
                <img src={item.image} className="border rounded" />
                <h4>Location: {item.location.name}</h4>
                <h4>Status: {item.status}</h4>
                </div>
            </>
            )
          })}
          </div>
        </div>
      );
}
};

Here is the code from the function component:

import React, { Component, useEffect, useState } from 'react';
import logo from '../rickandmortylogo.png';

export const Body = () => {
    const [characters, setCharacters] = useState([]);
    const [nameInput, setNameInput] = useState('');
    const [locationInput, setLocationInput] = useState('');
    const [loading, setLoading] = useState(true);

useEffect(() => {
    let url = 'https://rickandmortyapi.com/api/character/';
    let array = [];
    const fetchAPI = async () => {
      for (let i = 1; i < 34; i++) {
        let response = await fetch(url);
        let data = await response.json();
        for (let j = 0; j < 20; j++) {
          array.push(data.results[j]);
        }
        url = data.info.next;
      }
    }
    fetchAPI();
    setCharacters(array);
    setLoading(false);
}, []);

const readInput = (e) => {
    setNameInput(e.target.value);
    console.log(nameInput);
}

const readLocationInput = (e) => {
    setLocationInput(e.target.value);
    console.log(locationInput);
}

return (
      <>
      <div className="text-center">
        <img src={logo} className="img-fluid" />
      </div>
      <h2>Click on a character here to add them to your favorites. Choose "Check Favorites" in the menu bar to see your favorites and "Search Characters" to come back.</h2>
        <div className="all">
          <h4>Search by name:</h4>
          <input onChange={readInput} />
          <h4>Search by location:</h4>
          <input onChange={readLocationInput} />
          <br />
          <div className="row m-1">
            {loading ? 'Loading can take a few seconds. Your Rick and Morty experience will be ready soon!' : characters.filter((item) => {
              if (nameInput == "") {
                return item;
              } else {
                if (item.name.toLowerCase().includes(nameInput.toLowerCase())) {
                  return item;
                }
              }
            }).filter((item) => {
              if (locationInput == "") {
                return item;
              } else {
                if (item.location.name.toLowerCase().includes(locationInput.toLowerCase())) {
                  return item;
                }
              }
            }).map((item, id) => {
              return (
              <>
                  <div className="col-md-4 border border-dark rounded" id="square">
                  <h2>{item.name}</h2>
                  <img src={item.image} className="border rounded" />
                  <h4>Location: {item.location.name}</h4>
                  <h4>Status: {item.status}</h4>
                  </div>
              </>
              );
            })}
          </div>
        </div>
        </>
);
};
phershbe
  • 169
  • 4
  • 12
  • 1
    You need to move `setCharacters(array)` inside the `fetchAPI` function. `setLoading(false)` should probably go in there too – Phil Sep 07 '21 at 23:19
  • @Phil Yeah that worked, thank you. I don't understand the logic behind why it has to be inside of there though. Is it because otherwise the asynchronous call is not available yet? Also, when I created a button to log out "characters" and clicked it, that information was available to me, so why was it available there and not when I tried to map it? – phershbe Sep 07 '21 at 23:30
  • Does this answer your question? [Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference](https://stackoverflow.com/questions/23667086/why-is-my-variable-unaltered-after-i-modify-it-inside-of-a-function-asynchron) – Phil Sep 07 '21 at 23:33
  • `await fetchAPI();` otherwise it'll be always loaded. – windmaomao Sep 07 '21 at 23:40
  • 1
    @Phil Yeah that helps a lot, thank you. From what I understand now, the problem is because I'm setting those outside of the asynchronous call, which I'm not doing in the class component, so the the loading is getting set as false before the array is filled, so the data isn't ready. – phershbe Sep 07 '21 at 23:55
  • @windmaomao can't do that since the `useEffect` callback is not (and should not be) `async` – Phil Sep 08 '21 at 00:05
  • your logic is this, `fetch().then(do)`, not `fetch` and `do`. Fetch is an async, `setState` has to wait till the op finishes. Unless you are not trying to do that. – windmaomao Sep 08 '21 at 12:23

0 Answers0