Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Create a cascading dropdown in React by controlling each <select> with state, deriving each child list from its parent selection, and clearing descendants whenever an ancestor changes. The example below uses native React controls and local country, state, and city data; it does not require a dropdown library or a network API.

What is a cascading dropdown?

A cascading dropdown is a sequence of interdependent dropdowns: the choices in a child control depend on the selection in its parent. For example, choosing a country determines which states are available, and choosing a state determines which cities are available. Syncfusion’s React documentation describes this same country-state-city pattern.

Build the dropdowns with controlled React state

For a small form, native HTML selects are sufficient. Keep the selected country, state, and city IDs in React state. Derive the state and city options from those IDs rather than maintaining separate state arrays that duplicate what the source data already provides. React recommends avoiding redundant state because it can become inconsistent; see Managing State.

1. Define the data and selection state

This example uses stable string IDs and separate arrays for each level. Replace the illustrative entries with the data for your app.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { useState } from 'react';

const countries = [
  { id: 'us', name: 'United States' },
  { id: 'ca', name: 'Canada' },
];

const states = [
  { id: 'ca', countryId: 'us', name: 'California' },
  { id: 'ny', countryId: 'us', name: 'New York' },
  { id: 'on', countryId: 'ca', name: 'Ontario' },
];

const cities = [
  { id: 'la', stateId: 'ca', name: 'Los Angeles' },
  { id: 'sf', stateId: 'ca', name: 'San Francisco' },
  { id: 'nyc', stateId: 'ny', name: 'New York City' },
  { id: 'tor', stateId: 'on', name: 'Toronto' },
];

function LocationForm() {
  const [countryId, setCountryId] = useState('');
  const [stateId, setStateId] = useState('');
  const [cityId, setCityId] = useState('');

  const availableStates = states.filter(
    (state) => state.countryId === countryId
  );
  const availableCities = cities.filter(
    (city) => city.stateId === stateId
  );

  // Add the handlers and JSX shown below.
}

The IDs connect records across levels; the displayed names are labels for people. The entries are illustrative, not a complete or authoritative geographic dataset.

2. Update a parent and clear stale descendants

When the country changes, clear both the selected state and city. When the state changes, clear the city. Otherwise, a previously selected child may remain in state even though it no longer belongs to the new parent.

function handleCountryChange(event) {
  setCountryId(event.target.value);
  setStateId('');
  setCityId('');
}

function handleStateChange(event) {
  setStateId(event.target.value);
  setCityId('');
}

function handleCityChange(event) {
  setCityId(event.target.value);
}

3. Render labeled, controlled selects

Each select receives a value from state and an onChange handler that updates that state. React requires controlled selects to update their backing value synchronously in the change handler; without that update, React restores the supplied value. React’s select documentation covers controlled values, options, labels, disabled controls, and form names.

return (
  <form>
    <label htmlFor="country">Country</label>
    <select
      id="country"
      name="country"
      value={countryId}
      onChange={handleCountryChange}
    >
      <option value="">Select a country</option>
      {countries.map((country) => (
        <option key={country.id} value={country.id}>
          {country.name}
        </option>
      ))}
    </select>

    <label htmlFor="state">State or province</label>
    <select
      id="state"
      name="state"
      value={stateId}
      onChange={handleStateChange}
      disabled={!countryId}
    >
      <option value="">Select a state or province</option>
      {availableStates.map((state) => (
        <option key={state.id} value={state.id}>
          {state.name}
        </option>
      ))}
    </select>

    <label htmlFor="city">City</label>
    <select
      id="city"
      name="city"
      value={cityId}
      onChange={handleCityChange}
      disabled={!stateId}
    >
      <option value="">Select a city</option>
      {availableCities.map((city) => (
        <option key={city.id} value={city.id}>
          {city.name}
        </option>
      ))}
    </select>
  </form>
);

Place the handlers and return statement inside the component, then export LocationForm as needed by your app. The disabled attributes prevent choosing a child before its parent; the placeholder options make the current selection clear. The name attributes allow the selected values to be included in ordinary form submission.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why clearing child selections matters

Filtering the visible options alone is not enough. Suppose a user selects California and Los Angeles, then changes the country to Canada. Los Angeles is no longer a valid choice, so the country handler clears both state and city. The state handler performs the same cleanup one level down. This keeps the displayed options and selected values consistent.

Use a dropdown library only when its features fit

Native selects are a straightforward choice for a basic form. A component library may be appropriate when your project needs richer behavior or already uses a design system, but it adds a dependency and its own API and interaction patterns. Syncfusion’s EJ2 React cascading dropdown example uses DropDownListComponent, field mappings, filtering queries, change handlers, and explicit enable, disable, and clear operations. Those are Syncfusion-specific implementation details, not requirements for React.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When the options come from an API

The code above filters local arrays; it does not fetch data. For remote data, load the child options using the selected parent ID, and represent loading, empty, and error states in the interface. Clear descendant selections when the parent changes, and ensure that a response for an earlier parent cannot replace the options for the current selection. The appropriate fetching and cancellation approach depends on your app’s data layer, so there is no single API pattern implied by the local example.

Common mistakes to avoid

  • Leaving a stale child selected: clear all descendant IDs when an ancestor changes.
  • Using an uncontrolled value by accident: for controlled selects, pair value with an updating onChange.
  • Setting selected on an option: in React, set the parent select’s value or defaultValue instead.
  • Omitting a label: associate each visible label with its select using htmlFor and a matching id, or wrap the select in a label. This communicates the control’s caption to screen readers and lets users activate the control through its label.
  • Saving derived option lists as duplicate state: filter the source data from the selected parent unless you have a specific reason to store a separate fetched result.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.