cdt
cdt

Reputation: 223

Add Async/Await to React Fetch Request

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

Upvotes: 0

Views: 1597

Answers (4)

Eduardo Sousa
Eduardo Sousa

Reputation: 1075

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.

Upvotes: 1

codemonkey
codemonkey

Reputation: 7915

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.

Upvotes: 1

Ehsan
Ehsan

Reputation: 195

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

Upvotes: 0

codeth
codeth

Reputation: 567

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

Upvotes: -1

Related Questions