0

I'm making a fetch request to this API and I'm successfully getting the data and printing it to the console. However I'm new to asynchronous calls in Javascript/React. How do add async/await in this code to delay the render upon successful fetch? I'm getting the classic Uncaught (in promise) TypeError: Cannot read property '0' of undefined because I believe that the DOM is trying to render the data that hasn't been fully fetched yet.

import React, { useEffect, useState } from "react";

export default function News() {
  const [error, setError] = useState(null);
  const [isLoaded, setIsLoaded] = useState(false);
  const [stories, setStory] = useState([]);

  useEffect(() => {
    fetch(
      "http://api.mediastack.com/v1/news"
    )
      .then((res) => res.json())
      .then(
        (result) => {
          setIsLoaded(true);
          setStory(result);
          console.log(result.data[0]);       // printing to console to test response
          console.log(result.data[0].title); // printing to console to test response
        },
        (error) => {
          setIsLoaded(true);
          setError(error);
        }
      );
  }, []);

  if (error) {
    return <div>Error: {error.message}</div>;
  } else if (!isLoaded) {
    return <div>Loading...</div>;
  } else {
    return (
      <div>
        <p>{stories.data[0].title} </p> // this is the where render error is
      </div>
    );
  }
}
cdt
  • 193
  • 1
  • 2
  • 17

4 Answers4

1

async/await is just another form to retrieve asynchronous data, like you are doing with then.

The message:

 Cannot read property '0' of undefined

means that 'result.data' is undefined.

Anyway, if it entered the "then" callback it always means that the request was fetched, there is no such thing as "half fetched" or "fully fetched".

I suggest you to use the debugger, by placing

debugger;

right in the beginning of the 'then' callback to ensure what your result is. or you may also console.log the result.

Just to clarify:

myFunctionThatReturnsPromise.then(response => //do something with response)

is the same as

await response = myFunctionThatReturnsPromise;

You might consider using stories?.data?.[0]?.title to fix this problem.

Eduardo Sousa
  • 875
  • 10
  • 22
  • I know it's fetched the data because it appears in my console. However, I need help writing the async/await part because it's trying to render the

    tag with data that's not there yet.

    – cdt Feb 17 '21 at 20:46
  • maybe if you use the question mark operator, like: stories?.data?.[0]?.title – Eduardo Sousa Feb 17 '21 at 20:53
1

The problem is that your isLoaded state variable is updated BEFORE stories, despite the fact you set the former state first. Here is how to fix this:

import { useEffect, useState } from "react";

export default function App() {
  const [error, setError] = useState(null);
  const [stories, setStory] = useState(null);

  useEffect(() => {
    fetch("your_url")
      .then((res) => return res.json())
      .then((result) => {
        setStory(result);
        console.log("Success ", result);
      })
      .catch((error) => {
        console.log("Error", error);
      });
  }, []);

  if (error) {
    return <div>Error: {error.message}</div>;
  } else if (!stories) {
    return <div>Loading...</div>;
  } else {
    return (
      <div>
        <p>stories.data[0].title} </p>
      </div>
    );
  }
}

Get rid of the isLoaded var altogether and use the stories var to indicate that it's being loaded.

If you want to add artificial load time to your api call. You can do something like this:

  useEffect(() => {
    fetch("your_url")
      .then((res) => return res.json())
      .then((result) => {
        setTimeout(() => setStory(result), 2000)
      })
      .catch((error) => {
        console.log("Error", error);
      });
  }, []);

This will add 2 seconds before setting your state thus letting you see what your loader looks like.

codemonkey
  • 7,325
  • 5
  • 22
  • 36
0

your error will not be gone with async await because you are calling a nested empty object which has no value. render method in react is prior to the useEffect you have used. you can approach two solutins:

1.use optional chaining es20 feature like this:

<p>{stories?.data?.[0]?.title} </p> 

2.use nested if statement before the p tag to check whether it has data or not:

it seems the first option is much cleaner

Ehsan
  • 188
  • 12
-1

You already have async code using Promise.then so async/await will not help you here - it's just a different way of writing Promise-based functionality.

Your problem is just as you say - React is trying to render your else block before stories has anything for you to render. I think you just need to write stricter conditions, e.g.

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  if (!isLoaded || !stories.data.length) {
    return <div>Loading...</div>;
  }
  
  return (
    <div>
      <p>{stories.data[0].title} </p> // this is the where render error is
    </div>
  );
codeth
  • 557
  • 3
  • 7