React

Preserving state across routes

State survives a route change only when the component stays mounted: lift it above the router, or keep it in the URL, a context or storage.

Key points

  • A route change unmounts the component and its useState disappears.
  • State rendered above Routes survives navigation.
  • Filters and page number in the URL survive reloads and shared links.
  • replace avoids one history entry per keystroke.

State survives a route change only when the component that owns it stays mounted: move it above the router, or keep it in the URL, a context or storage, and decide what should reset.

When React unmounts, state dies

A route change replaces the element inside Routes: the previous page component unmounts and takes useState, ref values and local caches with it. This is not a bug, it is the same rule that clears a form when you change page.

Three places, three costs

  • Above the router: state lives in the app and is never lost; it costs memory while the tab stays open.
  • In the URL: filters and page number survive reloads and shared links; it costs one history entry per write.
  • In storage: it survives closing the browser; it costs a schema version and an expiry, or it stays there forever.

Filters in the URL

import { useSearchParams } from "react-router";

const [params, setParams] = useSearchParams();

const page = Number(params.get("page") ?? "1");
const query = params.get("q") ?? "";

function setQuery(next) {
  setParams({ q: next, page: "1" }, { replace: true });
}

Page number and search text come from the URL, so a reload keeps them and a link carries them. replace avoids one history entry per keystroke, otherwise the back button becomes useless.

State that must stay outside

function App() {
  const [draft, setDraft] = useState("");   // survives route changes

  return (
    
      
      ...
    
  );
}

Everything above Routes stays mounted: the draft survives navigation. Put only state that must really last there, because every held value is memory that never comes back.

The command

npm create vite@latest router-demo -- --template react-ts
cd router-demo
npm install react-router
npm run dev

React Router ships under the MIT licence; the project starts with Vite and the route is declared in src/App.tsx. The react-router package already contains the browser components, so the old react-router-dom is not needed.

Live records in the catalogue

Three records show state outliving a single field: expandable-tabs keeps a tab open, sidebar remembers whether it is expanded, and the input category holds the fields that produce that state.

The limitation

None of the three places is free: state above the router holds memory, the URL lands in history and server logs, storage needs a version and an expiry. A wrong choice shows up late, when a user returns to a page and finds someone else's data.

Where to continue

Tabs that live in the URL are in tabs and URL; navigation from code is in navigating with React Router; the same idea with Next.js search params is in pagination and search params; the comparison of the two setups is in Next.js vs Vite.

Frequently asked questions

Why is state lost when the route changes?

Because the page component unmounts and React drops its state. Only what stays mounted keeps its values.

Should filters live in the URL?

Yes when the user must share or reload the view. The cost is one history write per update, so use replace and a debounce.

Where do I keep a form draft that must survive?

Above the router if it only matters during the session, in sessionStorage if it must survive a reload, on the server if it must outlive the session.

Is keeping everything in a context a good idea?

No. The context holds everything while the app stays open and every update re-renders its readers: keep only genuinely shared state there.

Is state in the URL safe?

It is public: it lands in history, logs and shared links. Never put tokens, email addresses or personal data there.