I use useState hock to store the value but it did not update

Tags: , ,



I am new to Reactjs I am trying to build an address form with 3 Select ( country, State, City ) I used React hock so when the page first load it will fetch countries list to country select after that when user select country it will fetch states list to state select and after that when user select state it will fetch cities list to city select my issue with state hock I store the value of the user selected in the state but it did not update the value in the state in many locations I keep getting ” undefined ” like when the page load I get countries list as an array and I get the first country in the list as the default select item in country select but I still get keep getting ” undefined ” I tried many ways but still getting the same result and bellow is my code

import React,{ useEffect , useState , useCallback } from 'react';
import { InputLabel, Select , MenuItem , Grid , Typography } from '@material-ui/core';
import { useForm, FormProvider } from 'react-hook-form';
import CityHandler from 'countrycitystatejson';

export const TempAddresForm = () => 
{
    const methods = useForm();
    const [countries, setCountries] = useState([]);
    const [countryCode, setCountryCode] = useState('');
    const [country, setCountry] = useState('');
    const [states, setStates] = useState([]);
    const [stateName, setStateName] = useState('');
    const [cities, setCities] = useState([]);
    const [city, setCity] = useState('');


    const fetchCounters = () => {
        setCountries(CityHandler.getCountries());
        setFirstCountry();
    };

    const countryChangeHandler = (event) => {
        let tempCountryCode = event.target.value;
        setCountry(CityHandler.getCountryByShort(tempCountryCode));
        setCountryCode(tempCountryCode);
        fetchStates(tempCountryCode);
        setCities([]); 
    }

    const fetchStates = (countryCode) => 
    {
        setStates(CityHandler.getStatesByShort(countryCode));
    }

    const stateChangeHandler = (even) => 
    {
        let tempState = even.target.value;
        setStateName(even.target.value);
        fetchCities(tempState);
    }

    const fetchCities = (stateName) => {
        let tempCities = CityHandler.getCities(countryCode, stateName);
        setCities(tempCities);
    };

    const cityChangeHandler = (event) => 
    {
        let tempCity = event.target.value;
        setCity(tempCity);
        console.log("Temp City Name : " + tempCity)
        console.log("City Name : " + city)
    }

    const setFirstCountry = useCallback( () => 
    {
        if(countries)
        {
            let firstCountry = CityHandler.getCountries()[0];
            console.log ("[setFirstCountry] : First Country " + JSON.stringify(firstCountry.name));
            setCountry(firstCountry);
            console.log ("[setFirstCountry] : Country name " + JSON.stringify(country.name));
        }
    }, []);

    useEffect(() => {
        fetchCounters();
        //setFirstCountry();
        
    }, []);
    return (
        <>
            <Typography variant="h6" gutterBottom>Shipping Address</Typography>
            <FormProvider {...methods}>
                <form onSubmit={''}>
                    <Grid container spacing={3}>
                        
                        <Grid item xs={12} sm={6}>
                            <InputLabel>Country</InputLabel>
                            <Select value={country.name} fullWidth onChange={(event) => {countryChangeHandler(event)}}>
                                {countries.map((countryLoop) => (
                                    <MenuItem key={countryLoop.shortName} id={countryLoop.shortName} value={countryLoop.shortName}>
                                        {countryLoop.name}
                                    </MenuItem>
                                ))}
                                
                            </Select>
                        </Grid>
                        <Grid item xs={12} sm={6}>
                            <InputLabel>State</InputLabel>
                            <Select value={stateName} fullWidth onChange={(event) => {stateChangeHandler(event)}}>
                                {states.map((state, index) => {
                                    return(
                                        <MenuItem key={index} id={state} value={state}>
                                            {state}
                                        </MenuItem>
                                    );
                                })}
                                
                            </Select>
                        </Grid>
                        <Grid item xs={12} sm={6}>
                            <InputLabel>City</InputLabel>
                            <Select value={city} fullWidth onChange={(event) => {cityChangeHandler(event)}}>
                                {cities.map((city, index) => {
                                    return(
                                        <MenuItem key={index} id={city} value={city}>
                                            {city}
                                        </MenuItem>
                                    );
                                })}
                                
                            </Select>
                        </Grid>
                        
                    </Grid>
                    
                </form>
            </FormProvider>
        </>
    )    
}

