In a client-side router, what does it mean for a route entry to name a module loader instead of a component?
answer
- the table stores how to get it
- a function, not a component
- called on the first match
- resolved module is memoised
- matching never waits on code
basics
~20 sThe table stores a function that fetches that route's code on demand rather than a component the start-up bundle already contains. The router calls it on the first navigation that matches, waits, then renders what it resolves to.
solid answer
~40 sAn ordinary route entry points at a component that shipped in the start-up bundle, so the bundle grows with every route in the table. A lazy entry instead holds a **loader**: a function the router calls the first time that route matches, which fetches the route's own bundle and resolves to the component. The router memoises the resolved module, so a second visit renders with no request. While the loader is in flight the navigation is *pending* — nothing of the target screen can render, because the code describing it is not in memory. Note what does not change: the pattern, params and nesting still live in the table, so matching, param parsing and link generation all work before any route code arrives. Only rendering waits.
go deeper
Recall that the entry holds a function which fetches the screen's code the first time that URL is visited, and that the user waits during that fetch.
Explain the sequence out loud: match, call the loader, pending, resolve, memoise, render — and point out that matching and params work with no route code present.
Show that you plan for both new code paths a loader introduces: an acknowledged pending state, and a failure that needs a route-level recovery rather than a re-render.
Frame it as a cost you are moving, not removing: start-up bytes traded for a wait in the middle of a task, and each lazy boundary earning its pending and failure paths.
## What a route table entry holds A client-side router keeps a **route table**: entries pairing a URL pattern with something able to render a screen. In the ordinary case the entry holds a direct **component reference**, and that reference exists only because the start-up bundle already contains the component's code. Matching a URL and rendering are then the same instant, because the code is already in memory. The price is paid before the first pixel: every route in the table ships to every user, including the routes they never open. A lazy entry replaces the reference with a **module loader** — a function of no arguments that, when called, starts a request for a separate bundle and resolves to the module holding the route's component. The entry now describes *how to obtain* the screen rather than the screen itself. Nothing else about the entry changes: the pattern, its params, its children and its nesting are still plain data sitting next to the loader. ## The first navigation that matches 1. The router matches the incoming URL against the table and works out the branch of entries that will render. 2. For each matched entry it asks whether that entry's module is already resolved. If not, it calls the loader, which issues the request. 3. The navigation is now **pending**. None of the target screen can render, because the description of it is not in memory yet. 4. When the loader resolves, the router stores the module on the entry and commits the render. 5. A later navigation to the same route finds the stored module and renders with no request and no wait. Step 5 is the part candidates most often miss: the wait is **once per route per session**, not once per visit. Step 4 has a mirror-image rule — a *rejected* loader is normally not stored, because a retry has to be able to issue a fresh request instead of replaying a cached failure. ## Eager reference versus module loader | | Component reference | Module loader | |---|---|---| | When the code arrives | in the start-up bundle | on the first matching navigation | | Start-up cost | grows with every route in the table | only what the first screen needs | | Render after a match | immediate | after the loader resolves | | New failure mode | none at navigation time | a failed request blocks the screen | | Can be started early | nothing to start | yes, the loader is callable on its own | ## What still works without the route's code Because the pattern lives in the table beside the loader, everything that is *about the URL* works before any route code has been fetched: - deciding whether a URL matches at all, and which branch it matches; - reading and validating the route's params out of the URL; - generating a link to the route from its name and params; - knowing which modules the next screen will need, which is the whole basis of preloading. Only rendering waits. That separation is what makes a lazy route different in kind from code discovered while rendering: the router can act on a URL ahead of time precisely because the loaders are declared as data. ## What the router must provide around a loader - A **pending state** the app can render from, so a click is acknowledged rather than appearing to do nothing. - Memoisation of the resolved module, and the deliberate choice not to memoise a rejection. - A failure path for that rejection. It is not a bug inside the component, since the component never ran, so it belongs to a route-level boundary rather than to the component's own error handling. - **Deduplication**, so that a preload and a navigation wanting the same entry share one in-flight request instead of issuing two. - A way to call the loader outside a navigation, which is exactly what a preload hook is. ## Where the tradeoff actually sits Lazy entries pay off where the table describes screens users reach rarely or one at a time: an admin area, a settings tree, a seldom-opened report. They pay much less on the app's own first screen or on the next step of a funnel nearly everyone takes. There the loader converts a start-up cost the user had already absorbed into a wait dropped into the middle of their task, which is more visible, not less. Each lazy entry also adds two code paths a reader and a test suite must handle: the pending state and the failed load. Declaring a loader is cheap and never free — the entry has moved a cost, and choosing where the user waits is the actual decision.
- Does the router need the route's code to decide whether a URL matches?No. The pattern, its params and its children are declared in the table beside the loader, so matching, param validation and link generation all work with no route code loaded. Only rendering waits for the module, which is also why the router knows in advance which modules a URL will need.
- What happens on the second navigation to the same lazily loaded route?The router finds the module already stored on that entry and renders synchronously: no request, no pending state. The wait is once per route per session, not once per visit, which is why the pending path is easy to leave untested after the first click in development.
- Why is a failed loader usually not cached the way a successful one is?Because a retry has to be able to issue a fresh request. If the router kept the rejection, retrying would replay the stored failure instead of asking the network again, and the route would stay broken for the life of the document even after the cause had cleared.
A recipe index that lists page numbers instead of reprinting every recipe: you can look up which page you need instantly, but you still have to fetch that page before you can cook.
saying these in an interview costs you the question
- Thinks a lazy route reduces the total code a user who visits every route downloads
- Says the router must load a route's code before it can tell whether the URL matches
- Expects the loading wait on every visit rather than only the first
- Believes the loader can be rendered directly, like a component reference
- Assumes a failed load leaves the screen in a normal, re-renderable state