Skip to main content
Version: v0.30.x

Dependent selects: country, state, city

Three pickers where each one's options depend on the one before it: pick a country to get its states, then a state to get its cities. Each field waits, disabled, until the one above it has a value, and changing an earlier choice clears the ones after it.

Loading…
<View style={{ gap: 14 }}>
  <CountryPicker
    label="Country"
    countries={COUNTRIES}
    value={country}
    onChange={(code) => {
      setCountry(code);
      setState(null);
      setCity(null);
    }}
  />
  <SelectPicker
    label="State or province"
    placeholder={country ? 'Select…' : 'Choose a country first'}
    disabled={!country}
    options={toOptions(states)}
    value={state}
    onChange={(next) => {
      setState(next);
      setCity(null);
    }}
  />
  <SelectPicker
    label="City"
    placeholder={state ? 'Select…' : 'Choose a state first'}
    disabled={!state}
    options={toOptions(cities)}
    value={city}
    onChange={setCity}
  />
</View>

How it works​

  • The options come from the value above. states and cities are worked out on each render from country and state, so there's nothing to keep in sync.
  • A new choice clears what's below it. Changing the country resets the state and city, and changing the state resets the city, so a city never belongs to another country.
  • A field waits for the one above. disabled greys it out until there's something to pick, and the placeholder says what to choose first.
  • countries limits CountryPicker to the countries you ship to, with their flags and names in any language.

With a server​

When the lists come from an API, load each one when its parent changes. With loadOptions, the parent's value goes into the request, and a key starts the list over when it changes:

<SelectPicker
key={country ?? 'none'}
label="City"
disabled={!country}
loadOptions={(query) => api.cities({ country, query })}
value={city}
onChange={setCity}
/>

For a short list that doesn't need search, fetch it in an effect when the parent changes and pass it as options, with loading while it's on its way.

In a form​

With React Hook Form, watch the parent and clear the children with setValue:

const country = watch('country');

<Controller
name="country"
control={control}
render={({ field }) => (
<CountryPicker
label="Country"
value={field.value}
onChange={(code) => {
field.onChange(code);
setValue('state', null);
setValue('city', null);
}}
/>
)}
/>