export default TempAddresForm;

if anyone could help me with this

*Note: I use this package to get the Country list countrycitystatejson

Answer

country is actually always updating but you are logging it in a useCallback hook and did not add country to its dependency array. So it only captures the initial value of country which is an empty string "" and JSON.stringify("".name) is undefined. If you add country to the dependency array of useCallback you will see it updating.

console.log( JSON.stringify("".name) )

You don’t need to use useCallback here. Read this article to understand where and when to use useCallback and useMemo

The main problem is that you are mapping your country Select to country.name but your select options have country.shortName as their value – Try changing the Select value to country.shortName.

Also, you have too many state variables that are interdependent to each other. Here moving all your state variables to a single state object will make it a little bit easier to handle.

Ex Like below

{
 countries: [...],
 states: [...],
 cities: [...],
 stateName: "..",
 ...
 ...
}

countries is always constant & states, cities are just derived values. So your actual state just needs these 3 values countryShortCode, stateName and city.

Here is a snippet with all the above-mentioned changes

import React, { useState } from "react";
import {
  InputLabel,
  Select,
  MenuItem,
  Grid,
  Typography
} from "@material-ui/core";
import { useForm, FormProvider } from "react-hook-form";
import CityHandler from "countrycitystatejson";

// This countriesList doesn't change so it can just be a constant outside your component
const countriesList = CityHandler.getCountries();

// Your initial state
const initialState = {
  countryShortName: countriesList[0].shortName,
  stateName: "",
  city: ""
};

const TempAddresForm = () => {
  const methods = useForm();

  const [state, setState] = useState(initialState);

  const changeCountry = (e) => {
    setState((prevState) => ({
      ...prevState,
      countryShortName: e.target.value,
      stateName: "",
      city: ""
    }));
  };

  const changeState = (e) => {
    setState((prevState) => ({
      ...prevState,
      stateName: e.target.value,
      city: ""
    }));
  };

  const changeCity = (e) => {
    setState((prevState) => ({
      ...prevState,
      city: e.target.value
    }));
  };

  // Derive both states and cities here ( No need to store it in state :) )
  const states = CityHandler.getStatesByShort(state.countryShortName);
  const cities = state.stateName
    ? CityHandler.getCities(state.countryShortName, state.stateName)
    : [];

  return (
    <>
      <Typography variant="h6" gutterBottom>
        Shipping Address
      </Typography>
      <FormProvider {...methods}>
        <form onSubmit={() => console.log("Submitted")}>
          <Grid container spacing={3}>
            <Grid item xs={12} sm={6}>
              <InputLabel>Country</InputLabel>
              <Select
                value={state.countryShortName || ""}
                fullWidth
                onChange={changeCountry}
              >
                {countriesList.map((countryLoop) => (
                  <MenuItem
                    key={countryLoop.shortName}
                    id={countryLoop.shortName}
                    value={countryLoop.shortName}
                  >
                    {countryLoop.name}
                  </MenuItem>
                ))}
              </Select>
            </Grid>
            <Grid item xs={12} sm={6}>
              <InputLabel>State</InputLabel>
              <Select value={state.stateName} fullWidth onChange={changeState}>
                {states.map((state, index) => {
                  return (
                    <MenuItem key={index} id={state} value={state}>
                      {state}
                    </MenuItem>
                  );
                })}
              </Select>
            </Grid>
            <Grid item xs={12} sm={6}>
              <InputLabel>City</InputLabel>
              <Select value={state.city || ""} fullWidth onChange={changeCity}>
                {cities.map((city, index) => {
                  return (
                    <MenuItem key={index} id={city} value={city}>
                      {city}
                    </MenuItem>
                  );
                })}
              </Select>
            </Grid>
          </Grid>
        </form>
      </FormProvider>
    </>
  );
};

export default TempAddresForm;

Comment if you need to understand anything else



Source: stackoverflow