Cross-Feature Composition (Escape Hatch)
This pattern lets a page/route inject a component from one feature into another (e.g., PostsListItem from Posts into Search).
⚠️ Use only as a last resort. Normally, features must not import from each other (enforced by ESLint). Pages are the only place where cross-feature wiring can happen. This avoids moving half-baked components into
src/(shared) unnecessarily.
Checklist Before Using
Section titled “Checklist Before Using”- A simple prop/slot API won’t work
- Context is provided at the page/route level, not globally
- The contract is minimal and typed (no leaking feature internals)
- There’s a clear comment why this escape hatch is needed
Safer Alternatives (if possible)
Section titled “Safer Alternatives (if possible)”- Pass a render function (
itemRenderer) as a prop - Define a tiny contract type in
src/(shared) and keep implementations in features
The Escape Hatch Example
Section titled “The Escape Hatch Example”import { SearchList } from "@/features/search/components/SearchList";import { PostsListItem } from "@/features/posts/components/PostsListItem";import { PostListItemProvider } from "@/lib/PostListItemContext";
export const Route = createFileRoute("/_Layout/search")({ component: SearchPage,});
function SearchPage() { const { t } = useTranslation("search"); return ( <Card> <Card.Header> <Typography type="display-4xl">{t("title")}</Typography> </Card.Header> <Card.Content> <PostListItemProvider postsListItem={PostsListItem}> <SearchList /> </PostListItemProvider> </Card.Content> </Card> );}
// src/features/search/SearchList.tsxexport const SearchList = () => { const { t } = useTranslation("search"); const PostsListItem = usePostListItem(); const { data } = useSuspenseQuery(searchQueryOptions());
return ( <> {!!data.items.length ? ( <ul className="divide-y-1"> {data.items.map((post) => ( <PostsListItem key={post.id} post={post} /> ))} </ul> ) : ( <Typography className="text-muted-foreground"> {t("noResultsMessage")} </Typography> )} </> );};
// src/features/posts/providers/PostListItemContext.tsxtype PostListItemProps = { post: PostType;};
type PostListItem = (props: PostListItemProps) => React.ReactNode;
type PostListItemContextValue = { postListItem: PostListItem;};
const PostListItemContext = createContext<PostListItemContextValue>( undefined as never,);
type PostListItemProviderProps = { postListItem: PostListItem; children: ReactNode;};
export const PostListItemProvider = ({ postListItem, children,}: PostListItemProviderProps) => { return ( <PostListItemContext value={{ postListItem }}> {children} </PostListItemContext> );};
export const usePostListItem = () => { const context = useContext(PostListItemContext); if (!context) { throw new Error( '"usePostListItem" must be used within "<PostListItemProvider>"', ); } return context;};