TypeError: Cannot read property 'url' of undefined for JSONPlaceholder

Viewed 814

I am trying to load pictures from jsonPlaceholder. When I console.log out the images array, it works successfully. The moment I try to call the URL from the element, it throws a TypeError. I tried to do it the asynchronous way, even this doesn't work. Is there any workaround for this? What possibly is the problem?

import React, {Component} from 'react';
import axios from 'axios';

class Home extends Component{



    state = {
                posts : [],
                images : []

            };

    componentDidMount =  async () => {
        const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
            this.setState({posts: res.data.slice(0,10)})

        const res2 = await axios.get('https://jsonplaceholder.typicode.com/photos');
            this.setState({images: res2.data.slice(0,10)})

        }


    render = () => {    


        const {posts} = this.state.posts;
        const {images} = this.state.images;

        const PostList = this.state.posts.length ? (


            this.state.posts.map((post,index) => {
                return(
                    <div className = "post card" key = {post.id}>
                            <div className = "card-content">
                                <div className = "card-title">
                                    {post.title}
                                </div>
                                <div className>
                                    {(this.state.images[index].url)} //console.log(this.state.images[index] works, but .URL doesn't 
                                </div>
                                <div>
                                    <p>{post.body}</p>
                                </div>
                            </div>
                    </div>

                )
            })) : (<div className = "center">No Posts!</div> ) 

        return(
                <div className = "container">
                    <div className = "center">
                        {PostList}
                    </div>
                </div>)
    }

}

export default Home;
3 Answers

The issue was in multiple setState. Once you setState, it updates and renders asynchronously. So didn't wait for next setState. Better set both data in one call.

componentDidMount =  async () => {
        const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
            this.setState({posts: res.data.slice(0,10)})

        const res2 = await axios.get('https://jsonplaceholder.typicode.com/photos');
            this.setState({images: res2.data.slice(0,10)})

        }

You can clean up code like this:

Working post: https://codesandbox.io/s/articlepodcast-wd2rv?file=/home.js:0-1214

import React, { Component } from "react";
import axios from "axios";

class Home extends Component {
  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      images: []
    };
  }

  componentDidMount = async () => {
    const res = await axios.get("https://jsonplaceholder.typicode.com/posts");
    const res2 = await axios.get("https://jsonplaceholder.typicode.com/photos");
    this.setState({
      posts: res.data.slice(0, 10),
      images: res2.data.slice(0, 10)
    });
  };

  render = () => {
    const { posts, images } = this.state;
    const PostList = posts.length ? (
      posts.map((post, index) => {
        return (
          <div className="post card" key={post.id}>
            <div className="card-content">
              <div className="card-title">{post.title}</div>
              <div className>{images[index].url} </div>
              <div>
                <p>{post.body}</p>
              </div>
            </div>
          </div>
        );
      })
    ) : (
      <div className="center">No Posts!</div>
    );

    return (
      <div className="container">
        <div className="center">{PostList}</div>
      </div>
    );
  };
}

export default Home;

Because of your first setState, it goes to render before second async-await.

const res = await axios.get('https://jsonplaceholder.typicode.com/posts');
const res2 = await axios.get('https://jsonplaceholder.typicode.com/photos');

this.setState({images: res2.data.slice(0,10), posts: res.data.slice(0,10)})

try like this.

better to have something like this;

this.setState({
   posts: res.data.slice(0,10).map((post, index) => ({
     ...post,
    image: res2.data.slice(0,10)[index]

   }))

})

The Problem

The error happens because of the way the state is set in componentDidMount function. To break down why this happens I will explain what the code is trying to do: First, Axios will send a get request to obtain posts, because this is an async action that is being awaited javascript will work on something else while the data is being obtained. So it will continue and run the render function the first time. The render function will return No Posts! as this.state.posts.length ? is false.

Then when we finish getting the posts the componentDidMount will continue, in this case it will update the state this.setState({posts: res.data.slice(0,10)}), every time setState is called it will tell react to run the render function for that component again to make sure the page reflects what is in our state. Then we get to the next line where we await to get the photos, while we are waiting render function will run a second-time because (as just said) we changed the sate of the component. At this point in time, we still don't have the photos as we are currently awaiting for them but, we do have posts, so the check to see if we have the posts this.state.posts.length ? will be true. Then when we get to images[index] it will be undefined because we have not finished obtaining the photos and setting them into the state yet. (I hope this helps you understand the problem).

The Solution

Update the state after we have obtained all data so that the second time the render function runs we have images:

const res = await axios.get("https://jsonplaceholder.typicode.com/posts");
const res2 = await axios.get("https://jsonplaceholder.typicode.com/photos");
this.setState({
  posts: res.data.slice(0, 10),
  images: res2.data.slice(0, 10)
});

I would also suggest adding a safety check to this.state.images like you did with posts to make sure you actually have the data before you try to use it. This is good practice to do for all data that comes from network calls because many unexpected things could happen when trying to obtain data from a service.

Lastly, I also suggest you try following through your code with the chrome debugger. This will help you follow what the code is doing to get a better understanding of why an issue like this would be happening. Here is a nice guide on how to do that. https://developers.google.com/web/tools/chrome-devtools/javascript

Related