Search code examples
javascriptreactjsundefinedrefresh

After refreshing, React props turn undefined


I'm fetching from an API where I want to pass the response object as a prop onto a child component from App.js and use it in my Tags.js file. However, it only works after one time and when I refresh it, it gives me an error saying the props.response.names.length is undefined. I tried using the useEffect function to try and update it but it didn't work. I would appreciate any help.

My App.js file (still some remnants of when you run "npx create-react-app my-app"):

import './App.css';
import Tags from './Tags.js';
import React, { useState, useEffect } from 'react';

function App() {

  const makeRequest = async () => {
    try {
      let response = await fetch('RANDOM_API_URL');
      let json = await response.json();
      setResponse(json);
    } catch (error) {
      console.log(error);
    }
  }

  const [response, setResponse] = useState(makeRequest);

  useEffect(() => {
    setResponse(makeRequest);
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
      <Tags response={response}></Tags>
    </div>
  );
}

export default App;

My Tags.js:

import './App.js';

function Tags(props) {
  const makeButtons = () => {
    let result = [];
    for (let i = 0; i < props.response.names.length; i++) {
      result.push(<button key={i}>hello</button>);
    }
    return result;
  }

  return (
    <div>
      {makeButtons()}
    </div>
  );
}

export default Tags;

Solution

  • Your makeRequest function is async, and sets state internally. The reason you get this bug is that you don't always have a response.names to read length from - you only have an empty response object.

    Either make sure you always have the names array available in state, or avoid rendering your Tags component when names is not present in state.

    Also, try to avoid being creative with your dependency array, it's there for a reason. I see why you didn't include makeRequest in it though, since you create a new function on every render. That's something to keep in mind when you transition from class components to functional components. Class methods are stable across renders, functions declared in a functional component are not. To mimic a class method, you can declare functions using the useCallback hook, but again you need to include the dependency array. In your case, you can just create the async function inside useEffect, and then call it.

    const [response, setResponse] = useState({names: []});
    // or const [response, setResponse] = useState();
    // and then, in Tags:
    // function Tags({response = {names: []}) {
    
    useEffect(() => {
      const makeRequest = async () => {
        try {
          let response = await fetch('RANDOM_API_URL');
          let json = await response.json();
          setResponse(json);
        } catch (error) {
          console.log(error);
        }
      }
      makeRequest();
    }, []);
    

    Supplying a default response prop to Tags will make sure you can read the length of names even before you have a response.

    Looking more closely on your Tags component, I think it should be something like this:

    // don't import App.js here
    
    export default function Tags({response = {names: []}){
      return (
        <div>
          {response.names.map(name => {
            return <button key={name}>Hello {name}</button>
          })}
        </div>
      )
    }
    

    Don't use index as key, that will cause problems if you rearrange the names in your array. And I guess you want to supply an onClick function to your buttons.