skip to content

When should an app navigate with a rendered link, and when should it navigate by calling the router in code?

level: juniorimportance: must knowfreq 58%

answer

  1. who chooses the destination
  2. the address lives in the markup
  3. new tab, copy, keyboard come free
  4. code navigates after an event
  5. one router entry point either way

basics

~20 s

Render a link whenever the destination is known before the click: keyboard activation, open-in-new-tab, copy-address and a working fallback come free. Navigate in code only when code, not the user, picks the destination after an event.

solid answer

~50 s

A link is an element that carries a real address in the markup, and a client-side router only *upgrades* it: on a plain primary click the router cancels the browser's document load and renders the matching route instead. Because the address is really there, the browser still gives you focus and keyboard activation, open-in-new-tab, copy-and-share, a hover preview, and a full document navigation as the fallback if the app's code has not loaded. So the rule is about who chooses: if the user is aiming at a known destination, it is a link. If code decides the destination after something happens — a submission succeeded, a session ended, a wizard step finished, a timer fired — there is nothing for the user to aim at, and the router's navigate call is right. Both paths should then funnel through the same router entry point.

go deeper

for a junior

Remember the split: the user aiming at a known destination means a link with a real address; code deciding after an event means a router call. Do not turn a box into a navigation control.

for a middle

Be able to explain that the router upgrades a click rather than implements it, and list what the browser supplies because the address is in the markup: focus, keyboard, new-tab, copy, and the document-navigation fallback.

for a senior

Watch for the mixed cases in review: a handler that navigates without cancelling the default, a link whose handler goes somewhere else, navigation written straight to the address instead of through the router.

for a principal

The tradeoff you own is consistency: one router entry point so push-versus-replace and restoration are decided in one place, and a linting or component convention that stops hand-rolled clickable navigation from spreading.

## Two ways the address can change In a client-rendered app the address changes for exactly one of two reasons. Either the user activated something that already carried a destination, or code decided the destination after an event. Both end in the same place: the router records a history entry and renders the route that matches the new address. What differs is **who chose the destination** and **how much the browser does for you on the way**. ## Why a real link is the default A link is markup with an actual address on it. The browser recognises it as navigable, and that recognition is where the free behaviour comes from. | Capability | Link with a real address | Element with a click handler | |---|---|---| | Focusable and activated from the keyboard | built in | must be rebuilt by hand | | Exposed to assistive technology as a link | yes | only if fully re-described | | Middle-click or modified click opens a tab or window | yes | no | | Copy address, drag, share, status-bar preview | yes | no | | Works before the app's code runs, or if it fails | falls back to a document navigation | dead control | | Visible to crawlers, previewers and other tooling | yes | invisible | The router does not replace any of that. It listens for the plain primary click, cancels the default document load, pushes the entry and renders in place. When the script has not arrived yet, nothing cancels the click and it becomes an ordinary document navigation the server answers — slower, but not broken. That graceful degradation is the strongest single argument for links, and it is the reason a router is described as *intercepting* clicks rather than *implementing* them. ## Where navigating in code is the right call 1. **After a mutation succeeds** — a record is created and the user should land on it. The destination did not exist when the button was rendered. 2. **After an authentication or authorization outcome** — a sign-in completes, or a session ends and the app sends the user to a public screen. 3. **When the app corrects the current address** — a missing default is filled in, a value is normalised, a stale query is dropped. These should *replace* the current entry rather than add one. 4. **On a trigger that is not a click at all** — a countdown expires, another tab broadcasts a change, a long-running task finishes. 5. **When the destination is only computable at the end of an interaction** — a multi-step filter or a search that resolves to one result. In all five there is no element the user aimed at that could have carried an address, so there is nothing to put an address on. ## Anti-patterns that show up in review - A generic clickable box with a click handler used as navigation: not focusable, not announced, no new-tab, and invisible to anything that reads the markup. - A link with a placeholder address plus a handler that navigates elsewhere: copy-address yields junk, and middle-click opens the wrong thing. - A handler on a link that navigates in code **without** cancelling the default: two navigations, two history entries, one confused back button. - A link nested inside another clickable region, so one gesture starts two navigations. - Reproducing keyboard support by adding a key handler to a non-interactive element, and stopping at the one key the author remembered. ## One entry point, one policy However the navigation starts, it should go through the same router call, because that is where the decisions that make the back button sane live: whether this navigation is a new place or a correction of the current one, whether anything must be consulted before the screen changes, and what scroll position, focus target and title the new screen gets. Apps that sometimes write the address directly and sometimes go through the router end up with two competing policies and a history stack nobody can explain. ## A quick test Ask three questions of any navigation control. Could the user reasonably want this in a new tab? Would it still work if the app's code failed to load? Could someone copy it and send it to a colleague? Three yeses mean it must be a link. If the honest answer is that the destination is not known until after the click resolves, it is a call — and it should still be triggered by a real control, not a bare region of the screen.

  • Why does a link still work before the app's code has loaded?
    Because the destination is in the markup, not in a handler. With no listener attached yet, the click is an ordinary document navigation and the server answers it. The router only upgrades the click once it is running, so a slow or failed script degrades to a full page load instead of a dead control.
  • A navigation control must support opening in a new tab for some users. What does that force?
    It must be a real link with a real address; new-tab, new-window and copy-address are browser behaviours attached to the address, not to a click handler. Opening a window in code is a poor substitute: it needs a trusted gesture, it can be blocked, and it will drift from what the control says it does.
  • A control both submits a form and moves the user to the created record. Link or code?
    Code. The destination depends on the identifier the mutation returns, so it cannot be in the markup. Keep the control a button, and navigate from the success path — replacing the entry rather than pushing if the form's address should not be returned to.

A link is a signposted door: anyone can read where it goes, prop it open, or photograph the sign. Navigating in code is a staff member walking you through an unmarked one.

saying these in an interview costs you the question

  • Any clickable element works as a route link
  • A placeholder address plus a click handler is fine
  • Navigating in code is always cleaner than a link
  • Links cause full reloads, so apps should avoid them
  • Adding one key handler to a box makes it keyboard accessible
  • Copy-address and new-tab are edge cases not worth supporting