How to Create Dependent Dropdowns in React

Intermediate12 min interview
Skills tested:
Tracking two pieces of state: the first selected value and the derived options for the second dropdownResetting the dependent dropdown selection when the parent selection changesExtending the pattern to fetch dependent options from an API with useEffectHandling three levels of chained dropdowns cleanlyControlling both selects with value and onChange for predictable behavior

Advertisement

🧩 Scenario

Dependent dropdowns appear in shipping forms (country to state), product configurators (brand to model to variant), location pickers (region to city), and filter panels. The pattern is: one selection drives the options available in the next. State must track both the current selection and the derived options list, and the dependent selection must reset when its parent changes.

Architecture Walkthrough

State Design

A two-level dependent dropdown needs three state variables: the first selected value, the list of options for the second dropdown, and the second selected value. When the first selection changes, two things must happen simultaneously: set the new first value and reset the second value to empty (so the old selection does not persist with a new option set that may not include it).

For static data, the second options list can be derived directly during render from the first selection rather than stored in state: const secondOptions = data[firstValue] ?? []. This simplifies the state to just the two selected values. Only when options come from an async source (API) do you need to store the options list in state.

API-Driven Dependent Options

When the second dropdown's options come from an API, you need a useEffect that watches the first selected value and fetches the new options when it changes. Clear the second selection to empty at the start of the effect so the user is not left with a stale selection while new options are loading. Track an isLoading state to disable the second dropdown while the fetch is in progress.

Resetting on Parent Change

The most common mistake is forgetting to reset the dependent value. After changing the first selection, if the second value remains as "Delhi" but the new country is "USA" which has no "Delhi" option, the controlled select cannot find a matching option and renders with the placeholder invisible but the old value still in state. Always reset dependent values in the parent's onChange handler.


Key Code Explained

// Static data: derive second options during render (no extra state)
const REGIONS: Record<string, string[]> = {
  India: ['Delhi', 'Mumbai', 'Bangalore', 'Chennai'],
  USA: ['New York', 'California', 'Texas', 'Florida'],
  Germany: ['Berlin', 'Munich', 'Hamburg'],
};

function StaticDependentDropdowns() {
  const [country, setCountry] = useState('');
  const [city, setCity] = useState('');

  // Derived during render — no state needed for the options list
  const cities = REGIONS[country] ?? [];

  const handleCountryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setCountry(e.target.value);
    setCity('');  // reset dependent selection immediately
  };

  return (
    <div>
      <select value={country} onChange={handleCountryChange}>
        <option value="" disabled>Select Country</option>
        {Object.keys(REGIONS).map((c) => (
          <option key={c} value={c}>{c}</option>
        ))}
      </select>

      <select
        value={city}
        onChange={(e) => setCity(e.target.value)}
        disabled={!country}  // disabled until a country is chosen
      >
        <option value="" disabled>
          {country ? 'Select City' : 'Select a country first'}
        </option>
        {cities.map((c) => (
          <option key={c} value={c}>{c}</option>
        ))}
      </select>

      {country && city && (
        <p>Selected: {city}, {country}</p>
      )}
    </div>
  );
}


// API-driven: fetch dependent options with useEffect
interface State {
  code: string;
  name: string;
}

