DEV Community

Cover image for Cascading Form React Native Improved
RamR
RamR

Posted on • Edited on

Cascading Form React Native Improved

I want to share my 3 approaches of handling cascading form fields.

  1. First one is general common approach, using state variables.
  2. Second is to use ordinary variables and one boolean state variable to trigger the state effect (refresh page).
  3. Third is, dynamic form fields with ordinary variables.

First Approach, using state variables

Here in this post we see the second approach, using ordinary variables to handle cascading form fields based on country, state, city data.

Packages

react-native-element-dropdown
react-native-paper
Enter fullscreen mode Exit fullscreen mode

we are using react-native-element-dropdown for drop down fields

Base Page

Image description

import React, { useState, useEffect } from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { Dropdown } from "react-native-element-dropdown";

export default function App() {
  return (
    <View style={styles.container}>
      <View>
        <Text>Country</Text>
        <ZDropDown
          data={[]}
          labelField=""
          valueField=""
          value={null}
          isFocus={false}
          onChange={null}
        />
        <Text>State</Text>        
        <ZDropDown
          data={[]}
          labelField=""
          valueField=""
          value={null}
          isFocus={false}
          onChange={null}      
        />
        <Text>City</Text>
        <ZDropDown
          data={[]}
          labelField=""
          valueField=""
          value={null}
          isFocus={false}
          onChange={null}      
        />
      </View>
      <View>
        <Text>Selected Country</Text>
        <Text style={styles.selectedValue}>{country.selectedCountry.name}</Text>
        <Text>Selected State</Text>
        <Text style={styles.selectedValue}>{state.selectedState.name}</Text>
        <Text>Selected City</Text>
        <Text style={styles.selectedValue}>{city.selectedCity.name}</Text>
      </View>
      <TouchableOpacity onPress={null} style={styles.clrBtn}>
        <Text style={styles.clrBtnTxt}>Reset</Text>
      </TouchableOpacity>
    </View>
  );
}

const ZDropDown = ({
  data,
  labelField,
  valueField,
  value,
  onFocus,
  onBlur,
  onChange,
  isFocus,
}) => {
  return (
    <Dropdown
      mode={"auto"}
      style={[styles.dropdown, isFocus ? { borderColor: "dodgerblue" } : {}]}
      placeholderStyle={styles.placeholderStyle}
      selectedTextStyle={styles.selectedTextStyle}
      inputSearchStyle={styles.inputSearchStyle}
      iconStyle={styles.iconStyle}
      search={data.length > 5}
      maxHeight={300}
      searchPlaceholder="Search..."
      data={data}
      labelField={labelField}
      valueField={valueField}
      placeholder={!isFocus ? "Select item" : "..."}
      value={value}
      onFocus={onFocus}
      onBlur={onBlur}
      onChange={onChange}
    />
  );
};

const styles = StyleSheet.create({
// style props
});

Enter fullscreen mode Exit fullscreen mode

ZDropDown is a custom component.

Sample data

const listCountry = [
  { countryId: "1", name: "india" },
  { countryId: "2", name: "uk" },
  { countryId: "3", name: "canada" },
  { countryId: "4", name: "us" },
];

const listSate = [
  { stateId: "1", countryId: "1", name: "state1_india" },
  { stateId: "4", countryId: "2", name: "state1_uk" },
  { stateId: "7", countryId: "3", name: "state1_canada" },
  { stateId: "10", countryId: "4", name: "state1_us" },
];

const listCity = [
  {
    cityId: "1",
    stateId: "1",
    countryId: "1",
    name: "city1_state1_country1",
  },
   {
    cityId: "6",
    stateId: "2",
    countryId: "1",
    name: "city6_state2_country1",
  },
  {
    cityId: "7",
    stateId: "3",
    countryId: "1",
    name: "city7_state3_country1",
  }, 
  {
    cityId: "23",
    stateId: "8",
    countryId: "3",
    name: "city23_state8_country3",
  }, 
  {
    cityId: "30",
    stateId: "10",
    countryId: "4",
    name: "city30_state10_country4",
  }, 
  {
    cityId: "35",
    stateId: "12",
    countryId: "4",
    name: "city35_state12_country4",
  },
  {
    cityId: "36",
    stateId: "12",
    countryId: "4",
    name: "city36_state12_country4",
  },
];
Enter fullscreen mode Exit fullscreen mode

Form Variables

In the first approach we used 4 state variables but here we use 4 ordinary variables (3 for dropdown, 1 for focus field) and a state variable which is used to switch and trigger the state effect (refresh page).

var country = {
  list: [],
  selectedCountry: {},
  selectedValue: null,
};

var state = {
  list: [],
  selectedState: {},
  selectedValue: null,
};

var city = {
  list: [],
  selectedCity: {},
  selectedValue: null,
};

var focusField = '';

