Wiring a feature into a page
This repo consumes feature packages. It never forks or edits one, and never edits
node_modules.
Read the package first
Section titled “Read the package first”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.
Install and set up
Section titled “Install and set up”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.
Own the query, render the view
Section titled “Own the query, render the view”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.
Where to stop
Section titled “Where to stop”| 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.
Pages stay thin
Section titled “Pages stay thin”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.