function ApiDependentDropdowns() {
  const [countryCode, setCountryCode] = useState('');
  const [stateCode, setStateCode] = useState('');
  const [states, setStates] = useState<State[]>([]);
  const [isLoadingStates, setIsLoadingStates] = useState(false);

  const countries = [
    { code: 'IN', name: 'India' },
    { code: 'US', name: 'United States' },
  ];

  useEffect(() => {
    if (!countryCode) {
      setStates([]);
      return;
    }

    let cancelled = false;

    async function fetchStates() {
      setIsLoadingStates(true);
      setStateCode('');  // reset before fetch completes
      try {
        const res = await fetch(`/api/states?country=${countryCode}`);
        const data: State[] = await res.json();
        if (!cancelled) {
          setStates(data);
        }
      } finally {
        if (!cancelled) setIsLoadingStates(false);
      }
    }

    fetchStates();

    return () => {
      cancelled = true;  // prevent stale state if country changes quickly
    };
  }, [countryCode]);

  return (
    <div>
      <select
        value={countryCode}
        onChange={(e) => {
          setCountryCode(e.target.value);
          setStateCode('');
        }}
      >
        <option value="" disabled>Select Country</option>
        {countries.map((c) => (
          <option key={c.code} value={c.code}>{c.name}</option>
        ))}
      </select>

      <select
        value={stateCode}
        onChange={(e) => setStateCode(e.target.value)}
        disabled={!countryCode || isLoadingStates}
      >
        <option value="" disabled>
          {isLoadingStates ? 'Loading...' : 'Select State'}
        </option>
        {states.map((s) => (
          <option key={s.code} value={s.code}>{s.name}</option>
        ))}
      </select>
    </div>
  );
}

The cancelled flag in the useEffect cleanup prevents a race condition: if the user changes the country again before the first fetch completes, the stale response is discarded. Without this flag, a slow first response arriving after a fast second response would overwrite the correct states list with outdated data.


Tradeoffs

Options sourceState neededReset complexityLoading state needed
Static object2 selected values onlyLow (derive in render)No
API2 selected values + options arrayMedium (reset in effect + onChange)Yes

What Interviewers Actually Check

  • Whether you reset the dependent value when the parent changes
  • Whether you disable the dependent dropdown when no parent is selected
  • Whether you handle the API case with useEffect, loading state, and cleanup
  • Whether you know to derive the options list from state when data is static
  • Whether you use controlled selects for both dropdowns

Follow-Up Questions

  1. How would you extract the dependent dropdown logic into a custom hook useDependentSelect to make the three-level case composable?
  2. How would you cache the fetched states list so switching back to a previously-selected country does not re-fetch?
  3. How would react-query or SWR simplify the API-driven dependent dropdown compared to the manual useEffect approach?
  4. How would you persist the user's selections in the URL query string so the page reloads with the same selections pre-filled?
  5. How would you implement a searchable dependent dropdown with thousands of options using virtualization?

Common Candidate Mistakes

  • Not resetting the dependent value when the parent changes, leaving a stale selection that points to an option that no longer exists in the new list
  • Not disabling the dependent select while the parent has no selection, confusing users about which options are valid
  • Not handling the loading state when fetching dependent options, showing an empty dropdown with no feedback
  • Not cleaning up the useEffect fetch, causing a race condition when the user changes the parent selection quickly
  • Storing the full country object in state instead of just the code, making the dependency array and API call awkward

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you build a two-level dependent dropdown with static data and reset the dependent value on parent change?
  • Can you extend the pattern to fetch dependent options from an API with useEffect?
  • Can you disable the dependent dropdown while loading or while no parent is selected?
  • Can you handle a race condition when the parent selection changes before the previous fetch completes?
  • Can you derive second options from static data during render rather than storing them in state?

Summary

Dependent dropdowns chain two or more selects so that the second set of options derives from the first selection. The key state design principle is: store the selected values as state, and for static data, derive the options list during render instead of storing it as state. This keeps the state minimal: two selected values and nothing else.

When the first selection changes, always reset the dependent selection to empty immediately in the same handler. Without this, the old dependent value remains in state pointing to an option that may not exist in the new options list, making the controlled select display incorrectly.

For API-driven options, use useEffect watching the first selected value to fetch the new options. Set the dependent selection to empty at the start of the fetch, disable the dependent select while loading, and add a cleanup flag to discard stale responses if the user changes the parent selection before the previous fetch completes.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

How do I reset the second dropdown when the first selection changes?

Set the second dropdown value state back to an empty string inside the first dropdown onChange handler, before or alongside setting the new options list.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement