Last active
October 8, 2025 04:50
-
-
Save RoyalIcing/706f8b1d7ef13f4eb40a90f9dd8a3ac2 to your computer and use it in GitHub Desktop.
Simple interaction: one select changes the options of a second with a remote data source https://x.com/ryanflorence/status/1975402393534943529 https://stackblitz.com/edit/vitejs-vite-7qlpha4g?file=src%2FStateAndCityFieldset.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { setState } from "react"; | |
| class SharedState<State> extends EventTarget { | |
| #state: State; | |
| constructor(state: State) { | |
| super(); | |
| this.#state = state; | |
| } | |
| get current() { | |
| return this.#state; | |
| } | |
| modify(change: (state: State) => void) { | |
| change(this.#state); | |
| this.dispatchEvent(new CustomEvent("statechange")); | |
| } | |
| } | |
| function useSharedState<State>(sharedState: SharedState<State>) { | |
| return useSyncExternalStore( | |
| (subscribe) => { | |
| sharedState.addEventListener("statechange", subscribe); | |
| return () => sharedState.removeEventListener("statechange", subscribe); | |
| }, | |
| () => sharedState.current, | |
| () => sharedState.current | |
| ); | |
| } | |
| const states = new Map<string, string>([ | |
| ["AL", "Alabama"], | |
| ["AK", "Alaska"], | |
| ["AZ", "Arizona"], | |
| ["AR", "Arkansas"], | |
| ["CA", "California"], | |
| ["CO", "Colorado"], | |
| ["CT", "Connecticut"], | |
| ["DE", "Delaware"], | |
| ["FL", "Florida"], | |
| ["GA", "Georgia"], | |
| ["HI", "Hawaii"], | |
| ["ID", "Idaho"], | |
| ["IL", "Illinois"], | |
| ["IN", "Indiana"], | |
| ["IA", "Iowa"], | |
| ["KS", "Kansas"], | |
| ["KY", "Kentucky"], | |
| ["LA", "Louisiana"], | |
| ["ME", "Maine"], | |
| ["MD", "Maryland"], | |
| ["MA", "Massachusetts"], | |
| ["MI", "Michigan"], | |
| ["MN", "Minnesota"], | |
| ["MS", "Mississippi"], | |
| ["MO", "Missouri"], | |
| ["MT", "Montana"], | |
| ["NE", "Nebraska"], | |
| ["NV", "Nevada"], | |
| ["NH", "New Hampshire"], | |
| ["NJ", "New Jersey"], | |
| ["NM", "New Mexico"], | |
| ["NY", "New York"], | |
| ["NC", "North Carolina"], | |
| ["ND", "North Dakota"], | |
| ["OH", "Ohio"], | |
| ["OK", "Oklahoma"], | |
| ["OR", "Oregon"], | |
| ["PA", "Pennsylvania"], | |
| ["RI", "Rhode Island"], | |
| ["SC", "South Carolina"], | |
| ["SD", "South Dakota"], | |
| ["TN", "Tennessee"], | |
| ["TX", "Texas"], | |
| ["UT", "Utah"], | |
| ["VT", "Vermont"], | |
| ["VA", "Virginia"], | |
| ["WA", "Washington"], | |
| ["WV", "West Virginia"], | |
| ["WI", "Wisconsin"], | |
| ["WY", "Wyoming"], | |
| ["DC", "District of Columbia"], | |
| ["AS", "American Samoa"], | |
| ["GU", "Guam"], | |
| ["MP", "Northern Mariana Islands"], | |
| ["PR", "Puerto Rico"], | |
| ["UM", "United States Minor Outlying Islands"], | |
| ["VI", "Virgin Islands, U.S."], | |
| ]); | |
| const statesToCities = new SharedState(new Map<string, readonly string[]>()); | |
| function loadCitiesForState(state: string) { | |
| if (statesToCities.current.has(state)) { | |
| return; | |
| } | |
| const url = new URL("/api/cities"); | |
| url.searchParams.set("state", state); | |
| let loadCount = 0; | |
| async function load() { | |
| if (loadCount >= 3) { | |
| return; | |
| } | |
| loadCount++; | |
| await fetch(url) | |
| .then((res) => res.json()) | |
| .then((cities) => { | |
| statesToCities.modify(map => map.set(state, cities)); | |
| }) | |
| .catch((error) => { | |
| window.reportError(error); | |
| return load(); | |
| }); | |
| } | |
| load(); | |
| } | |
| function StateAndCityFieldset(): JSX.Element { | |
| const [selectedState, setSelectedState] = useState<string>(); | |
| const [selectedCity, setSelectedCity] = useState<string>(); | |
| const statesToCitiesMap = useSharedState(statesToCities); | |
| const cities = statesToCitiesMap.get(selectedState); | |
| return ( | |
| <fieldset> | |
| <label for="state"> | |
| City | |
| <select | |
| id="state" | |
| value={selectedState} | |
| onChange={(event) => { | |
| const state = event.target.value; | |
| setSelectedState(state); | |
| setSelectedCity(undefined); | |
| loadCitiesForState(state); | |
| }} | |
| > | |
| {Array.from(cities, ([code, name]) => ( | |
| <option key={code} value={code}> | |
| {name} | |
| </option> | |
| ))} | |
| </select> | |
| </label> | |
| <label for="city"> | |
| City | |
| <select | |
| id="city" | |
| value={selectedCity} | |
| disabled={cities === undefined} | |
| onChange={(event) => { | |
| const city = event.target.value; | |
| setSelectedCity(city); | |
| }} | |
| > | |
| {cities === undefined && <option disabled>Loading…</option>} | |
| {Array.from(cities, city => ( | |
| <option key={city} value={city}> | |
| {city} | |
| </option> | |
| ))} | |
| </select> | |
| </label> | |
| </fieldset> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment