skip to content

In a multi-framework component library, why define a combobox's interaction behaviour once as a framework-agnostic state machine, and what must each framework adapter still own?

level: middleimportance: nice to knowfreq 30%

answer

  1. behaviour is the expensive part
  2. states, events, transitions, context
  3. render-free and side-effect free
  4. the adapter binds input and renders
  5. async results arrive as events

basics

~20 s

Interaction rules are the costliest part to get right, so a render-free state machine lets every framework share one tested implementation. Each adapter still renders markup, binds platform input to machine events, moves real focus and runs side effects.

solid answer

~50 s

A combobox's value lies in its behaviour — when the popup opens, which option is highlighted, what Enter and Escape do, what happens on blur — and that is the part most likely to be implemented subtly differently per framework. Writing it once as a **state machine** (named states, events, a transition function and a small context of data) makes it framework-free and testable without any UI: feed events, assert states. Each **framework adapter** then owns what the machine cannot: rendering markup in the framework's idiom, translating keyboard, pointer and focus events into machine events, applying the accessibility attributes the machine's state implies, moving real focus, running side effects such as fetching search results, and re-rendering when the machine changes. The cost is a learning curve and the discipline to keep the machine free of rendering and side effects.

code

pseudocode · 33 lines
pseudocode
machine PlayerSearchCombobox
  context: query = "", options = [], highlighted = none, value = none

  state Closed
    on TEXT_TYPED(text):
      query = text
      emit REQUEST_OPTIONS(text)
      go Open
    on ARROW_DOWN when options is not empty:
      highlighted = first(options)
      go Open

  state Open
    on TEXT_TYPED(text):
      query = text
      highlighted = none
      emit REQUEST_OPTIONS(text)
    on OPTIONS_LOADED(list):
      options = list
    on ARROW_DOWN when options is not empty:
      highlighted = next(options, highlighted)
    on ARROW_UP when options is not empty:
      highlighted = previous(options, highlighted)
    on ENTER when highlighted is not none:
      value = highlighted
      query = label(highlighted)
      emit VALUE_CHANGED(value)
      go Closed
    on ESCAPE:
      highlighted = none
      go Closed
    on FOCUS_LEFT:
      go Closed

go deeper

for a junior

Recall the idea: interaction rules live once in a framework-free machine of states and events, and each framework renders what the machine says.

for a middle

Explain states, events, transitions, context and effects, and draw the line between what the machine decides and what an adapter must do with live elements and services.

for a senior

Show the failure modes: a render or network call leaking into the machine, adapters wiring events differently, and how a machine suite plus adapter conformance tests catch them.

for a principal

Judge where a behaviour core pays off — widgets with rich interaction across several frameworks — and where it is overhead for simple display components.

## Why behaviour is the expensive part In a companion app for a multiplayer game, the clan portal, the match-stats dashboard and the event site each need a **player-search combobox**: type part of a gamertag, see matching players in a popup, pick one with the mouse or keyboard. Drawing the input and the list is easy. The **behaviour** is where the bugs live: when the popup opens, which option is highlighted after new results arrive, what the arrow keys, Enter and Escape do, what happens when focus leaves. If three framework teams each implement that, they will disagree in small ways, and every accessibility fix has to land three times. A **framework-agnostic state machine** moves that behaviour into one place that no framework owns. ## What a behaviour state machine is - **States**: the named situations the widget can be in, such as `Closed` and `Open`. - **Events**: things that happen to it — typed text, an arrow key, Enter, Escape, loss of focus, search results arriving. - **Transitions**: for each state and event, the next state. An event with no transition in the current state is ignored, which removes a whole class of impossible-state bugs. - **Context**: the data carried alongside the state — the query, the option list, the highlighted option, the chosen value. - **Effects**: requests the machine emits ("fetch options for this query", "value changed") for someone else to carry out. The machine is a pure function of state and event. It never touches markup, never sets focus and never makes a network call, which is exactly what lets every framework reuse it. ## The player-search combobox as a machine | State | Event | Next state | Effect or context change | |---|---|---|---| | Closed | Text typed | Open | Store query, request options | | Closed | Down Arrow, options known | Open | Highlight first option | | Open | Options loaded | Open | Store options | | Open | Down or Up Arrow | Open | Move highlight | | Open | Enter with a highlight | Closed | Set value, emit a change | | Open | Escape | Closed | Clear highlight, keep value | | Open | Focus leaves | Closed | — | The keys follow the WAI-ARIA Authoring Practices combobox pattern (Down Arrow moves into the popup, Escape dismisses it, Enter accepts); the pattern itself belongs to the library's accessibility work, and the machine is simply where it is encoded once. ## What the core owns and what each adapter owns The **core** owns the states, the transitions, the context and the rules that decide them. It can also compute a framework-neutral description of what the markup should say — which element is expanded, which option is highlighted — for adapters to apply. Each **adapter** owns: 1. **Rendering** the input and popup in its framework's idiom, from the machine's current state. 2. **Binding input**: translating keyboard, pointer and focus events into machine events. 3. **Applying accessibility attributes** that the state implies, so assistive technology sees the same thing in every framework. 4. **Moving real focus** and scrolling the highlighted option into view — operations on live elements the machine cannot perform. 5. **Running effects**: calling the search service when the machine asks, then sending the results back as an event. 6. **Subscribing and re-rendering** when the machine's state changes, including on the server and during hydration. ## Costs and limits - **A learning curve**: contributors must think in states and events rather than scattered flags. - **Discipline**: the first rendering or network call inside a transition breaks portability. - **The adapters can still drift**, so they need a shared conformance suite; the machine only guarantees the rules, not that each adapter wires them correctly. - **Not every component needs one**: a static badge or a card has no behaviour worth a machine. The payoff is concrete. The machine is tested once with plain event sequences, a behaviour fix ships to every framework at the next release, and a fourth framework costs an adapter rather than a rewrite.

  • How do you test a shared behaviour machine without any framework?
    Drive it with event sequences and assert the resulting state, context and emitted effects: type text, expect a request effect; load options and press Down Arrow, expect the first option highlighted; press Escape, expect Closed with the value unchanged. These tests are fast and framework-free. Each adapter then needs its own smaller suite that proves real input reaches the machine and its state reaches the markup.
  • Why must the machine emit effects instead of fetching search results itself?
    A machine that makes network calls is tied to one runtime, one data layer and one timing model, and becomes hard to test. By emitting a request and accepting the results as an event, it stays pure: the adapter or the consuming app decides how to fetch, cancel stale requests or debounce, and the machine simply reacts to what arrives.

saying these in an interview costs you the question

  • A shared state machine should render the markup so frameworks need not.
  • Moving behaviour into a machine makes the widget accessible whatever the adapter renders.
  • The machine should fetch search results itself to keep adapters simple.
  • Every component in the library needs its own state machine.
  • Once the machine is shared, the adapters cannot drift apart.