export default function App() {
 const [refreshPage, setRefreshPage] = useState(false);

  return (
    <View style={styles.container}>
      <View>
        <Text>Country</Text>
        <ZDropDown
          data={country.list}
          labelField="name"
          valueField="countryId"
          value={country.selectedValue}
          isFocus={focusField === 'country'}
          onChange={null}
        />
        <Text>State</Text>
        <ZDropDown
          data={state.list}
          labelField="name"
          valueField="stateId"
          value={state.selectedValue}
          isFocus={focusField === 'state'}
          onChange={null}
        />
        <Text>City</Text>
        <ZDropDown
          data={city.list}
          labelField="name"
          valueField="cityId"
          value={city.selectedValue}
          isFocus={focusField === 'city'}
          onChange={null}
        />
      </View>
      . . .
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Focus and Blur events get triggered more than onChange event so it is better to maintain a separate variable to represent the focus field and avoid data mess up situations.

Load Country

Load country dropdown from the sample data. (you can use api call)

export default function App() {
. . .
  const loadCountry = () => {
    // load data from api call
    country.list = [...listCountry];
    // switching value will refresh the ui
    setRefreshPage(!refreshPage);
  };

  useEffect(() => {
    loadCountry();
  }, []);

return (
. . .
)
Enter fullscreen mode Exit fullscreen mode

Focus / Blur

We're using a function to set the focus field and refresh the page by switching the state variable(refreshPage)

  const changeFocusField = (fld = '') => {
    focusField = fld;
    setRefreshPage(!refreshPage);
  };
Enter fullscreen mode Exit fullscreen mode
        <Text>Country</Text>
        <ZDropDown
          . . .
          isFocus={focusField === 'country'}
          onFocus={() => changeFocusField('country')}
          onBlur={() => changeFocusField('')}
          onChange={null}
        />
        <Text>State</Text>
        <ZDropDown
        . . .
          isFocus={focusField === 'state'}
          onFocus={() => changeFocusField('state')}
          onBlur={() => changeFocusField('')}
          onChange={null}
        />
        <Text>City</Text>
        <ZDropDown        
        . . .
          isFocus={focusField === 'city'}
          onFocus={() => changeFocusField('city')}
          onBlur={() => changeFocusField('')}
          onChange={null}       
        />
Enter fullscreen mode Exit fullscreen mode

Image description

We are half way done now.

Load State STATE

On country selected, we need to load the respective states STATES based on the country selection.

Update country field, focus off country and load STATES based on country.

  <Text>Country</Text>
  <ZDropDown
    . . .
    onChange={(item) => {
      country.selectedCountry = item;
      country.selectedValue = item.countryId;
      focusField = '';
      loadState();
    }}
  />
Enter fullscreen mode Exit fullscreen mode

Did you see the difference in the first approach? Previously there were 3 state variables updated but here only one state variable gets updated.

When country changed, both states and cities will be changed. So before setting up the new value we need to clear the existing data.

 const loadState = async () => {
   state = { list: [], selectedState: {}, selectedValue: null };
   city = { list: [], selectedCity: {}, selectedValue: null };
   const arr = listSate.filter(
     (ele) => ele.countryId === country.selectedValue
   );
   if (arr.length) {
     state.list = arr;
   }
   refreshPage(!refreshPage);
 };
Enter fullscreen mode Exit fullscreen mode

Image description

Load City

And load city field based on country and state.

    <Text>State</Text>
    <ZDropDown
      . . .
      onChange={(item) => {
        state.selectedValue = item.stateId;
        state.selectedState = item;
        changeFocusField('');
        loadCity();
      }}
     />
Enter fullscreen mode Exit fullscreen mode
  const loadCity = async () => {
    city = { list: [], selectedCity: {}, selectedValue: null };
    const arr = listCity.filter((ele) => ele.stateId === state.selectedValue);
    if (arr.length) city.list = arr;
    setRefreshPage(!refreshPage);
  };
Enter fullscreen mode Exit fullscreen mode

Image description

All set, the form fields are working properly now.

Image description

2 more additional features, one is resetting the page and the other one is show warning.

Reset page

Form variables and focus variable should be cleared.

. . .
  const resetForm = () => {
    country = {list: [...listCountry],selectedCountry: {}, selectedValue: null};
    state = { list: [], selectedState: {}, selectedValue: null };
    city = { list: [], selectedCity: {}, selectedValue: null };
    focusField = '';
    setRefreshPage(!refreshPage);
  };
. . .

  <TouchableOpacity onPress={() => resetForm()} style={styles.clrBtn}>
    <Text style={styles.clrBtnTxt}>Reset</Text>
  </TouchableOpacity>
. . .

Enter fullscreen mode Exit fullscreen mode

Warning

We have to show a warning msg if the parent field value is null. For that we are using SnackBar component from paper.

import { Snackbar } from "react-native-paper";
. . .
var snackMsg = '';
export default function App() {
  . . .
  const [visible, setVisible] = useState(false);

  const onToggleSnackBar = () => setVisible(!visible);
  const onDismissSnackBar = () => setVisible(false);
  . . .

  return (
    <View style={styles.container}>
    . . .
      <Snackbar duration={2000} visible={visible} onDismiss={onDismissSnackBar}>
        {snackMsg}
      </Snackbar>
    </View>
  );
}

Enter fullscreen mode Exit fullscreen mode

We moved snackMsg to ordinary variable from state variable.

Since State and City fields have parent field, they have to be validated.

        <Text>State</Text>
        <ZDropDown          
          onFocus={() => {
            focusField('state');
            if (!country.selectedValue) {
              snackMsg = 'Select country';
              onToggleSnackBar();
              changeFocusField('country');
            }
          }}
          . . .
        />
        <Text>City</Text>
        <ZDropDown          
          onFocus={() => {
            focusField('city');
            if (!country.selectedValue) {
              snackMsg = 'Select country';
              onToggleSnackBar();
              changeFocusField('country');
            } else if (!state.selectedValue) {
              snackMsg = 'Select state';
              onToggleSnackBar();
              changeFocusField('state');
            }
          }}
          . . .
        />
Enter fullscreen mode Exit fullscreen mode

Image description

Done.

Thank you.

Full code reference here

Top comments (0)