Build a Dropdown as a JavaScript Class, Then Port It to React

intermediate
By FrontendAtlas Editorial · Updated Oct 23, 2026
intermediate level
Plain JavaScript DOM components with classes Event delegation and document listeners One pure reducer shared by two renderers Porting imperative DOM updates to React state Combobox and listbox ARIA with aria-activedescendant Listener cleanup on destroy and unmount
Build one basemap picker twice: first as a plain JavaScript class that creates and updates its own DOM, then as a React component. Both versions share a pure reducer, render the same combobox and listbox markup, work with the mouse and the keyboard, close on Escape and on outside clicks, and report a change only when the selection changes. The checks run the same cases against both versions, so the port is tested against the original. Concepts: dom, classes, react. React focus: moving imperative DOM updates into state, sharing a reducer through useReducer, refs for focus, and effect cleanup.
Plain JavaScript DOM components with classesEvent delegation and document listenersOne pure reducer shared by two renderersPorting imperative DOM updates to React stateCombobox and listbox ARIA with aria-activedescendantListener cleanup on destroy and unmount

Requirements

  • Work in src/App.tsx. Keep VanillaBasemapPicker, Status and App as given, and implement pickerReducer, the BasemapPicker class and ReactBasemapPicker.
  • Make pickerReducer pure and use it in both versions: no DOM, no React and no callbacks inside it, and the same state object back when an action changes nothing.
  • Render the same DOM in both versions: a .picker root, a .picker-label span with the id <idPrefix>-label, a button.picker-trigger with role="combobox", aria-haspopup="listbox", aria-controls, aria-labelledby and aria-expanded, and a ul.picker-list with role="listbox" and the id <idPrefix>-list that stays mounted and is hidden while closed.
  • Render each option as li.picker-option with role="option", the id <idPrefix>-opt-<index> and aria-selected. The trigger text is the selected label.
  • Clicking the trigger opens or closes the list. Clicking an option selects it, closes the list and moves focus back to the trigger.
  • Handle keys on the trigger: ArrowDown and ArrowUp open a closed list on the selected option; in an open list they move the active option and stop at the ends, Home and End jump to the first and last option, Enter selects the active option and Escape closes without selecting.
  • Mark the active option with the class is-active and point the trigger at it with aria-activedescendant while the list is open; remove the attribute when the list closes.
  • Close the list on a click outside the picker by listening on document. Remove that listener in destroy() and in the React effect cleanup.
  • Call onChange(item) only when the selection changes. Choosing the selected option again reports nothing.
  • Render labels as text. The last option is a user-named preset whose name contains angle brackets.

Expected behavior

  • On first render both triggers show Satellite, both lists are hidden and both status lines report 0 changes.
  • Choosing Streets with the mouse updates that version's trigger and status line to 1 change and puts focus back on its trigger; choosing Streets again keeps the count at 1.
  • ArrowDown opens the list on Satellite, End jumps to the preset, Home returns to Satellite, and two more ArrowDown presses followed by Enter select Streets with focus still on the trigger.
  • Escape, or a click on the panel heading, closes the list and keeps Satellite selected.
  • The preset appears as My preset <draft> in the list, in the trigger and in the status line.
  • A choice in one version never changes the other.
  • After the preview unmounts, no listener remains on document.

Implementation notes

  • Create the class's elements once in the constructor and update their text and attributes in render(). Rebuilding them on every render loses focus, and the document listener then receives a click target that is no longer in the DOM.
  • Handle option clicks with one listener on the list: find the option with closest('[role="option"]') and read its index from a data-index attribute.
  • Store the handlers as arrow-function fields, so removeEventListener receives the same function that addEventListener did.
  • In React, take the state from useReducer(pickerReducer, …). In event handlers, run the same reducer on the current state to decide whether to call onChange, instead of watching the selection in an effect that also fires on mount.
  • Call preventDefault() only for the keys the picker handles, so the page does not scroll. Enter and Space on a closed trigger already click the button.
  • The preview passes useState, useEffect, useMemo, useRef, useReducer, useContext, createContext and useCallback in as globals; reach any other hook through React.

Solution idea

Put every state change in one pure pickerReducer over { open, activeIndex, selectedIndex, count }. Both versions send it the same actions, toggle, close, choose and key, and it returns the same object when nothing changes, so each version can skip rendering and side effects.

Public interview reports

These public, self-reported interview accounts informed this practice version. The exercise is modelled on reported rounds, not copied from them, and the reports are not verified official questions or endorsements.

  • Taro: Anduril software engineer interview, 2025 · Taro, January 2025

    A HackerRank coding interview asked for an ES6 class written from a blank document, then the same component set up in Vue, Angular or React; the author kept the exact question private. This item practises that format with a dropdown; it is not the reported task.

  • Taro: Anduril software engineer interview, December 2024 · Taro, December 2024

    An account with a frontend focus lists writing a timer class and then a React app around it. This item practises the same order, a class first and the framework second, with a different component.

Reported in a public Anduril interview account. Anduril frontend interview questions