0

When I make a request to an API and setting the state to the results from the Axios request it still shows up null. I am using React useState and setting the results from the request and wanting to check to see if its coming through correctly and getting the right data its still resulting into null. The request is correct but when I use .then() to set the state that is the issue I am having.

Below is the component that I am building to make the request called Details.js (first code block) and the child component is the DetailInfo.js file (second code block) that will be displaying the data. What am I missing exactly or could do better when making the request and setting the state correctly display the data?

import React, {useEffect, useState} from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import axios from 'axios';
import { getCookie } from '../utils/util';

import DetailInfo from '../components/DetailInfo';
import DetailImage from '../components/DetailImage';




const Details = () => {

    const [ countryData, setCountryData ] = useState(null);
    let country;  
    let queryURL = `https://restcountries.eu/rest/v2/name/`;

    useEffect(() => {
        country = getCookie('title');
        console.log(country);
        queryURL += country;
        console.log(queryURL);
        axios.get(queryURL)
        .then((res) => {
            console.log(res.data[0])
            setCountryData(res.data[0]);
        })
        .then(() => {
            console.log(countryData)
        }
        );

    }, [])

    return (
        <>
            <Container className="details">
                <Row>
                    <Col sm={6}>
                        <DetailImage />
                    </Col>
                    <Col sm={6}>
                        <DetailInfo 
                        name={countryData.name} 
                        population={countryData.population} 
                        region={countryData.region}
                        subRegion={countryData.subRegion}
                        capital={countryData.capital}
                        topLevelDomain={countryData.topLevelDomain}
                        currencies={countryData.currencies}
                        language={countryData.language}
                        />
                    </Col>
                </Row>
            </Container>
        </>
    )
}

export default Details;

The child component below......

import React from 'react';


const DetailInfo = (props) => {
    const {name, population, region, subRegion, capital, topLevelDomain, currencies, language} = props;
    return (
        <>detail info{name}{population} {region} {capital} {subRegion} {topLevelDomain} {currencies} {language}</>
    )
}


export default DetailInfo;
Micah Johnson
  • 92
  • 2
  • 9
  • Does this answer your question? [useState set method not reflecting change immediately](https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately) – Ryan Le Sep 11 '21 at 04:49

4 Answers4

1

Ultimately, the problem comes down to not handling the intermediate states of your component.

For components that show remote data, you start out in a "loading" or "pending" state. In this state, you show a message to the user saying that it's loading, show a Spinner (or other throbber), or simply hide the component. Once the data is retrieved, you then update your state with the new data. If it failed, you then update your state with information about the error.

const [ dataInfo, setDataInfo ] = useState(/* default dataInfo: */ {
  status: "loading",
  data: null,
  error: null
});

useEffect(() => {
  let unsubscribed = false;

  fetchData()
    .then((response) => {
      if (unsubscribed) return; // unsubscribed? do nothing.
      setDataInfo({
        status: "fetched",
        data: response.data,
        error: null
      });
    })
    .catch((err) => {
      if (unsubscribed) return; // unsubscribed? do nothing.
      console.error('Failed to fetch remote data: ', err);
      setDataInfo({
        status: "error",
        data: null,
        error: err
      });
    });

  return () => unsubscribed = true;
}, []);

switch (dataInfo.status) {
  case "loading":
    return null; // hides component
  case "error":
    return (
      <div class="error">
        Failed to retrieve data: {dataInfo.error.message}
      </div>
    );
}

// render data using dataInfo.data
return (
  /* ... */
);

If this looks like a lot of boiler plate, there are useAsyncEffect implementations like @react-hook/async and use-async-effect that handle it for you, reducing the above code to just:

import {useAsyncEffect} from '@react-hook/async'

/* ... */

const {status, error, value} = useAsyncEffect(() => {
  return fetchData()
    .then((response) => response.data);
}, []);

switch (status) {
  case "loading":
    return null; // hides component
  case "error":
    return (
      <div class="error">
        Failed to retrieve data: {error.message}
      </div>
    );
}

// render data using value
return (
  /* ... */
);
samthecodingman
  • 23,122
  • 4
  • 30
  • 54
0

Because state only update when component re-render. So you should put console.log into useEffect to check the new value:

useEffect(() => {
    country = getCookie('title');
    console.log(country);
    queryURL += country;
    console.log(queryURL);
    axios.get(queryURL).then(res => {
        console.log(res.data[0]);
        setCountryData(res.data[0]);
    });
}, []);

useEffect(() => {
    console.log(countryData);
}, [countryData]);
Viet
  • 12,133
  • 2
  • 15
  • 21
  • While this would help OP log whenever `countryData` changes, it doesn't solve OP's "reading property of `null`" error on the first render cycle. – samthecodingman Sep 11 '21 at 05:20
0

useState does reflecting its change immediately. I think that it would be probably solved if you set countryData to second argument of useEffect.

useEffect(() => {
        country = getCookie('title');
        console.log(country);
        queryURL += country;
        console.log(queryURL);
        axios.get(queryURL)
        .then((res) => {
            console.log(res.data[0])
            setCountryData(res.data[0]);
        })
        .then(() => {
            console.log(countryData)
        }
        );

    }, [countryData])
TIshow
  • 918
  • 4
  • 12
  • 27
0

The issue is, as samthecodingman, pointed out, an issue of intermediate data. Your component is being rendered before the data is available, so your child component needs to re-render when its props change. This can be done via optional chaining, an ES6 feature.

import React, { useEffect, useState } from "react";
import DetailInfo from "./DetailInfo";
import { Col, Container, Row } from "react-bootstrap";
import axios from "axios";

const Details = () => {
  const [countryData, setCountryData] = useState({});
  let country = "USA";
  let queryURL = `https://restcountries.eu/rest/v2/name/`;

  useEffect(() => {
    console.log(country);
    queryURL += country;
    console.log(queryURL);
    axios
      .get(queryURL)
      .then((res) => {
        console.log(res.data[0]);
        setCountryData(res.data[0]);
      })
      .then(() => {
        console.log(countryData);
      });
  }, []);

  return (
    <Container className="details">
      <Row>
        <Col sm={6}>
          <DetailInfo
            name={countryData?.name}
            population={countryData?.population}
            region={countryData?.region}
            subRegion={countryData?.subRegion}
            capital={countryData?.capital}
            language={countryData?.language}
          />
        </Col>
        <Col sm={6}></Col>
      </Row>
    </Container>
  );
};

export default Details;

Checkout my Codesandbox here for an example.