Search code examples
javascriptreactjsfunctionreact-hookscascadingdropdown

How can I convert the this cascading dropdown class component to functional one using React Hooks?


I have this cascading dropdown in class component form and it is working fine but I want to convert it to functional one with React Hooks. It is a Country, State, City dropdown. When a user select country, the states under it will load. When they select a state, the cities under it will load. I have changed the constructor part to start using state as seen below. Also, the componentDidMount has been converted to useEffect. The handlers have also been converted.

But the render part below where the form is displayed is giving me tough time. When I tried, I was getting the error that it is not properly arranged. How can I convert the part inside the container div from using this. to be in sync with the rest of the function. I know I'll have to do away with this. but I don't know how to go about it? The sample function is posted below.

function LocationDropdown() {
    const [state, setState] = useState({
            countries : [],
            states : [],
            lgas : [],
            selectedCountry : '--Choose Country--',
            selectedState : '--Choose State--'
        });

    useEffect(() => {
  setState(prevState => ({
            ...prevState,
            countries : [
                { name: 'Nigeria', value: 'nigeria', 
                states: [ {name: 'Abia', value: 'abia', 
                lgas: [
                        {name: "Aba", value: 'aba'},
                        {name: "Oru", value: 'oru'},

        ]}, {name: 'Adamawa', value: 'adamawa', 
                lgas: [
                        {name: 'Demsa', value: 'demsa'},
                        {name: 'Fufure', value: 'fufure'},
        ]}, 
    },  
            ]
   }));
}, [])

    changeCountry(event) {
        this.setState({selectedCountry: event.target.value});
        this.setState({states : this.state.countries.find(cntry => cntry.name === event.target.value).states});
    }

    changeState(event) {
        this.setState({selectedState: event.target.value});
        const stats = this.state.countries.find(cntry => cntry.name === this.state.selectedCountry).states;
        this.setState({lgas : stats.find(stats => stats.name === event.target.value).lgas});
    }

    render() {

        return (
            <div id="container">
                <h2>Cascading or Dependent Dropdown using React</h2>
                <div>
                    <Label>Country</Label>
                    <Select placeholder="Country" value={this.state.selectedCountry} onChange={this.changeCountry}>
                        <option>--Choose Country--</option>
                        {this.state.countries.map((e, key) => {
                            return <option key={key}>{e.name}</option>;
                        })}
                    </Select>
                </div>

                <div>
                    <Label>State</Label>
                    <Select placeholder="State" value={this.state.selectedState} onChange={this.changeState}>
                        <option>--Choose State--</option>
                        {this.state.states.map((e, key) => {
                            return <option key={key}>{e.name}</option>;
                        })}
                    </Select>
                </div>

                <div>
                    <Label>LGA</Label>
                    <Select placeholder="LGA" value={this.state.selectedLga}>
                        <option>--Choose LGA--</option>
                        {this.state.lgas.map((e, key) => {
                            return <option key={key}>{e.name}</option>;
                        })}
                    </Select>
                </div>
            </div>
        )
    }
}

export default LocationDropdown;

Solution

  • I changed your code. I renamed state and setState, so it's not confusing with actual States. Also I changed Select and Label to normal html element so i can test.

    import React, { useEffect, useState } from "react";
    
    function LocationDropdown() {
      const [myData, setMyData] = useState({
        countries: [],
        states: [],
        lgas: [],
        selectedCountry: "--Choose Country--",
        selectedState: "--Choose State--"
      });
    
      useEffect(() => {
        setMyData(prevState => ({
          ...prevState,
          countries: [
            {
              name: "Nigeria",
              value: "nigeria",
              states: [
                {
                  name: "Abia",
                  value: "abia",
                  lgas: [
                    { name: "Aba", value: "aba" },
                    { name: "Oru", value: "oru" }
                  ]
                },
                {
                  name: "Adamawa",
                  value: "adamawa",
                  lgas: [
                    { name: "Demsa", value: "demsa" },
                    { name: "Fufure", value: "fufure" }
                  ]
                }
              ]
            }
          ]
        }));
      }, []);
    
      const mergeAndUpdateMyData = newData => {
        setMyData({ ...myData, ...newData });
      };
    
      const changeCountry = event => {
        mergeAndUpdateMyData({
          selectedCountry: event.target.value,
          states: myData.countries.find(cntry => cntry.name === event.target.value)
            .states
        });
      };
    
      const changeState = event => {
        const stats = myData.countries.find(
          cntry => cntry.name === myData.selectedCountry
        ).states;
        mergeAndUpdateMyData({
          selectedState: event.target.value,
          lgas: stats.find(stats => stats.name === event.target.value).lgas
        });
      };
    
      return (
        <div id="container">
          <h2>Cascading or Dependent Dropdown using React</h2>
          <div>
            <label>Country</label>
            <select
              placeholder="Country"
              value={myData.selectedCountry}
              onChange={changeCountry}
            >
              <option>--Choose Country--</option>
              {myData.countries.map((country, key) => {
                return (
                  <option value={country.name} key={key}>
                    {country.name}
                  </option>
                );
              })}
            </select>
          </div>
    
          <div>
            <label>State</label>
            <select
              placeholder="State"
              value={myData.selectedState}
              onChange={changeState}
            >
              <option>--Choose State--</option>
              {myData.states.map((state, key) => {
                return (
                  <option value={state.name} key={key}>
                    {state.name}
                  </option>
                );
              })}
            </select>
          </div>
    
          <div>
            <label>LGA</label>
            <select placeholder="LGA" value={myData.selectedLga}>
              <option>--Choose LGA--</option>
              {myData.lgas.map((lga, key) => {
                return (
                  <option value={lga.name} key={key}>
                    {lga.name}
                  </option>
                );
              })}
            </select>
          </div>
        </div>
      );
    }
    
    export default LocationDropdown;
    

    EDITED: Please note that in React, when you set a state, the statement is async. This means when you call setMyData, the value of myData is not updated immediately. So you cannot call mergeAndUpdateMyData multiple times in a row.

    By the way, you can use multiple useState in one function components. For example:

    const [countries, setCountries] = useState();
    const [lgas, setLags] = useState();
    ...