Wednesday, April 1, 2026

React.js multiselect dropdown using Material UI

React Multi-Select Country → State → City Dropdown (Service Based)

React Material UI Multi-Select Dropdown (Country → State → City)

This tutorial explains how to build a React Material UI multi-select dropdown using chips, where:

  • Country dropdown loads from API
  • Based on selected countries, states dropdown loads from API
  • Based on selected states, cities dropdown loads from API
  • Uses a Service-Based Approach (Enterprise friendly)

Folder Structure

src/
  components/
    location/
      CountryMultiSelect.jsx
      StateMultiSelect.jsx
      CityMultiSelect.jsx
      LocationSelector.jsx

  services/
    axiosInstance.js
    locationService.js

  App.js
  index.js
      

Step 1: Create React App

npx create-react-app location-dropdown-app
cd location-dropdown-app
    

Step 2: Install Dependencies

npm install @mui/material @emotion/react @emotion/styled axios
    

Step 3: Create .env File

Create .env in root folder:

REACT_APP_API_BASE_URL=https://localhost:5001/api
    

Important: Restart your React server after adding .env.

Step 4: Create axiosInstance.js

File: src/services/axiosInstance.js

import axios from "axios";

/**
 * Central Axios instance.
 * All common configuration is done here.
 * Example: base URL, headers, tokens, interceptors.
 */
const axiosInstance = axios.create({
  baseURL: process.env.REACT_APP_API_BASE_URL, // Read from .env file
  headers: {
    "Content-Type": "application/json",
  },
});

/**
 * OPTIONAL:
 * If you want to attach JWT token later automatically,
 * you can uncomment this interceptor.
 */
// axiosInstance.interceptors.request.use((config) => {
//   const token = localStorage.getItem("token");
//   if (token) {
//     config.headers.Authorization = `Bearer ${token}`;
//   }
//   return config;
// });

export default axiosInstance;
    

Step 5: Create locationService.js

File: src/services/locationService.js

import axiosInstance from "./axiosInstance";

/**
 * Service Layer:
 * UI components should not call axios directly.
 * All API calls related to location go here.
 */
export const locationService = {

  // Load countries
  getCountries: async () => {
    const response = await axiosInstance.get("/countries");
    return response.data;
  },

  // Load states by selected countries
  getStatesByCountries: async (countryIds) => {
    const response = await axiosInstance.post("/states/by-countries", {
      countryIds,
    });
    return response.data;
  },

  // Load cities by selected states
  getCitiesByStates: async (stateIds) => {
    const response = await axiosInstance.post("/cities/by-states", {
      stateIds,
    });
    return response.data;
  },
};
    

Step 6: CountryMultiSelect.jsx

File: src/components/location/CountryMultiSelect.jsx

import React, { useEffect, useState } from "react";
import { Autocomplete, TextField, Chip, CircularProgress } from "@mui/material";
import { locationService } from "../../services/locationService";

/**
 * Country dropdown component.
 * - Loads country list from API
 * - Allows multi-select with chips
 */
