Skip to content

Wiring a feature into a page

This repo consumes feature packages. It never forks or edits one, and never edits node_modules.

Each package ships its own docs. Read them rather than guessing the API: what it covers and what it explicitly does not, the providers it assumes, the hooks and views it exports, and which seams it opens. Signatures come from the package, not from memory.

Add the dependency — through the workspace catalog where the repo has one — and add the package to transpilePackages in next.config.ts, since these packages are consumed as TypeScript source. Import its styles.css in global.css, or Tailwind never scans it and generates none of its classes, silently.

Render the peers the feature assumes above it — react-query, the GraphQL fetcher, next-intl — plus any session or business-unit provider seam it declares. Wire real values; a package’s defaults exist for tests, not for production.

The consumer owns the query. Fetch with the feature’s query options or hook, then render its pure view, supplying renderItem and any named slots. Where the fetch goes is a rendering decision, not a feature one — see evolve-site-rendering.

const { data: list } = useQuery(
shoppingListDetailQueryOptions({ gqlClient, shoppingListId }),
);
<ShoppingListView
list={list}
renderItem={(item, api) => (
<LineItem
item={item}
onRemove={api.remove}
onQuantityChange={api.setQuantity}
busy={api.isRemoving || api.isUpdatingQuantity}
/>
)}
slots={{ listActions: <AddAllToCartButton items={list.items} /> }}
/>;

Write through the api the view hands you, not through a second hook of your own. The list owns the optimistic update and the undo window; a parallel mutation opens a second one that does not reconcile with it.

Need more data? Compose the feature’s base fragment plus your own fields in your own query. The extended type flows into renderItem and the slots, so typing stays intact. Extending the query does not eject the view.

Need different behaviour? Look for typed options on the feature’s own hook before anything else. A feature resolves its modes internally and exposes them as options, so changing what it does should never mean owning its markup.

The change Where it belongs
Colour, spacing, size, what a role looks like evolve-site-theming
Wording, including aria-labels evolve-site-messages
What the feature does typed options on its hook
Content, arrangement, structure renderItem, children, slots
More or different data your own query
A form on the page evolve-site-form

Composition covers far more than it looks like it will. Reach past it only for a genuine structural change — a different layout, reordered sections, regions the slots do not provide. Ejecting a view to restyle it, or to change a label, is always the wrong tool.

Where a project still carries a local, shadcn-derived ui package, do not extend it. Its CVA variants, size props and data-slot hooks are a different and incompatible styling model.

A page is fetch, render and slots. Business logic stays in the feature; markup for one row lives in a component of yours, not inline in the page. Verify with pnpm check that the extended query types resolve, and confirm the behaviour against what the package’s docs say it covers.