7

I'm fetching data from a weather api using useEffect hook and declaring the dependency correctly as well. My state is still not being updated and I get errors in my render function because of that. I've pretty much tried everything from getting rid of the dependency array to declaring multiples in the dependency array. I don't know what's wrong with my function. The API's JSON response is in this format:

{
 location: {
 name: "Paris",
 region: "Ile-de-France",
 },
 current: {
  last_updated_epoch: 1564279222,
  last_updated: "2019-07-28 04:00",
  temp_c: 16,
  temp_f: 60.8,
  is_day: 0,
  condition: {
    text: "Clear",
    icon: "//cdn.apixu.com/weather/64x64/night/113.png",
    code: 1000
  },
  wind_mph: 6.9,
  wind_kph: 11.2
 }
}

and this is what my code looks like:

const Weather = ({ capital }) => {
  const [weather, setWeather] = useState(null);

  useEffect(() => {
    console.log("useEffect called");
    const getWeather = async () => {
      try {
        const res = await axios.get(
          `http://api.apixu.com/v1/current.json?key=53d601eb03d1412c9c004840192807&q=${capital}`
        );
        setWeather(res.data);
      } catch (e) {
        console.log(e);
      }
    };
    getWeather();
  }, [capital]);
  console.log(weather);

  return (
    <Card style={{ width: "18rem", marginTop: "25px" }}>
      <Card.Img variant="top" src={weather.current.condition.icon} />

      <Card.Header style={{ textAlign: "center", fontSize: "25px" }}>
        Weather in {capital}
      </Card.Header>
    </Card>
  )
}

I expect to get to be shown image of the icon but I get this error message in my render function:

TypeError: Cannot read property 'current' of null
Weather
src/components/Weather.js:26
  23 | 
  24 | return (
  25 |   <Card style={{ width: "18rem", marginTop: "25px" }}>
  26 |     <Card.Img variant="top" src={weather.current.condition.icon} />
     | ^  27 | 
  28 |     <Card.Header style={{ textAlign: "center", fontSize: "25px" }}>
  29 |       Weather in {capital}

and my console.log(weather) return null, the original state even though its being called after useEffect() and console.log(useEffect called) does not log at all which mean useEffect is not being called.

Moe
  • 2,794
  • 1
  • 15
  • 25
Navjeet Kaur
  • 73
  • 1
  • 1
  • 4
  • Remove your dependency `[capital]` – Ru Chern Chong Jul 30 '19 at 02:24
  • I've tried removing the dependency array altogether, but it still doesn't solve the issue and i've also tried with just `[]` but doesn't work. However, I do need to re-render and update state if capital changes so I know for sure `[]` this isn't the answer. – Navjeet Kaur Jul 30 '19 at 02:28

3 Answers3

16

The error message gives it away, Cannot read property 'current' of null, the only place where current is called is in weather.current in the src of Card.Img, so we deduce that weather was null during the render.

The reason this happens is because the api call is asynchronus, it doesn't populate the state immediately, so the render happens first and tries to read .current from the initial weather state null.

Solution: in your render method, make sure not to read weather.current while weather is null.

You can for example use {weather && <Card>...</Card} to hide the whole card until the data is loaded and show a loading indicator, or you can use src={weather && weather.current.condition.icon} as a quick workaround.

const Weather = ({capital}) => {
  const [weather, setWeather] = useState(null);

  useEffect(() => {
    console.log("useEffect called");
    const getWeather = async () => {
      try {
        const res = await axios.get(
          `http://api.apixu.com/v1/current.json?key=53d601eb03d1412c9c004840192807&q=${capital}`,
        );
        setWeather(res.data);
      } catch (e) {
        console.log(e);
      }
    };
    getWeather();
  }, [capital]);
  console.log(weather);

  return (
    <Card style={{width: "18rem", marginTop: "25px"}}>
      <Card.Img variant="top" src={weather && weather.current.condition.icon} />

      <Card.Header style={{textAlign: "center", fontSize: "25px"}}>
        Weather in {capital}
      </Card.Header>
    </Card>
  );
};
Moe
  • 2,794
  • 1
  • 15
  • 25
  • WOW that worked. Even the `console.log(weather)` is populating and `useEffect called` is also being logged to console. I understand why I had to check for weather in my render function but how come my `console.log(weather)` was not logging the weather data or my state was not being updated with my `setWeather()`? – Navjeet Kaur Jul 30 '19 at 02:34
  • 2
    the reason this happens is because the api call is asynchronus, it doesn't populate the state immediately, so the render happens first and tries to read `.current` from the initial state `null` – Moe Jul 30 '19 at 02:38
2

So, I was facing a similar issue, where it seemed like useEffect hook was not getting invoked at all. The concerned code snippet is given below:

Within my functional component, this was the sequence of the appearance of the code:

  • a variable const facetFields = []; declaration which was supposed to be set by a AJAX call from within useEffect hook

  • useEffect hook within which the above variable was getting set with AJAX.

     useEffect( ()=>{
          console.log('FacetTreeView: useEffect entered');
          facetFields = getFacetFieldNames(props.tabIndex);   
       }, []);
    
  • JSX code that uses the variable.

    return (<MyComponent> { facetFields.map((facetLabel, index) => { populateFacetInstances(facetLabel) }) } </Component>)

With this code, the console statement inside the hook was not printing at all. Also, I kept getting a undefined error while map'ing the facetFields. So, it turns out that the useHook is called after the component is rendered. So, during the rendering, the variable facetFields is undefined.So, I fixed this by adding this line to my JSX rendering part of code:

facetFields && facetFields.map(.....

Once, I made the above change, the control started going to the hook.So, in summary, if there is any code within JSX that is being set from useEffect hook, then its best to defer the execution of the JSX till hook execution is completed. Although, this is not mandatory, but it will make the JSX code easier to read. This can be achieved in a simple way by using a boolean state flag.

  1. Define the state:

const [readyForRender, setReadyForRender] = React.useState(false); 2. Set state in hook.

useEffect( ()=>{
            console.log('FacetTreeView: useEffect entered');
            facetFields = getFacetFieldNames(props.tabIndex);
            setReadyForRender(true); }, []);
  1. Render JSX conditionally.

if(readyForRender){ return ( { facetFields.map((facetLabel, index) => { populateFacetInstances(facetLabel) }) } ); } else{ return null; }

Binita Bharati
  • 4,026
  • 1
  • 34
  • 22
2

I had the same puzzling issue one time

You can try adding a key prop on the component when it is created in the parent code

<yourcomponent key="some_unique_value" />

This is because in most cases, when your component is reused, based on the way it is created, it may usually re-render it with some changes instead of creating it again when you reuse it, Hence the useEffect is not going to be called. eg in SwitchRoute, loops, conditionals...

So adding a key will prevent this from happening. If it is in a loop, you need to make sure each element is unique, maybe by including the index i in the key if you can't find any better unique key.

Abraham
  • 5,299
  • 1
  • 20
  • 39