skip to content

As owner of a design system's Playwright story registry, when do you add a story export instead of a prop?

level: principalimportance: nice to knowfreq 22%

answer

  1. Registry is an API surface
  2. Data is a prop, code is an export
  3. Compositions consumers really build
  4. One recorder convention per package
  5. Drift shows as unreadable export lists

basics

~20 s

Add an export when the rendered tree or the wiring differs, and a prop when only data differs. Exports are code you maintain forever, so each one should stand for a composition consumers actually build, not a data permutation.

solid answer

~50 s

Treat the registry as an API surface with a maintenance cost. Because the props argument carries only serialisable data, anything expressible as data belongs in props, and everything expressible only as code - a different child tree, a different wrapper, a different callback wiring - forces an export. That rule keeps the count honest, but it is not the whole decision. Each export is a fixture other engineers copy, so it should mirror a composition consumers really build; a story nobody would write in an application tests a shape the package does not ship. Standardise the scaffolding too: one recorder helper, one naming scheme for `data-testid` values, story files owned by the same team that owns the component. The failure mode to design against is drift - dozens of near-identical exports that nobody dares delete because no one can tell which behaviour each one still proves.

code

typescript · 11 lines
typescript
// story-utils.tsx - one recorder convention for every story in the package
import { useState } from 'react';

export function useRecorder(testId: string) {
  const [log, setLog] = useState<string[]>([]);
  const record = (entry: unknown) => setLog((prev) => [...prev, JSON.stringify(entry)]);
  const recorder = (
    <input type="hidden" data-testid={testId} readOnly value={JSON.stringify(log)} />
  );
  return { record, recorder };
}

go deeper

for a junior

Take the simple version away: a new prop for different data, a new story export for different markup inside the component.

for a middle

Be able to justify the split by the serialisation boundary, and show how a string prop plus a lookup avoids an unnecessary export.

for a senior

Talk about keeping a registry small in practice: naming, collapsing near-duplicates, and one shared recorder helper across the package.

for a principal

Own the policy and its cost. Decide what earns an export, who maintains story files, and how drift between stories and real consumer compositions gets caught.

## The registry is an API surface, not a scratch pad Every story export is code somebody maintains, reviews and eventually has to decide about. It is also the fixture other engineers copy when they add a test, so its shape propagates. That makes the registry closer to a public surface of the design-system package than to test scratch: cheap to add, expensive to own, and awkward to delete once several specs mount it. The technical constraint gives you the first half of the policy for free. `mount` serialises its props, so data-shaped variation must be a prop and code-shaped variation must be an export. The judgement lives in the second half: which code-shaped variations deserve to exist at all. ## Where the line falls | The scenario differs by | Prop or export | Reasoning | |---|---|---| | A label, row set, locale, density flag | Prop | Serialisable, so one export covers the whole family | | Which handler a callback uses | Prop plus a lookup in the story | Send a string, let the story choose the function | | The child tree rendered inside | Export | Elements cannot be serialised | | The wrapper or provider around it | Export | Composition is code, not data | | A permutation no consumer builds | Neither | It tests a shape the package does not ship | That last row is the one teams skip. A registry grows fastest not from real compositions but from combinations someone enumerated because the type system allowed them. ## Bounding the growth 1. Start from consumer code, not from the props type: list the compositions applications actually assemble. 2. Give each of those one export, named for the scenario rather than the props it takes. 3. Push every remaining difference into plain props, including which handler the story wires. 4. Collapse exports that differ by one string, and delete exports whose scenario no consumer builds any more. 5. Re-read the export list quarterly as an inventory of tested behaviour; if it no longer reads as one, it has drifted. ## Standardise the scaffolding Recorder inputs are the part that quietly goes feral. Six authors invent six conventions, and a copy-pasted assertion starts reading the wrong recorder. - one helper that returns a `record` function plus the recorder element, used by every story; - `data-testid` values named after the callback they record, never after the test that reads them; - recorders confined to story files, never added to the component the package publishes; - one story file per component, so the export list is a readable inventory; - serialised payloads so a growing payload does not silently escape an assertion. ## Ownership and drift The team that owns a component should own its stories, for the same reason they own its types: when the component changes, the fixtures have to change with it, and a fixture maintained by strangers becomes a reason not to refactor. The drift to watch for is a story that no longer resembles any real usage - it keeps passing, so nobody questions it, while the composition consumers actually ship goes untested. Deleting such an export is the cheapest quality action available and is almost never taken, because a passing test feels like an asset. ## What I would write down - Props are data; anything with behaviour or identity lives in the story. - An export exists to cover a composition, not a data permutation. - Exports are named for scenarios and reviewed like production code. - Every callback has exactly one recorder, named after the callback. - Story files ship with their component and are owned by the same team. Four short rules keep the registry the size of the package's real surface, which is the only size at which it stays worth reading.

  • How do you decide an existing story export should be deleted?
    Ask which composition in consumer code it stands for. If no application assembles that shape any more, the export tests something the package does not ship, and its green result is noise. Delete it with the spec that mounts it rather than keeping both alive.
  • Should story files live beside the component or beside the tests?
    Beside the component, owned by the team that owns it. Stories change whenever the component's composition changes, and fixtures maintained at a distance become a reason to avoid refactoring. The specs can import them from there without owning them.
  • What is the earliest warning sign that a registry has drifted?
    The export list stops reading as a description of the component. When names turn into variants and cases, or two exports differ by a single string, the registry has become a permutation table rather than an inventory of behaviour.

saying these in an interview costs you the question

  • Adding a story export for every data permutation
  • Letting each author invent a recorder convention
  • Naming exports after cases instead of scenarios
  • Keeping stories no consumer composition matches
  • Story files owned by a team that does not own the component