4

I'm new to react functional components and I'm trying to get the weather data on multiple cities on page load but useEffect is now re-rending each call. How can I write this so useEffect doesn't cause re-renders?

function App() {
    const [data, setData] = useState([]);
    const [activeWeather, setActiveWeather] = useState([]);

    useEffect(() => {
        const key = process.env.REACT_APP_API_KEY;

        const fetchData = async (city) => {
            const res = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`);
            setData((data) => [
                ...data,
                { description: res.data.weather[0].description, icon: res.data.weather[0].icon, temp: res.data.main.temp, city: res.data.name, country: res.data.sys.country, id: res.data.id },
            ]);
        };
        const fetchCities = () => {
            const cities = [fetchData("Ottawa"), fetchData("Toronto"), fetchData("Vancouver"), fetchData("California"), fetchData("London")];

            Promise.all(cities).catch((err) => {
                console.log(err);
            });
        };
        fetchCities();
    }, []);
zer0day
  • 236
  • 1
  • 3
  • 11

2 Answers2

5

You can make the fetchData function to return the data you need without updating the state, then you can fetch x amount of cities and only when all of the requests complete update the state.

Note that if one of the requests inside Promise.all fail, it will go to the catch block without returning any data back, basically all or nothing

const key = process.env.REACT_APP_API_KEY

const fetchCity = async city => {
  const { data } = await axios.get(
    `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`,
  )

  return {
    description: data.weather[0].description,
    icon: data.weather[0].icon,
    temp: data.main.temp,
    city: data.name,
    country: data.sys.country,
    id: data.id,
  }
}

function App() {
  const [cities, setCities] = useState([])
  const [activeWeather, setActiveWeather] = useState([])

  useEffect(() => {
    const fetchCities = async () => {
      const citiesData = await Promise.all(
        ['Ottawa', 'Toronto', 'Vancouver'].map(fetchCity),
      )

      setCities(prevState => prevState.concat(citiesData))
    }

    fetchCities()
  }, [])
}
Asaf Aviv
  • 11,279
  • 1
  • 28
  • 45
  • 1
    This is a great answer overall! Side note: You're right ofc about the all-or-nothing aspect (can use `allSettled` to get more fine-grained info), but re *"...it will go to the catch block without resolving any of the promises..."* `Promise.all` doesn't *resolve* any promises other than its own, the promises will settle regardless of what `Promise.all` does. All `Promise.all` does is observe the results and either fulfill or reject its own promise. (Re "settle" vs. "fulfill" vs. "resolve", I [did a blog post](https://thenewtoys.dev/blog/2021/02/08/lets-talk-about-how-to-talk-about-promises/).) – T.J. Crowder Jul 01 '21 at 12:36
  • 1
    @T.J.Crowder Yes great points, will update the answer. (and read the post on the way) – Asaf Aviv Jul 01 '21 at 12:39
2

You can use Promise.all and then call setData once. something like this:

useEffect(() => {
  const fetchCity = (city) => axios.get(`${base}/${city}`);
  const cities = ["Ottawa", "Toronto"];
  const promises = cities.map(fetchCity);
  Promise.all(promises).then((responses) => {
    setData(cities.map((city, index) => ({ city, ...responses[index] })));
  });
}, []);
Dvir Hazout
  • 251
  • 1
  • 3