Home > Software design >  React Native state does not update immediately when state is changed
React Native state does not update immediately when state is changed

Time:11-15

So I'm quite new to React native and I am making updates to an existing app. I have two dropdowns: a country dropdown and a city dropdown (These were originally text fields). When the country is set, the cities dropdown is populated from a JSON file. All this works okay, except when you change country the cities list is not populated immediately. If you set country twice - it sets the cities of the previous selection.

This is the relevant code in the page component:

[imports all in place]

const countries2 = require('../../common/constants/countries2.json');
const cities = require('../../common/constants/cities.json');
const validator = {
  city: {
    required: true,
    string: true,
  },
  country: {
    required: true,
    string: true,
  },
};
const RegistrationAddress = ({ route }) => {
  const navigation = useNavigation();
  const { personalDetails, socialRegistration } = route.params || {};
  const [updateUser] = useMutation(UPDATE_PROFILE, {
    fetchPolicy: 'no-cache',
  });

  const [state, setState] = useState({
    city: '',
    country: '',
    personalDetails: personalDetails,
  });
  const [errors, setErrors] = useState({});
  const [filteredCities, setCities] = useState([]);
  const onChange = (field, val) => {
    if (field === 'country') {
      updateCityPicker();
    }
    const newState = { ...state, [field]: val };
    setState(newState);
    const err = validate(newState, validator);
    if (err) {
      setErrors(err);
    } else {
      setErrors({});
    }
  };

  useEffect(() => {
    const err = validate({}, validator);
    setErrors(err);
    getUserAddressDetails();
  }, []);

  const getUserAddressDetails = async () => {
    const userAddressDetail = await AsyncStorage.getItem('userAddressDetails');
    const userAddressDetails = JSON.parse(userAddressDetail);
    const userAddressDetailsState = {
      ...state,
      city: userAddressDetails.city || '',
      country: userAddressDetails.country || '',
    };

    setState(userAddressDetailsState);
    const err = validate(userAddressDetailsState, validator);
    if (err) {
      setErrors(err);
    } else {
      setErrors({});
    }
  };
  const updateCityPicker = () => {
    const suggestions = cities.filter((el) => {
      var s = el[country];
      return s;
    });
    var list = [];
    suggestions.forEach((element) => {
      element[country].forEach((cl) => {
        var obj = {};
        obj['label'] = cl;
        obj['value'] = cl;
        list.push(obj);
      });
    });
    setCities(list);
  };

  const { city, country } = state;

  const navigateToRegistrationConfirmDetails = () => {
    if (socialRegistration) {
      updateUser({
        variables: {
          data: {
            city,
            country,
          },
        },
      }).then(async () => {
        await AsyncStorage.removeItem('userBasicDetails');
        await AsyncStorage.removeItem('userAddressDetails');
        navigation.navigate('QuestionnaireIntroduction');
      });
    } else {
      navigation.navigate('RegistrationConfirmDetails', { userDetails: state });
    }
  };

  return (
    <ImageBackground style={styles.imageBackground}>
      <View style={styles.regStepView}>
        <Text style={styles.regStep}>
          Address
          <Text style={styles.oneOutOfThree}> - 3/3</Text>
        </Text>
      </View>
      <ScrollView showsVerticalScrollIndicator={false}>
        <KeyboardAvoidingView style={styles.inputsView}>
          <Dropdown
            mainContainerStyle={{
              width: '100%',
              backgroundColor: 'white',
              marginTop: 5,
            }}
            textStyle={{ fontSize: verticalScale(14) }}
            value={country}
            onValueChange={(val) => onChange('country', val)}
            testID="countryID"
            placeholder="eg. United Kingdom"
            items={countries2}
            checkDropdownErrors={false}
            error={errors.accountCurrency && errors.accountCurrency[0]}
            showDropdownError={''}
            title="Which country do you currently live in?"
          />
          <Dropdown
            mainContainerStyle={{
              width: '100%',
              backgroundColor: 'white',
              marginTop: 5,
            }}
            textStyle={{ fontSize: verticalScale(14) }}
            value={city}
            onValueChange={(val) => onChange('city', val)}
            testID="cityID"
            placeholder="eg. London"
            items={filteredCities}
            checkDropdownErrors={false}
            error={errors.city && errors.city[0]}
            showDropdownError={''}
            title="Which city do you currently live in?"
          />
        </KeyboardAvoidingView>
      </ScrollView>
      <View>
        <Button
          disabled={Object.keys(errors).length}
          styleContainer={{ marginBottom: scale(24) }}
          title="Next"
          onPressFunc={async () => {
            await AsyncStorage.setItem(
              'userAddressDetails',
              JSON.stringify(state),
            )
              .then(() => navigateToRegistrationConfirmDetails())
              .catch((err) => console.log({ err }));
          }}
        />
      </View>
    </ImageBackground>
  );
};

export default RegistrationAddress;

I also get a warning about setting the state of a component from within another component, which is understandable, but I don't know the solution.

