1

I am creating a simple weather app to solidify my react hook knowledge. When using useEffect I keep coming up with an error when using async await functions. I looked around and found ways around to use async functions one time in use effect.

My problem is that I want to use async/await functions twice. I use navigator.geolocation to find current location and set lat and long in state. Then, once they are set run a weather api that uses the new state of lat and long. I found multiple solutions on here about how setting state waits til the next render so using the newly set state in the next fetchAPI function wont work.

Thus, I came up with this solution.

  const [lat, setLat] = useState([]);
  const [long, setLong] = useState([]);
  const [data, setData] = useState();

  useEffect(() => {
    fetchLocation(/*uses navigator.geolocation to setLong and setLat*/);

    // hacked out a way to not call fetch data until lat and long are set.
    if (typeof lat == "number" && typeof long == "number") {
      fetchWeatherData();
    }
    console.log(lat, "<= lat");
    console.log(long, "<= long");
  }, [lat, long]); 

This solution works like I wanted on localhost because it only fetches the weatherAPI when the lat and long states are set with the first function. Before useEffect would load the weatherAPI with lat and long still set to empty causing an error. I am wondering if this is correct way to go about this problem or if there are unknown side effects that I haven't found yet.

Also this warning pops up afterwards and I am not sure if how to handle it.

"src/App.js Line 37:6: React Hook useEffect has a missing dependency: 'fetchWeatherData'. Either include it or remove the dependency array react-hooks/exhaustive-deps"

EDIT: full code as requested from comments

import React, { useState, useEffect } from "react";
import WeatherDisplay from "./weather";
require("dotenv").config();

function App() {
  const [lat, setLat] = useState([]);
  const [long, setLong] = useState([]);
  const [data, setData] = useState();

  const fetchLocation = () => {
    navigator.geolocation.getCurrentPosition((position) => {
      setLat(position.coords.latitude);
      setLong(position.coords.longitude);
    });
  };

  const fetchWeatherData = () => {
    fetch(
      `${process.env.REACT_APP_API_URL}/weather/?lat=${lat}&lon=${long}&units=metric&APPID=${process.env.REACT_APP_API_KEY}`
    )
      .then((res) => res.json())
      .then((result) => {
        setData(result);
        console.log(result);
      });
  };

  useEffect(() => {
    fetchLocation();

    // hacked out a way to not call fetch data until lat and long are set.
    if (typeof lat == "number" && typeof long == "number") {
      fetchWeatherData();
    }
    console.log(lat, "<= lat");
    console.log(long, "<= long");
  }, [lat, long]); // set as empty arrays if locations don't work

  return (
    <div className="App">
      {/* data is the data that was fetched from fetchWeatherData() */}
      <WeatherDisplay weatherData={data} />
    </div>
  );
}

export default App;
  • 1
    can you share full code so we can get it better. – Pankaj Chaturvedi Apr 23 '21 at 07:49
  • The function passed to `useEffect` runs when any of its dependencies change. So yes, the correct way to do this is to use a useEffect hook with an empty dependency array to start the geolocation, then use one with `[lat, long]` to load the weather data. I'm not sure what exactly the issue is, mostly because you've removed lots of relevant code. –  Apr 23 '21 at 07:49
  • Here's the basic idea: https://codesandbox.io/s/upbeat-nightingale-098lx?file=/src/App.js –  Apr 23 '21 at 07:57
  • Sorry, I added the full code now – Hunter Scott Lacefield Apr 23 '21 at 08:06

4 Answers4

2

Putting lat and long in two separate useState's makes you lose control. You better put them inside a single useState variable:

const [coordinates, setCoordinates] = useState([]); // [lat, long]

This way, the geolocation routine calls the setter only once and the useEffect hook, which will depend on [coordinates], is always triggered at the right moment with complete information.

Regarding the danger of triggering useEffect before coordinates are set, you have two possibilities:

  • initialize the hook providing some default values
  • put an if-guard at the start of the function run by useEffect

Regarding the missing dependency inside the hook function, please check this comprehensive answer

Marco Faustinelli
  • 3,734
  • 5
  • 30
  • 49
  • This is good advice but not really an answer to the specific question. –  Apr 23 '21 at 07:58
  • Still digging at it :-) – Marco Faustinelli Apr 23 '21 at 08:02
  • 1
    Putting lat and long into one variable is great advice. I did this and it is much easier to understand. Thank you! I am now using an if guard at the start of the function run by useEffect. My main question for this was if using an if guard in useEffect was a good practice. Thanks to all of you I now understand that it is. Thank you! – Hunter Scott Lacefield Apr 23 '21 at 08:59
  • @HunterScottLacefield - there may come a stage where the composite variable `[lat, long, height, etc. etc.]` becomes too large. At that time you better pass to the `useReducer` hook. It allows you to trigger effects depending on parts of the state (for example: `useEffect(fn, [state.lat]`)) – Marco Faustinelli Apr 23 '21 at 09:52
  • @HunterScottLacefield - another use case for `useReducer` is when you realize that there are two different `useState`'s in which the two variables are actually correlated in the life cycle of the page; for example, a flag that signals stale weather data and the flag that signals weather data are being downloaded. You put them both in a single `useReducer` state and can manage their mutual dependencies with full control. – Marco Faustinelli Apr 23 '21 at 10:04
