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.