Any help would be hugely appreciated. Sorry if it's an existing question - other answers didn't quite work for me.

CodePudding user response:

Problem is setState is async. Its order is not guaranteed. So in your case its good to keep single useState as you are already using dictionary. Idea is that when you change country cityList will be filtered according to country else its empty. I have updated code as below, try it. It should work for you.

Explanation: I have introduced new cityList attribute which will be updated in case of country is changed. Also to display dropdown list we use the same cityList attribute for city dropdown.

[imports all in place]

const countries2 = require('../../common/constants/countries2.json');
const cities = require('../../common/constants/cities.json');
const validator = {
  city: {
    required: true,
    string: true,
  },
  country: {
    required: true,
    string: true,
  },
};
const RegistrationAddress = ({ route }) => {
  const navigation = useNavigation();
  const { personalDetails, socialRegistration } = route.params || {};
  const [updateUser] = useMutation(UPDATE_PROFILE, {
    fetchPolicy: 'no-cache',
  });

  const [state, setState] = useState({
    city: '',
    country: '',
    cityList: [],
    personalDetails: personalDetails,
  });
  const [errors, setErrors] = useState({});
  const onChange = (field, val) => {
      let cityList = state.cityList;
    if (field === 'country') {
        cityList =  cities.filter((el) => {
            var s = el[country];
            return s;
          });
    }
    const newState = { ...state, [field]: val, cityList };
    setState(newState);
    const err = validate(newState, validator);
    if (err) {
      setErrors(err);
    } else {
      setErrors({});
    }
  };

  useEffect(() => {
    const err = validate({}, validator);
    setErrors(err);
    getUserAddressDetails();
  }, []);

  const getUserAddressDetails = async () => {
    const userAddressDetail = await AsyncStorage.getItem('userAddressDetails');
    const userAddressDetails = JSON.parse(userAddressDetail);
    const userAddressDetailsState = {
      ...state,
      city: userAddressDetails.city || '',
      country: userAddressDetails.country || '',
    };

    setState(userAddressDetailsState);
    const err = validate(userAddressDetailsState, validator);
    if (err) {
      setErrors(err);
    } else {
      setErrors({});
    }
  };
  const updateCityPicker = () => {
    const suggestions = cities.filter((el) => {
      var s = el[country];
      return s;
    });
    var list = [];
    suggestions.forEach((element) => {
      element[country].forEach((cl) => {
        var obj = {};
        obj['label'] = cl;
        obj['value'] = cl;
        list.push(obj);
      });
    });
    setCities(list);
  };

  const { city, country, cityList } = state;

  const navigateToRegistrationConfirmDetails = () => {
    if (socialRegistration) {
      updateUser({
        variables: {
          data: {
            city,
            country,
          },
        },
      }).then(async () => {
        await AsyncStorage.removeItem('userBasicDetails');
        await AsyncStorage.removeItem('userAddressDetails');
        navigation.navigate('QuestionnaireIntroduction');
      });
    } else {
      navigation.navigate('RegistrationConfirmDetails', { userDetails: state });
    }
  };

  return (
    <ImageBackground style={styles.imageBackground}>
      <View style={styles.regStepView}>
        <Text style={styles.regStep}>
          Address
          <Text style={styles.oneOutOfThree}> - 3/3</Text>
        </Text>
      </View>
      <ScrollView showsVerticalScrollIndicator={false}>
        <KeyboardAvoidingView style={styles.inputsView}>
          <Dropdown
            mainContainerStyle={{
              width: '100%',
              backgroundColor: 'white',
              marginTop: 5,
            }}
            textStyle={{ fontSize: verticalScale(14) }}
            value={country}
            onValueChange={(val) => onChange('country', val)}
            testID="countryID"
            placeholder="eg. United Kingdom"
            items={countries2}
            checkDropdownErrors={false}
            error={errors.accountCurrency && errors.accountCurrency[0]}
            showDropdownError={''}
            title="Which country do you currently live in?"
          />
          <Dropdown
            mainContainerStyle={{
              width: '100%',
              backgroundColor: 'white',
              marginTop: 5,
            }}
            textStyle={{ fontSize: verticalScale(14) }}
            value={city}
            onValueChange={(val) => onChange('city', val)}
            testID="cityID"
            placeholder="eg. London"
            items={cityList}
            checkDropdownErrors={false}
            error={errors.city && errors.city[0]}
            showDropdownError={''}
            title="Which city do you currently live in?"
          />
        </KeyboardAvoidingView>
      </ScrollView>
      <View>
        <Button
          disabled={Object.keys(errors).length}
          styleContainer={{ marginBottom: scale(24) }}
          title="Next"
          onPressFunc={async () => {
            await AsyncStorage.setItem(
              'userAddressDetails',
              JSON.stringify(state),
            )
              .then(() => navigateToRegistrationConfirmDetails())
              .catch((err) => console.log({ err }));
          }}
        />
      </View>
    </ImageBackground>
  );
};

export default RegistrationAddress;
  • Related