1

First question that arises: do you actually need these coordinates? I mean, apart from passing them to fetchWeahterData?

If not, why bother with two useEffect?


const fetchWeatherData = (position) => {
    fetch(
      `${process.env.REACT_APP_API_URL}/weather/?lat=${position.lat}&lon=${position.long}&units=metric&APPID=${process.env.REACT_APP_API_KEY}`
    )
      .then((res) => res.json())
      .then((result) => {
        setData(result);
        console.log(result);
      });
  };

  useEffect(() => {
    navigator.geolocation.getCurrentPosition((position) => {
      fetchWeatherData(position)
    });
  }, []);

If you don't teleport, you won't have to set a dependency on position. Just fetch the position once, and subsequently call fetchWeatherData with it.

jperl
  • 4,662
  • 2
  • 16
  • 29
  • No, I do not need them apart from using from using them for fetchWeatherData(). This seems to do the trick. I was just wondering if using an if condition in useEffect like Chris G in one of the first comments did was bad practise. – Hunter Scott Lacefield Apr 23 '21 at 08:42
  • 1
    Not at all, I do it a lot, too. – jperl Apr 23 '21 at 08:51
  • Just when you need to use some data and you won't want to wait for a rerender as the setter from useState is asynchronous, use the data at the source. – jperl Apr 23 '21 at 08:53
  • Otherwise, you need to separate this into two useEffects like the others did and set position as the dependency and add a if condition like you did. And no, it's not bad practice. – jperl Apr 23 '21 at 08:55
  • @HunterScottLacefield - first time you realize that you need effects depending on just changes in `lat` you better pass to `useReducer`. You may well write effects like `useEffect(fn, [coordinates[0]])` but they become much less understandable. – Marco Faustinelli Apr 23 '21 at 10:18
0

If I were you, I'd refactor the code like the following:


const [coords, setCoords] = useState();

// get current position as soon as the component is mounted
useEffect(() => {
  navigator.geolocation.getCurrentPosition(res => {
    if (res && res.coords) {
      setCoords(coords);
    }
  });
}, []);

// fetch weather data when coords is not empty
useEffect(() => {
  if (!coords) {
    return;
  }
  fetchWeatherData(coords);
}, [coords]);

If you want to clean your code with custom hooks, it's definitely worth to take a look at this useGeolocation hook.

import { useGeolocation } from 'beautiful-react-hooks'; 

const PositionReporter = () => {
  const [geoState, { onChange }] = useGeolocation(); 
  
  onChange(() => {
    console.log('Position changed...');
  });
    
  return (
   <DisplayDemo>
     The current position is:
     {geoState.isRetrieving && (<p>Retrieving position...</p>)}
     {geoState.isSupported && geoState.position && [
       <p key={0}>Lat: {geoState.position.coords.latitude}</p>,
       <p key={1}>Lng: {geoState.position.coords.longitude}</p>
     ]}
   </DisplayDemo>
  );
};

<PositionReporter />
glinda93
  • 7,659
  • 5
  • 40
  • 78
0

It's ok to use internal async routines in useEffect, but in this case, you should take care of cleaning/canceling tasks when components unmounting to avoid React leakage warning.

Working demo with a custom hook (Live sandbox):

import React, { useState } from "react";
import {
  useAsyncEffect,
  E_REASON_UNMOUNTED,
  CanceledError
} from "use-async-effect2";
import cpFetch from "cp-fetch";

const API_KEY = "YOUR API KEY"; // <------Change this

const getCurrentPosition = (options) => {
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(resolve, reject, options);
  });
};

export default function TestComponent(props) {
  const [text, setText] = useState("");

  const cancel = useAsyncEffect(function* () {
    try {
      setText("requesting coords...");
      const {
        coords: { latitude, longitude }
      } = yield getCurrentPosition();
      setText(`${latitude} : ${longitude}`);
      const response = yield cpFetch(
        `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${API_KEY}`
      ).timeout(props.timeout);
      setText(JSON.stringify(yield response.json(), null, 2));
    } catch (err) {
      CanceledError.rethrow(err, E_REASON_UNMOUNTED);
      setText(`Failed: ${err.toString()}`);
    }
  });

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>{text}</div>
      <button className="btn btn-warning" onClick={cancel}>
        Cancel request
      </button>
    </div>
  );
}
Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7