export default function CountryMultiSelect({ value, onChange }) {
  const [countries, setCountries] = useState([]);
  const [loading, setLoading] = useState(false);

  // Load countries only once when component mounts
  useEffect(() => {
    loadCountries();
  }, []);

  const loadCountries = async () => {
    try {
      setLoading(true);
      const data = await locationService.getCountries();
      setCountries(data);
    } catch (error) {
      console.error("Error loading countries:", error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Autocomplete
      multiple
      options={countries}
      value={value}
      loading={loading}
      getOptionLabel={(option) => option.name}
      onChange={(event, newValue) => onChange(newValue)}
      renderTags={(value, getTagProps) =>
        value.map((option, index) => (
          <Chip
            label={option.name}
            {...getTagProps({ index })}
            key={option.id}
          />
        ))
      }
      renderInput={(params) => (
        <TextField
          {...params}
          label="Select Countries"
          placeholder="Choose countries..."
          InputProps={{
            ...params.InputProps,
            endAdornment: (
              <>
                {loading ? <CircularProgress size={20} /> : null}
                {params.InputProps.endAdornment}
              </>
            ),
          }}
        />
      )}
    />
  );
}
    

Step 7: StateMultiSelect.jsx

File: src/components/location/StateMultiSelect.jsx

import React, { useEffect, useState } from "react";
import { Autocomplete, TextField, Chip, CircularProgress } from "@mui/material";
import { locationService } from "../../services/locationService";

/**
 * State dropdown component.
 * - Loads states from API based on selected countryIds
 * - Clears selection if countryIds becomes empty
 */
export default function StateMultiSelect({ countryIds, value, onChange }) {
  const [states, setStates] = useState([]);
  const [loading, setLoading] = useState(false);

  // Reload states whenever countryIds change
  useEffect(() => {
    if (!countryIds || countryIds.length === 0) {
      setStates([]);
      onChange([]); // reset selection
      return;
    }

    loadStates();
  }, [countryIds]);

  const loadStates = async () => {
    try {
      setLoading(true);
      const data = await locationService.getStatesByCountries(countryIds);
      setStates(data);
    } catch (error) {
      console.error("Error loading states:", error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Autocomplete
      multiple
      options={states}
      value={value}
      loading={loading}
      getOptionLabel={(option) => option.name}
      onChange={(event, newValue) => onChange(newValue)}
      renderTags={(value, getTagProps) =>
        value.map((option, index) => (
          <Chip
            label={option.name}
            {...getTagProps({ index })}
            key={option.id}
          />
        ))
      }
      renderInput={(params) => (
        <TextField
          {...params}
          label="Select States"
          placeholder="Choose states..."
          InputProps={{
            ...params.InputProps,
            endAdornment: (
              <>
                {loading ? <CircularProgress size={20} /> : null}
                {params.InputProps.endAdornment}
              </>
            ),
          }}
        />
      )}
    />
  );
}
    

Step 8: CityMultiSelect.jsx

File: src/components/location/CityMultiSelect.jsx

import React, { useEffect, useState } from "react";
import { Autocomplete, TextField, Chip, CircularProgress } from "@mui/material";
import { locationService } from "../../services/locationService";

/**
 * City dropdown component.
 * - Loads cities from API based on selected stateIds
 * - Clears selection if stateIds becomes empty
 */
export default function CityMultiSelect({ stateIds, value, onChange }) {
  const [cities, setCities] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!stateIds || stateIds.length === 0) {
      setCities([]);
      onChange([]); // reset selection
      return;
    }

    loadCities();
  }, [stateIds]);

  const loadCities = async () => {
    try {
      setLoading(true);
      const data = await locationService.getCitiesByStates(stateIds);
      setCities(data);
    } catch (error) {
      console.error("Error loading cities:", error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Autocomplete
      multiple
      options={cities}
      value={value}
      loading={loading}
      getOptionLabel={(option) => option.name}
      onChange={(event, newValue) => onChange(newValue)}
      renderTags={(value, getTagProps) =>
        value.map((option, index) => (
          <Chip
            label={option.name}
            {...getTagProps({ index })}
            key={option.id}
          />
        ))
      }
      renderInput={(params) => (
        <TextField
          {...params}
          label="Select Cities"
          placeholder="Choose cities..."
          InputProps={{
            ...params.InputProps,
            endAdornment: (
              <>
                {loading ? <CircularProgress size={20} /> : null}
                {params.InputProps.endAdornment}
              </>
            ),
          }}
        />
      )}
    />
  );
}
    

Step 9: LocationSelector.jsx (Parent Component)

File: src/components/location/LocationSelector.jsx

import React, { useState } from "react";
import { Box, Typography, Paper } from "@mui/material";

import CountryMultiSelect from "./CountryMultiSelect";
import StateMultiSelect from "./StateMultiSelect";
import CityMultiSelect from "./CityMultiSelect";

/**
 * Parent component:
 * - Stores selected countries, states, cities
 * - Controls when to display state/city dropdowns
 * - Resets dependent dropdown selections automatically
 */
export default function LocationSelector() {
  const [selectedCountries, setSelectedCountries] = useState([]);
  const [selectedStates, setSelectedStates] = useState([]);
  const [selectedCities, setSelectedCities] = useState([]);

  // Extract IDs for backend request payload
  const countryIds = selectedCountries.map((c) => c.id);
  const stateIds = selectedStates.map((s) => s.id);

  return (
    <Paper elevation={3} sx={{ p: 3, borderRadius: 2 }}>
      <Typography variant="h5" gutterBottom>
        Location Selector (Multi Select)
      </Typography>

      <Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>

        {/* Countries Dropdown */}
        <CountryMultiSelect
          value={selectedCountries}
          onChange={(countries) => {
            setSelectedCountries(countries);

            // Reset dependent dropdown selections when countries change
            setSelectedStates([]);
            setSelectedCities([]);
          }}
        />

        {/* States Dropdown */}
        {selectedCountries.length > 0 && (
          <StateMultiSelect
            countryIds={countryIds}
            value={selectedStates}
            onChange={(states) => {
              setSelectedStates(states);

              // Reset cities when states change
              setSelectedCities([]);
            }}
          />
        )}

        {/* Cities Dropdown */}
        {selectedStates.length > 0 && (
          <CityMultiSelect
            stateIds={stateIds}
            value={selectedCities}
            onChange={(cities) => setSelectedCities(cities)}
          />
        )}

        {/* Debug Output */}
        <Box>
          <Typography variant="subtitle1">Selected Values:</Typography>
          <pre style={{ background: "#0d1117", padding: "10px" }}>
            {JSON.stringify(
              {
                countries: selectedCountries,
                states: selectedStates,
                cities: selectedCities,
              },
              null,
              2
            )}
          </pre>
        </Box>

      </Box>
    </Paper>
  );
}
    

Step 10: App.js

File: src/App.js

import React from "react";
import { Container } from "@mui/material";
import LocationSelector from "./components/location/LocationSelector";

/**
 * Main App Component
 */
export default function App() {
  return (
    <Container sx={{ mt: 5 }}>
      <LocationSelector />
    </Container>
  );
}
    

Step 11: Run the Project

npm start
    

Expected API Response Format

GET /api/countries

[
  { "id": 1, "name": "USA" },
  { "id": 2, "name": "India" }
]
      

POST /api/states/by-countries

Request Body:
{
  "countryIds": [1, 2]
}

Response:
[
  { "id": 101, "name": "Texas", "countryId": 1 },
  { "id": 102, "name": "California", "countryId": 1 },
  { "id": 201, "name": "Telangana", "countryId": 2 }
]
      

POST /api/cities/by-states

Request Body:
{
  "stateIds": [101, 201]
}

Response:
[
  { "id": 1001, "name": "Houston", "stateId": 101 },
  { "id": 2001, "name": "Hyderabad", "stateId": 201 }
]
      

Step 12: Zip and Upload to GitHub

Windows

Right click folder → Send to → Compressed (zip)

Mac/Linux

zip -r location-dropdown-app.zip location-dropdown-app
    

✅ You now have a complete React project using Material UI multi-select dropdown with chips using a clean service-based architecture.

No comments:

Post a Comment

React.js multiselect dropdown using Material UI

React Multi-Select Country → State → City Dropdown (Service Based) React Material UI Multi-Select Dropdown (Count...