[Deep Dive #46] Component Router Switchyard - discussion
Deep Dive #46 tries to reconcile the two routing vocabularies by treating the component router as a switchyard: you can accept multiple systems as long as the boundaries are explicit and the 'switches' are visible.
It ties routing and navigation back to components and composition, plus the app router mindset guidance (including the pragmatic 'pages vs app router: use both' framing).
How do you define and enforce the boundary between file router and component router in the same codebase? Do you use mega components as a way to centralize routing contracts, or does it just hide navigation logic inside one component? The post implies 'events are route transitions' as a general mindset. Do you agree, or do you keep event/state surfaces separate?
Comments (10)
Back to latestI like the idea of a switchyard because it legitimizes reality: teams will mix systems.
The job is making the switches obvious and reversible.
Related: Routing and Navigation and The App Router Mindset.
Mega components are a good switchyard tool because they give you one place to hang routing contracts.
If you already have a lot of deeply nested props objects, a mega component surface can actually reduce mental load.
The 'events are route transitions' line is provocative but useful. It forces you to think: does this interaction change the user's navigation state?
If yes, route it. If not, keep it local. That split is a pretty good rule of thumb.
Related: Core Concepts Best Practices.
I still want some hard boundaries. If a screen uses component router, I do not want file router hooks sprinkled in random children.
Put the switch in the shell, not in the leaves.
I like that it ties back to components and composition. Routing isn't just URL state; it is how components receive and coordinate contracts.
Related: Components and Composition.
Switchyard metaphor only works if you also have signage. If the app can switch modes, users and devs need to see which yard they are in.
Mode evidence should be part of the route shell by default.
We enforced the boundary by making the shell the only place allowed to translate file-route params into component-route intent.
Everything below the shell receives a single intent object and treats it as the source of truth for navigation state.
Minimal pattern that worked for us: one "arrival" function that runs once per file-route change and writes into a route ledger.
tsx
import { useRouteState, useLiveState, useFlowEffect } from "react";
function useSwitchyardArrivals() {
const route = useRouteState();
const ledger = useLiveState("switchyard:ledger", { arrivals: [] as any[] });
useFlowEffect(() => {
ledger.write("arrivals", [...ledger.read("arrivals"), route.read("path")]);
}, [route.read("path")]);
return ledger;
}Counterpoint: mega components make it *harder* to reason about routing because everything is in one file and you lose navigational affordances.
I prefer a shell + explicit track modules, so the switchyard is a directory boundary, not just a component boundary.
I think that's the real point: centralize the contract, not necessarily the code.
You can keep track modules and still have a single switchyard contract surface in the shell.
The best "signage" we added was a URL-to-intent mapping table rendered in the shell during QA.
It looked silly, but it made it immediately clear when a file route was bypassing the switchyard and mutating local state directly.