Home/Learn/React/Navigation Patterns — Scroll, Modals and 404s

Navigation Patterns — Scroll, Modals and 404s

Advanced
Routing

The details that separate a single-page app from a website that feels right: where the page scrolls to, what the back button does to a modal, and what an unknown URL renders.

Overview

A browser gives you a set of behaviours for free — scrolling to the top on navigation, restoring position on back, a 404 for a URL that does not exist — and a single-page app breaks all of them by default. Nothing here is difficult, but each one is invisible until a user complains that the back button "did nothing" or that a long list dumped them at the bottom of a new page. These are also exactly the details an interviewer probes to find out whether someone has shipped a real application.

Scroll Behaviour

Scroll to top on a new page, restore position on back, and respect anchors.

Top on push, restore on pop, anchor on hash
// Without this, navigating from halfway down a list leaves the new
// page scrolled halfway down.
function ScrollToTop() {
  const { pathname, hash } = useLocation()
  const navigationType = useNavigationType()

  useEffect(() => {
    if (navigationType === 'POP') return          // back/forward: let the
                                                   // browser restore position
    if (hash) {
      document.querySelector(hash)?.scrollIntoView()
      return
    }
    window.scrollTo(0, 0)
  }, [pathname, hash, navigationType])

  return null
}

// React Router's <ScrollRestoration /> does this, including saving
// and restoring positions per history entry.

// Focus matters as much as scroll: after navigation, move focus to
// the main heading, or a screen-reader user stays where they were.
useEffect(() => { headingRef.current?.focus() }, [pathname])

Modals and the Back Button

Two kinds of overlay, and picking the wrong one is a common complaint.

Ephemeral in state, shareable in the URL
// Ephemeral overlay -> local state. Back should LEAVE the page.
const [confirmOpen, setConfirmOpen] = useState(false)

// Content the user might share or return to -> a route or a search
// param, so back closes it and the URL is copyable.
const [params, setParams] = useSearchParams()
const previewSlug = params.get('preview')

{previewSlug && (
  <Modal onClose={() => setParams(p => { p.delete('preview'); return p })}>
    <ProblemPreview slug={previewSlug} />
  </Modal>
)}

// The test: if the user pressed back expecting the modal to close,
// which is what phones have taught everyone, then it belongs in
// the URL.

// Route-based modals keep the list rendered underneath while the
// URL points at the item — the pattern behind image lightboxes and
// detail overlays in most modern apps.

404s and Redirects

An unknown URL, a moved page, and the server config people forget.

Catch-all, redirects, and the SPA refresh 404
// A catch-all route, always
{ path: '*', element: <NotFound /> }

// Two different 404s, and users need to be able to tell them apart:
//   the ROUTE does not exist        -> NotFound page
//   the route exists, the DATA does -> "This problem was removed",
//                                      with the surrounding nav intact

// Redirect old URLs rather than 404ing them
{ path: 'old-problems', element: <Navigate to="/dsa/problems" replace /> }
// Permanent moves ideally happen at the CDN or server so search
// engines see a real 301.

// The refresh 404 — every SPA hits this once. /problems/two-sum has
// no file on disk, so the server must return index.html for unknown
// paths. Vercel and Netlify do it by default; a bare nginx does not:
//   try_files $uri /index.html;

// And set the page title per route, or every page reads the same in
// history, bookmarks and screen-reader announcements.

Key Points to Remember

  • 1Scroll to top on a push navigation but let the browser restore position on back and forward
  • 2Move focus to the main heading after navigation, or screen-reader users are left behind
  • 3An overlay users might share or expect back to close belongs in the URL; an ephemeral one belongs in state
  • 4Distinguish a missing route from missing data — they need different pages and different words
  • 5A client-side route 404s on refresh unless the server returns index.html for unknown paths

Interview Questions

Sign in to ask Aria
1

How do you handle scroll position when navigating in a single-page app?

Medium
2

When should a modal be part of the URL?

Medium
3

Why does refreshing a client-side route sometimes 404, and how is it fixed?

Medium

Ask Aria about Navigation Patterns — Scroll, Modals and 404s

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…