Darren
Darren

Reputation: 2290

React hooks useState to set multiple states in context provider

I have a createContext component that is using useState to set multiple values returned from a fetch function. However in my code below, when a state is updated, the others states return to the original value.

For example, in getCountryCode() the state is updated for countryCode, but then iconCode in weatherInit() fetches its value and countryCode returns to the original US.

import React, { createContext, useState, useEffect } from 'react';

export const GlobalConsumer = createContext();

export const GlobalProvider = ({ children }) => {
  const [state, setState] = useState({
    menuPanel: false,
    countryCode: 'US',
    weatherLoading: true,
    iconCode: '',
    fahrenheit: '',
    celcius: '',
    showCelcius: false
  });

  const getCountryCode = () => {
    const url = `https://ipapi.co/json/`;
    fetch(url)
      .then(data => data.json())
      .then(data => {
        const countryCode = data.country;
        setState({ ...state, countryCode });
      });
  };

  const weatherInit = () => {
    const CITY_LAT = '...';
    const CITY_LON = '...';
    const OW_KEY = '...';
    const url = `//api.openweathermap.org/data/2.5/weather?lat=${CITY_LAT}&lon=${CITY_LON}&units=imperial&appid=${OW_KEY}`;
    fetch(url)
      .then(data => data.json())
      .then(data => {
        const iconCode = data.weather[0].id;
        setState({ ...state, iconCode });
        const fahrenheit = Math.round(data.main.temp_max);
        setState({ ...state, fahrenheit });
        const celcius = Math.round((5.0 / 9.0) * (fahrenheit - 32.0));
        setState({ ...state, celcius });
        setTimeout(() => {
          setState({ ...state, weatherLoading: false });
        }, 150);
      });
  };

  useEffect(() => {
    getCountryCode();
    weatherInit();
  }, []);

  return (
    <GlobalConsumer.Provider
      value={{
        contextData: state,
        togglemMenuPanel: () => {
          setState({ ...state, menuPanel: !state.menuPanel });
        },
        toggleCelcius: () => {
          setState({ ...state, showCelcius: !state.showCelcius });
        }
      }}
    >
      {children}
    </GlobalConsumer.Provider>
  );
};

I believe that this is caused because each value requires it's own useState. However, can these values be merged or is there another way to achieve this outcome, where I am only required to pass as data to the Provider context?

Upvotes: 1

Views: 3101

Answers (1)

Patrick Wozniak
Patrick Wozniak

Reputation: 1572

It's because you are using the old value of state when calling setState(). As documented here (Scroll down to the "Note"-block) you have to pass a function to your setState call:

const iconCode = data.weather[0].id;
setState(prevState => ({ ...prevState, iconCode }));
const fahrenheit = Math.round(data.main.temp_max);
setState(prevState => ({ ...prevState, fahrenheit }));
const celcius = Math.round((5.0 / 9.0) * (fahrenheit - 32.0));
setState(prevState => ({ ...prevState, celcius }));
setTimeout(() => {
  setState(prevState => ({ ...prevState, weatherLoading: false }));
}, 150);

Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:

setState(prevState => {
  // Object.assign would also work
  return {...prevState, ...updatedValues};
});

Upvotes: 2

Related Questions