body { font-family: sans-serif; }
.select-container { display: flex; gap: 1rem; }
select { font-size: 1rem; padding: 0.25rem; }
<div id="root"></div><script src="https://unpkg.com/react@18.1.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@18.1.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.17.10/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import * as ReactDOM from 'react-dom/client';
// import {
// type Dispatch,
// type ReactElement,
// type SetStateAction,
// useEffect,
// useRef,
// useState,
// } from 'react';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {
useEffect,
useRef,
useState,
} = React;
// The next section is just a mock API for getting dependent options (like your States/Counties example):
async function getOptionsApi (level: 1): Promise<string[]>;
async function getOptionsApi (
level: 2,
level1Option: string,
): Promise<string[]>;
async function getOptionsApi (
level: 1 | 2,
level1Option?: string,
) {
const OPTIONS: Record<string, string[]> = {
colors: ['red', 'green', 'blue'],
numbers: ['one', 'two', 'three'],
sizes: ['small', 'medium', 'large'],
};
if (level === 1) return Object.keys(OPTIONS);
else if (level1Option) {
const values = OPTIONS[level1Option];
if (!values) throw new Error('Invalid level 1 option');
return values;
}
throw new Error('Invalid level 1 option');
}
// This section includes the React components:
type SelectInputProps = {
options: string[];
selectedOption: string;
setSelectedOption: Dispatch<SetStateAction<string>>;
};
function SelectInput (props: SelectInputProps): ReactElement {
return (
<select
onChange={(ev) => props.setSelectedOption(ev.target.value)}
value={props.selectedOption}
>
{props.options.map((value, index) => (
<option key={`${index}.${value}`} {...{value}}>{value}</option>
))}
</select>
);
}
function App (): ReactElement {
// Use a ref to track whether or not it's the initial render
const isFirstRenderRef = useRef(true);
// State for storing the top level array of options
const [optionsLvl1, setOptionsLvl1] = useState<string[]>([]);
const [selectedLvl1, setSelectedLvl1] = useState('');
// State for storing the options that depend on the selected value from the level 1 options
const [optionsLvl2, setOptionsLvl2] = useState<string[]>([]);
const [selectedLvl2, setSelectedLvl2] = useState('');
// On the first render only, get the top level options from the "API"
// and set the selected value to the first one in the list
useEffect(() => {
const setOptions = async () => {
const opts = await getOptionsApi(1);
setOptionsLvl1(opts);
setSelectedLvl1(opts[0]!);
};
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false;
setOptions();
}
}, []);
// (Except for the initial render) every time the top level option changes,
// get the dependent options from the "API" and set
// the selected dependent value to the first one in the list
useEffect(() => {
const setOptions = async () => {
const opts = await getOptionsApi(2, selectedLvl1);
setOptionsLvl2(opts);
setSelectedLvl2(opts[0]!);
};
if (isFirstRenderRef.current) return;
setOptions();
}, [selectedLvl1]);
return (
<div>
<h1>Dependent select options</h1>
<div className="select-container">
<SelectInput
options={optionsLvl1}
selectedOption={selectedLvl1}
setSelectedOption={setSelectedLvl1}
/>
<SelectInput
options={optionsLvl2}
selectedOption={selectedLvl2}
setSelectedOption={setSelectedLvl2}
/>
</div>
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root')!)
reactRoot.render(<App />);
</script>