In Playwright component tests, how do you mount a data grid that needs children rendered inside it?
answer
- Elements are not serialisable data
- Composition belongs in browser-side code
- One export per tree shape
- Plain props for what varies inside
- Name exports after the scenario
basics
~20 sChildren cannot travel in the props object because a framework element is not serialisable data. Write the composed tree as its own story export, mount that export, and keep only plain values such as labels and row arrays in props.
solid answer
~40 sIn Playwright 1.63 the props argument to `mount` carries serialisable data only, and a child element is not data - it is an object full of component references. So a grid that needs children is expressed as a story export that composes them: `GridWithEmptyState` renders `<DataGrid rows={[]}>` wrapping the real `EmptyState`, and the test mounts that story instead of trying to pass the subtree in. Anything genuinely variable stays in props as plain values, so one export can still cover many cases: `title`, `rows`, or a discriminator such as `density: 'compact' | 'comfortable'`. The registry of exports then reads as a catalogue of compositions, one per shape you actually need to test. Add a new export when the tree differs; add a prop when only the data differs.
code
typescript · 15 lines// data-grid.story.tsx
import { DataGrid } from '../src/data-grid';
import { EmptyState } from '../src/empty-state';
export function GridWithRows(props: { rows: Array<{ id: string; label: string }> }) {
return <DataGrid rows={props.rows} />;
}
export function GridWithEmptyState(props: { title: string }) {
return (
<DataGrid rows={[]}>
<EmptyState title={props.title} action={<button type="button">Add row</button>} />
</DataGrid>
);
}go deeper
Know the rule of thumb: if a case needs different markup inside the component, it needs its own story export rather than a new prop.
Explain the mechanism, that elements cannot be serialised into the page, and show where the line between a prop and an export falls.
Describe how you keep a package's story registry small: compositions consumers really build, data differences pushed into props, story files reviewed like production code.
Treat the export list as the tested surface of the package and decide who owns it, how it is named, and what evidence a new export has to justify.
## Why children cannot ride in the props object `mount` serialises the props object to send it into the browser page. A child element is not plain data: it is an object holding references to component functions and closures, and neither of those has a serialisable form. So the request "render this grid with my custom empty state inside it" cannot be expressed as a prop, no matter how the component itself declares children or slots. That is not a limitation to work around; it is what tells you where the composed tree belongs. Composition is browser-side code, and the story file is the browser-side code you already have. ## The story export is the composition A story export is a small component that imports the real component and renders it the way a consumer would. That makes it the natural home for the child tree, and it stays honest: the story looks like the code an application would write. In Playwright 1.63 this is the whole mechanism - the registry of exports is the set of compositions your suite can mount. - one export per shape of tree, named for the scenario (`GridWithEmptyState`, `GridWithRowActions`); - plain props for the parts that vary within that shape (`title`, `rows`, `density`); - callbacks wired inside the export, with their payloads recorded into a hidden input the test reads with `toHaveValue`; - the same file holds the recorder markup, so the shipped component stays free of test scaffolding. ## When to add a prop, and when to add an export | The scenario differs by | Add | Why | |---|---|---| | A label, a row count, a locale | A prop | Plain data serialises, so one export covers it | | Which handler the callback uses | A prop plus a mapping in the story | Send a flag; the story picks the function | | The child tree rendered inside | A story export | Elements cannot be serialised at all | | Which component wraps the subject | A story export | Wrapping is composition, not data | The dividing line is exactly the serialisation boundary from the props rule: if the difference can be written as data, it is a prop, and if it can only be written as code, it is an export. ## Keeping the registry from exploding A story per permutation is how a design-system package ends up with two hundred exports nobody reads. Bound it deliberately: 1. Enumerate the compositions that consumers actually build, not the ones the type system permits. 2. Give each composition one export, and push every remaining difference into plain props. 3. Where two exports differ only by a wrapper, keep the wrapper and delete the duplicate. 4. Review story files like production code, because they are the fixtures for a whole package. ## Naming matters more than usual The export name is the only thing a reader sees when a test fails, and it is what the test file cites when it mounts. Name after the scenario rather than the props (`GridWithEmptyState`, not `GridCase3`), keep one story file per component, and keep recorder test ids named after the callback they record. A grid story whose export list reads as a sentence about the component is a better inventory of behaviour than a table of props ever is. ## What this looks like in review - A test that tries to pass a subtree as a prop is a missing story export, not a Playwright bug. - An export that takes ten props is usually two compositions wearing a trench coat. - Two exports that differ only in a string should be one export plus a prop. - Recorder inputs living anywhere but the story file are scaffolding in the wrong repo layer. Treated this way, children stop being an awkward special case. Data flows in through props, composition is expressed as code in the story, and the test picks the story whose shape it means to exercise.
- Does the same reasoning apply to a render prop that returns markup?Yes. A render prop is a function, so it fails the serialisation rule for the same reason children do. Define the renderers inside the story and let the test pick one with a plain string prop, which keeps the choice in the test without moving code across the boundary.
- How do you avoid a story file that grows to dozens of near-identical exports?Only give a composition its own export when the rendered tree genuinely differs. Data differences belong in props, and two exports separated by one label should be collapsed into one that takes that label. Review story files with the same eye as production code.
saying these in an interview costs you the question
- Trying to pass a child subtree through the props object
- Believing children are just another serialisable prop
- Creating one story export per data permutation
- Naming exports after props instead of scenarios
- Putting composition code in the test file rather than the story