React Router v8 vs Vue Router: 14-Step Setup Guide [2026]

React Router v8 shipped on June 17, 2026, and Vue Router 4.4+ now underpins Nuxt 4.5’s file-based routing. Both ecosystems moved fast this summer, and the two routers no longer look or behave the way they did even a year ago. If you’re starting a new single-page app in August 2026, or migrating an old one off React Router v6 or Vue Router 3, the setup steps, navigation guard syntax, and data-loading patterns have all shifted enough that old tutorials will actively mislead you.

This tutorial walks through building the same small task-tracking app twice — once with React 19.2 and React Router v8, once with Vue 3 and Vue Router 4 — so you can see the routing decisions side by side. You’ll install both routers, configure nested routes, add navigation guards, handle dynamic route parameters, lazy-load route components, and test the whole thing with Vitest. By the end you’ll have two working projects and a clear sense of which router fits your stack.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Prerequisites and Version Requirements

Before you start, confirm your toolchain matches what React Router v8 and current Vue Router releases actually require. React Router v8 raised its minimum baseline this year, and skipping this check is the single most common reason developers hit a wall on step one.

  • Node.js: 22.22.0 or newer (React Router v8’s hard minimum; Vue Router works fine on 20.19+, but match Node 22 for consistency across both projects)
  • npm: 10.x or newer, or pnpm 9.x / yarn 4.x if you prefer a different package manager
  • React: 19.2.7 or newer
  • react-router: v8.3.0 (the current stable line as of this tutorial; v8.0.0 shipped June 17, 2026)
  • Vue: 3.5.x (Vue 3 is required; Vue Router 4 does not support Vue 2)
  • vue-router: 4.4.x or newer
  • Vite: 7.x (React Router v8 requires Vite 7+ if you use the Vite plugin workflow)
  • Vitest: 4.1.x or the 5.0.0 release candidate, for the testing sections
  • A code editor (VS Code is assumed in screenshots, but any editor works)
  • Basic familiarity with JSX and Vue single-file components — this is not a beginner JavaScript tutorial

Check your Node version before anything else:

node --version
# Should print v22.22.0 or higher

npm --version
# Should print 10.x or higher

If Node reports an older version, update it with nvm install 22 (or the Node version manager of your choice) before continuing. React Router v8 will fail to install cleanly on Node 20 or earlier — this is the first pitfall developers hit, and it’s covered in more detail in the troubleshooting section below.

Step 1: Scaffold the React Project With Vite

Start with a clean Vite project targeting React 19.2. Vite 7 is the current major version and pairs correctly with React Router v8’s baseline requirements.

npm create vite@latest react-router-demo -- --template react
cd react-router-demo
npm install
npm install [email protected]

Confirm the install landed on v8 by checking package.json:

cat package.json | grep react-router
# "react-router": "^8.3.0"

Note that React Router v8 merged the old react-router-dom package into a single react-router package. If you copy setup instructions from a 2024 or 2025 tutorial that tells you to install react-router-dom, you’ll end up running a stale v6 dependency tree alongside your new one, which causes duplicate router instances and broken context. Install react-router only.

Step 2: Scaffold the Vue Project With Vite

Do the same for Vue. Vite’s official Vue template already wires up the dev server correctly for Vue 3.5.

npm create vite@latest vue-router-demo -- --template vue
cd vue-router-demo
npm install
npm install vue-router@4

Verify the version:

cat package.json | grep vue-router
# "vue-router": "^4.4.5"

If you’re building a larger app and want file-based routing without hand-writing route configs, install unplugin-vue-router instead of configuring routes manually — it generates typed routes from your file structure, similar in spirit to Nuxt’s pages directory, but for plain Vite + Vue projects. This tutorial uses manual configuration so the routing logic stays visible for comparison purposes.

Step 3: Define Routes in React Router v8

React Router v8 kept the data router API from v6.4+ as the default recommended pattern. Create src/router.jsx:

import { createBrowserRouter } from "react-router";
import App from "./App";
import TaskList from "./pages/TaskList";
import TaskDetail from "./pages/TaskDetail";
import NewTask from "./pages/NewTask";
import NotFound from "./pages/NotFound";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    children: [
      { index: true, element: <TaskList /> },
      { path: "tasks/new", element: <NewTask /> },
      { path: "tasks/:taskId", element: <TaskDetail /> },
      { path: "*", element: <NotFound /> },
    ],
  },
]);

Then wire it into main.jsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router";
import { router } from "./router";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>
);

Notice there’s no top-level <BrowserRouter> wrapping component anymore in this pattern — createBrowserRouter and RouterProvider replace it. Nested routes render through an <Outlet /> placed inside App.jsx, exactly like Vue Router’s <router-view>, which is the first real structural parallel between the two libraries.

Step 4: Define Routes in Vue Router 4

Vue Router configures routes as a plain array of objects rather than JSX. Create src/router/index.js:

import { createRouter, createWebHistory } from "vue-router";
import TaskList from "../pages/TaskList.vue";
import TaskDetail from "../pages/TaskDetail.vue";
import NewTask from "../pages/NewTask.vue";
import NotFound from "../pages/NotFound.vue";

const routes = [
  { path: "/", name: "tasks", component: TaskList },
  { path: "/tasks/new", name: "new-task", component: NewTask },
  { path: "/tasks/:taskId", name: "task-detail", component: TaskDetail, props: true },
  { path: "/:pathMatch(.*)*", name: "not-found", component: NotFound },
];

export const router = createRouter({
  history: createWebHistory(),
  routes,
});

Register it in main.js:

import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";

createApp(App).use(router).mount("#app");

The props: true option on the task-detail route automatically passes taskId as a component prop instead of forcing you to read route.params.taskId manually inside the component. React Router has no direct equivalent — you always pull params through the useParams() hook — which is a small but real ergonomic difference between the two APIs.

Step 5: Render Nested Routes and Navigation Links

In React’s App.jsx, use <Outlet /> to render the matched child route and <Link> for navigation:

import { Outlet, Link, NavLink } from "react-router";

export default function App() {
  return (
    <div className="app-shell">
      <nav>
        <NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}>
          Tasks
        </NavLink>
        <NavLink to="/tasks/new">New Task</NavLink>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

In Vue’s App.vue, the equivalent is <router-view> and <router-link>:

<template>
  <div class="app-shell">
    <nav>
      <router-link to="/" exact-active-class="active">Tasks</router-link>
      <router-link to="/tasks/new">New Task</router-link>
    </nav>
    <main>
      <router-view />
    </main>
  </div>
</template>

Both frameworks give you an “active link” styling hook out of the box. React’s NavLink passes an isActive boolean into a render function; Vue Router applies an active-class (or exact-active-class) directly to the rendered anchor tag as a CSS class, no render function required.

Step 6: Read Dynamic Route Parameters

React Router exposes params through the useParams hook inside any component rendered by a matching route:

import { useParams } from "react-router";

export default function TaskDetail() {
  const { taskId } = useParams();
  return <h2>Task #{taskId}</h2>;
}

Vue Router gives you the same data through the useRoute() composable, or, since we set props: true earlier, directly as a component prop:

<script setup>
defineProps(["taskId"]);
</script>

<template>
  <h2>Task #{{ taskId }}</h2>
</template>

This prop-based pattern is one of the more common “gotcha” points when engineers move between the two stacks: forgetting to set props: true on the Vue Router route definition means taskId arrives as undefined in the component, with no console error at all.

Step 7: Add Navigation Guards

This is where the two routers diverge most sharply. Vue Router has navigation guards baked directly into its API — beforeEach, beforeEnter, and beforeRouteEnter — as first-class, documented primitives:

router.beforeEach((to, from) => {
  const isAuthenticated = Boolean(localStorage.getItem("auth_token"));
  if (to.name === "new-task" && !isAuthenticated) {
    return { name: "tasks" };
  }
});

React Router v8 has no built-in guard concept. Instead, you protect a route by wrapping it in a component that checks auth state and redirects with the <Navigate> component, or by using a loader function that throws a redirect response before the route ever renders:

import { redirect } from "react-router";

export async function requireAuthLoader() {
  const isAuthenticated = Boolean(localStorage.getItem("auth_token"));
  if (!isAuthenticated) {
    throw redirect("/");
  }
  return null;
}

// in router.jsx
{ path: "tasks/new", element: <NewTask />, loader: requireAuthLoader }

Neither approach is “wrong,” but they reflect a real philosophical split: Vue Router treats routing as a first-class app-level concern with dedicated lifecycle hooks, while React Router increasingly folds route protection into its data-loading layer (loaders and actions) rather than a separate guard system. If your team is used to Vue’s guard model, budget extra ramp-up time for React Router’s loader-based redirects — they solve the same problem but read very differently in code review.

Step 8: Lazy-Load Route Components

Both routers support code-splitting so route bundles only download when a user actually navigates to them. In React Router v8, use dynamic import() combined with React.lazy, or the newer lazy route property that avoids wrapping every element in Suspense manually:

{
  path: "tasks/:taskId",
  lazy: () => import("./pages/TaskDetail"),
}

Vue Router has supported this pattern natively since Vue Router 3, using dynamic imports directly as the component value:

const routes = [
  {
    path: "/tasks/:taskId",
    name: "task-detail",
    component: () => import("../pages/TaskDetail.vue"),
    props: true,
  },
];

Vue’s version requires zero extra configuration — no Suspense boundary, no separate lazy wrapper. This is one area where Vue Router’s design has stayed simpler for longer, since the underlying build tooling (Vite’s Rollup-based chunking) handles the splitting automatically for both frameworks.

Step 9: Fetch Data With Loaders (React) and onBeforeRouteUpdate (Vue)

React Router v8’s loader pattern lets a route fetch its own data before rendering, avoiding the waterfall of “render, then fetch, then re-render” that plagued older React apps:

export async function taskLoader({ params }) {
  const response = await fetch(`/api/tasks/${params.taskId}`);
  if (!response.ok) throw new Response("Not Found", { status: 404 });
  return response.json();
}

// in router.jsx
{ path: "tasks/:taskId", element: <TaskDetail />, loader: taskLoader }

Inside the component, pull the loader’s result with useLoaderData():

import { useLoaderData } from "react-router";

export default function TaskDetail() {
  const task = useLoaderData();
  return <h2>{task.title}</h2>;
}

Vue Router doesn’t ship an equivalent loader API. The idiomatic pattern is fetching inside onMounted or a composable, and re-fetching on the onBeforeRouteUpdate guard when the same component is reused across param changes (for example navigating from /tasks/1 straight to /tasks/2 without unmounting):

<script setup>
import { ref, onMounted } from "vue";
import { onBeforeRouteUpdate, useRoute } from "vue-router";

const route = useRoute();
const task = ref(null);

async function loadTask(id) {
  const response = await fetch(`/api/tasks/${id}`);
  task.value = await response.json();
}

onMounted(() => loadTask(route.params.taskId));
onBeforeRouteUpdate((to) => loadTask(to.params.taskId));
</script>

<template>
  <h2 v-if="task">{{ task.title }}</h2>
</template>

This is a genuinely important pitfall: without onBeforeRouteUpdate, navigating between two routes that reuse the same component (like our task detail page) will not re-fetch data, because Vue reuses the component instance rather than remounting it. React Router’s loaders handle this automatically since the loader re-runs on every navigation that matches the route, regardless of component reuse.

Step 10: Add a Framework-Level SSR Option (Next.js vs Nuxt)

If your app needs server-side rendering, most teams don’t hand-roll it with React Router or Vue Router directly — they reach for Next.js or Nuxt, which wrap routing into a file-based convention on top of the same underlying libraries. Next.js 16.3, which reached stable on August 3, 2026, ships an opt-in “Instant Navigations” feature with partial prefetching, cuts dev-server memory usage by up to roughly 90%, and speeds up repeat builds by as much as 5.5x according to the official Next.js blog. Nuxt 4.5.x, with 4.5.2 released August 5, 2026, builds its file-based pages directory on top of Vue Router 4 internally, and Nuxt’s changelog confirms Nuxt v3 is now end-of-life, meaning new Vue SSR projects should target the 4.x line.

To scaffold either quickly:

# Next.js 16.3
npx create-next-app@latest my-next-app

# Nuxt 4.5
npx nuxi@latest init my-nuxt-app

Both generate a pages/ (or app/) directory where file names become routes automatically — no manual route array or JSX route tree required. This tutorial focuses on standalone React Router and Vue Router because that’s what you need to understand before a framework’s file-based routing makes sense; skipping straight to Next.js or Nuxt without this foundation makes debugging routing issues much harder later.

Step 11: Write Route Tests With Vitest

Vitest has become the default test runner for both ecosystems, with v4.1.11 released August 18, 2026 and a 5.0.0 release candidate already in testing. Install it alongside the appropriate testing library for each framework:

# React project
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom

# Vue project
npm install -D vitest @vue/test-utils jsdom

A React Router test using React Testing Library and createMemoryRouter to avoid touching the real browser history:

import { render, screen } from "@testing-library/react";
import { createMemoryRouter, RouterProvider } from "react-router";
import { test, expect } from "vitest";
import TaskList from "./pages/TaskList";

test("renders the task list at the root route", () => {
  const router = createMemoryRouter(
    [{ path: "/", element: <TaskList /> }],
    { initialEntries: ["/"] }
  );
  render(<RouterProvider router={router} />);
  expect(screen.getByRole("heading")).toBeInTheDocument();
});

The Vue Router equivalent, using Vue Test Utils and a fresh router instance per test:

import { mount } from "@vue/test-utils";
import { createRouter, createMemoryHistory } from "vue-router";
import { test, expect } from "vitest";
import TaskList from "./pages/TaskList.vue";

test("renders the task list at the root route", async () => {
  const router = createRouter({
    history: createMemoryHistory(),
    routes: [{ path: "/", component: TaskList }],
  });
  router.push("/");
  await router.isReady();

  const wrapper = mount(TaskList, { global: { plugins: [router] } });
  expect(wrapper.find("h2").exists()).toBe(true);
});

Both patterns rely on an in-memory router (createMemoryRouter / createMemoryHistory) so tests never touch the browser’s actual URL bar or history stack. Skipping this and mounting components with the real createBrowserRouter or createWebHistory in a test file is a common cause of flaky, order-dependent test failures.

Step 12: Handle 404s and Catch-All Routes

We already declared a catch-all in both route configs above, but it’s worth confirming the syntax difference explicitly, since it trips up developers switching stacks. React Router v8 uses a splat parameter:

{ path: "*", element: <NotFound /> }

Vue Router 4 requires a named parameter with a custom regex, because plain wildcards were removed from the path-matching syntax in Vue Router 4 (they worked differently in Vue Router 3):

{ path: "/:pathMatch(.*)*", name: "not-found", component: NotFound }

Copying a Vue Router 3 catch-all route (path: "*") into a Vue Router 4 project will silently fail to match anything — it throws no error, the route configuration just never triggers, and every unmatched URL will 404 at the server level instead of hitting your app’s custom NotFound component.

Step 13: Handle Query Strings and Search Params

Route params handle path segments like /tasks/:taskId, but most real apps also need query strings for filtering, sorting, or pagination — think /tasks?status=done&sort=recent. React Router v8 exposes this through the useSearchParams hook, which returns a URLSearchParams instance paired with a setter function:

import { useSearchParams } from "react-router";

export default function TaskList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const status = searchParams.get("status") ?? "all";

  function handleFilterChange(event) {
    setSearchParams({ status: event.target.value });
  }

  return (
    <select value={status} onChange={handleFilterChange}>
      <option value="all">All</option>
      <option value="done">Done</option>
      <option value="pending">Pending</option>
    </select>
  );
}

Vue Router exposes the same query object as a reactive property on useRoute(), and updates it through router.push or router.replace rather than a dedicated setter hook:

<script setup>
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";

const route = useRoute();
const router = useRouter();
const status = computed(() => route.query.status ?? "all");

function handleFilterChange(event) {
  router.push({ query: { ...route.query, status: event.target.value } });
}
</script>

<template>
  <select :value="status" @change="handleFilterChange">
    <option value="all">All</option>
    <option value="done">Done</option>
    <option value="pending">Pending</option>
  </select>
</template>

A subtle difference worth flagging: Vue’s router.push({ query: ... }) replaces the entire query object unless you manually spread the existing route.query first, as shown above. Forgetting the spread wipes out every other query param on the URL whenever you update just one filter — a bug that’s easy to miss in manual testing but shows up immediately once a page has two or more independent filters active at once.

Step 14: Compare Route Transition Performance

Routing libraries themselves add negligible overhead to page transitions — the real performance story in 2026 lives in the meta-frameworks built on top of them. Next.js 16.3’s “Instant Navigations” feature, which reached stable on August 3, 2026, introduces partial prefetching so a navigation can start rendering before the full route payload finishes downloading, and the Next.js team’s own build reports repeat builds running up to roughly 5.5x faster than the prior 16.2 baseline. Dev-server memory usage also dropped by up to roughly 90% in the same release, which matters more for local development speed than for end-user navigation latency, but it directly affects how fast your feedback loop is while working through the steps in this tutorial.

Nuxt doesn’t publish a directly comparable navigation-speed benchmark in its 4.5.x release notes, but its architecture inherits Vue Router’s client-side transition model, meaning route changes within a Nuxt app resolve as fast as a standard Vue Router navigation once the initial page has hydrated. The practical takeaway for this tutorial’s plain React Router and Vue Router setup (no meta-framework) is that transition speed between the two is essentially a wash — both resolve client-side navigations in single-digit milliseconds once route components are already loaded in memory. Where you’ll actually notice a difference is in lazy-loaded route bundles: the network request for a not-yet-downloaded chunk dominates perceived navigation time far more than any router-internal overhead, which is why the lazy-loading pattern in Step 8 and the prefetch-on-hover tip later in this article matter more for real-world performance than the router choice itself.

Feature Comparison: React Router v8 vs Vue Router 4.4+

FeatureReact Router v8Vue Router 4.4+
Route definition styleJSX tree or array of route objectsPlain array of route objects
Minimum framework versionReact 19.2.7+Vue 3.x only (no Vue 2 support)
Minimum Node.js version22.22.0+No hard requirement; matches Vite/Node LTS
Built-in navigation guardsNone (use loaders + redirect)beforeEach, beforeEnter, beforeRouteEnter
Data loadingBuilt-in loaders and actionsNot built in; use composables or onBeforeRouteUpdate
Lazy-loaded routeslazy route property or React.lazyNative dynamic import() as component value
Param accessuseParams() hookuseRoute() composable or props: true
Active link stylingNavLink render-prop isActiveAutomatic active-class on router-link
Catch-all route syntaxpath: “*”path: “/:pathMatch(.*)*”
Latest stable release (Aug 2026)v8.3.0 (July 22, 2026)v4.5.2-line via Nuxt tracking (Aug 5, 2026)
Companion meta-frameworkNext.js 16.3 (Aug 3, 2026)Nuxt 4.5.x (Aug 5, 2026); Nuxt v3 is EOL
Recommended test runnerVitest 4.1.x / 5.0.0-rcVitest 4.1.x / 5.0.0-rc

Search Interest and Ecosystem Signals

Search demand still favors React’s ecosystem by a wide margin: “react router” pulls roughly 6,600 monthly US searches versus roughly 1,300 for “vue router,” according to keyword research data pulled in August 2026, though both keywords carry low ranking competition, meaning there’s real room for fresh, updated tutorial content in both camps. That five-to-one search gap roughly tracks the broader framework popularity split reported in recent developer surveys, where React continues to lead as the most-used web framework and Vue holds a smaller but consistently loyal niche, particularly in Asia-Pacific markets and among teams that prioritize a gentler learning curve.

MetricReact Router ecosystemVue Router ecosystem
Monthly US search volume (primary keyword)~6,600~1,300
Keyword ranking difficultyLow (38/100)Low (36/100)
12-month search trend-64% year over year-62% year over year
Primary meta-framework release cadence (2026)Next.js ships roughly monthly patch/minor updatesNuxt ships roughly biweekly patch updates

The year-over-year decline in both keywords doesn’t mean interest in routing is disappearing — it more likely reflects that both ecosystems have matured to the point where fewer developers need to search for basic setup help, since file-based routing in Next.js and Nuxt increasingly absorbs the manual router-configuration step this tutorial covers.

Common Pitfalls When Switching Between React Router and Vue Router

These are the mistakes that show up most often in code review when a team has developers moving between the two stacks, or migrating an app from one to the other.

  • Installing the wrong package name. React Router v8 merged react-router-dom into react-router. Installing the old package name pulls a deprecated major version and causes two router instances to exist in the same app.
  • Forgetting props: true in Vue Router. Without it, route params must be read manually via useRoute().params; components expecting a direct prop silently receive undefined with no thrown error.
  • Reusing a Vue Router 3 wildcard catch-all. path: "*" does not match anything in Vue Router 4 — it requires path: "/:pathMatch(.*)*" instead, and the failure is silent.
  • Assuming React Router has guards. There is no beforeEach equivalent. Route protection has to be built with loaders that throw a redirect(), or a wrapper component checking auth state.
  • Not re-fetching on param change in Vue. Navigating between two routes that reuse the same component instance (e.g. /tasks/1 to /tasks/2) will not automatically re-run a fetch inside onMounted — you need onBeforeRouteUpdate.
  • Running React Router v8 on Node below 22.22.0. Installation may complete but the dev server throws obscure errors at build time rather than a clear version-mismatch message.
  • Mounting components with a real browser router inside unit tests. This causes tests to share history state across test files, producing flaky, order-dependent failures. Use createMemoryRouter or createMemoryHistory instead.
  • Skipping the router-view / Outlet placement. Both frameworks require an explicit render target for nested routes (<Outlet /> in React, <router-view> in Vue). Forgetting it means child routes match successfully in dev tools but never actually render anything on screen.

Expected Output

After completing steps 1 through 9, running npm run dev in either project should produce a working local server. The React project’s terminal output looks like this:

  VITE v7.1.2  ready in 312 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

Visiting http://localhost:5173/ should show the task list with a working “New Task” nav link, and navigating to /tasks/1 should render the task detail heading with the ID interpolated from the URL. The Vue project’s dev server output is nearly identical, just on Vue’s default port:

  VITE v7.1.2  ready in 289 ms

  ➜  Local:   http://localhost:5174/
  ➜  Network: use --host to expose

Running npx vitest run in either project after step 11 should report passing tests with output resembling:

 ✓ src/pages/TaskList.test.jsx (1 test) 42ms

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Duration  612ms

Troubleshooting Guide

These are the specific errors you’re most likely to hit while following this tutorial, and the fix for each one.

  • “Unsupported engine” warning on npm install: Your Node version is below 22.22.0. Run node --version and upgrade with nvm or your platform’s Node installer.
  • “Cannot find module ‘react-router-dom'”: You copied an old import statement. React Router v8 uses a single react-router package — update every import to drop the -dom suffix.
  • Route params come back undefined in a Vue component: Check that props: true is set on that route in your router config, or switch to reading useRoute().params directly.
  • Navigating to an unmatched Vue Router URL 404s at the server instead of showing your NotFound page: Your catch-all route is using the Vue Router 3 syntax. Replace path: "*" with path: "/:pathMatch(.*)*".
  • useLoaderData() returns undefined in React: Confirm the route actually has a loader property set in createBrowserRouter, and that you’re calling useLoaderData() from a component rendered by that specific route, not a parent or sibling.
  • Vue task detail page doesn’t update when navigating between two task IDs: Add an onBeforeRouteUpdate handler to re-run your fetch — the component instance is being reused, not remounted.
  • Blank white screen with no console errors after adding nested routes: You likely forgot to render <Outlet /> (React) or <router-view> (Vue) in the parent layout component.
  • Vitest tests fail intermittently in CI but pass locally: You’re probably instantiating a real browser-history router inside test files. Switch to createMemoryRouter or createMemoryHistory per test.
  • “vite: command not found” after npm install: Your global npm cache may be corrupted from a partial install. Delete node_modules and package-lock.json, then run npm install again.
  • React Router loader throws but the error page never renders: Add an errorElement property to the route definition — loaders that throw need an explicit error boundary route to catch the thrown Response or Error.

Advanced Tips for Production Routing

Once the basic setup works, a few production-oriented adjustments are worth making before shipping either app.

Prefetch on hover, not just on click. React Router v8’s data router supports link prefetching via the <Link prefetch="intent"> pattern in framework mode, warming up a route’s loader before the user actually clicks. Vue Router doesn’t ship this natively, but Nuxt’s built-in NuxtLink component prefetches automatically when a link enters the viewport, which is one of the stronger arguments for reaching for Nuxt over plain Vue Router on content-heavy sites.

Scroll restoration needs explicit configuration in both. Neither router restores scroll position on back/forward navigation by default in a way that always matches user expectation for SPAs with dynamic content heights. React Router ships a <ScrollRestoration> component you drop near the root of your app; Vue Router accepts a scrollBehavior function passed into createRouter() that returns a saved position or a target selector.

Guard against layout shift from lazy-loaded routes. Both lazy-loading patterns introduce a network round-trip before a route’s component code arrives. Pair every lazy route with a loading fallback (React’s Suspense fallback or Vue’s dedicated loading component pattern via defineAsyncComponent) sized to roughly match the eventual content, or you’ll see visible layout jumps on slower connections.

Consider a meta-framework once route count exceeds roughly a dozen pages. Hand-maintaining a route array or JSX tree past a certain size becomes a genuine maintenance cost. Next.js 16.3’s file-based app directory and Nuxt 4.5’s pages directory both eliminate that array entirely — file names become the route tree, which is a large part of why both frameworks continue gaining adoption even among teams that started with plain React Router or Vue Router.

Complete Working Project Structure

By the end of this tutorial, your React project should look like this:

react-router-demo/
├── src/
│   ├── pages/
│   │   ├── TaskList.jsx
│   │   ├── TaskDetail.jsx
│   │   ├── NewTask.jsx
│   │   └── NotFound.jsx
│   ├── App.jsx
│   ├── router.jsx
│   └── main.jsx
├── package.json
└── vite.config.js

And your Vue project should mirror it with the framework-specific conventions:

vue-router-demo/
├── src/
│   ├── pages/
│   │   ├── TaskList.vue
│   │   ├── TaskDetail.vue
│   │   ├── NewTask.vue
│   │   └── NotFound.vue
│   ├── router/
│   │   └── index.js
│   ├── App.vue
│   └── main.js
├── package.json
└── vite.config.js

Both projects run with the same two commands: npm run dev for local development and npm run build to produce a production bundle. Neither router requires additional build configuration beyond what Vite’s default templates already provide.

Which Router Should You Choose?

If you’re already committed to React, React Router v8 is the only realistic option and it’s a mature, well-tested library backed by the same team behind Remix, with a full changelog documenting its steady release cadence. Its loader-and-action model is genuinely powerful once you internalize it, even though it takes longer to learn than Vue Router’s more explicit guard system. If you’re on Vue, React’s own version history shows a similarly steady cadence on the framework side, and Vue Router 4 remains the default and only first-party choice for Vue 3, with its navigation guards and prop-based param passing making route protection and data flow slightly more explicit to newcomers.

Neither library is objectively “better” — they reflect the design philosophy of their parent frameworks. React Router pushes routing logic toward composable hooks and data-loading conventions; Vue Router keeps routing closer to a configuration file with explicit lifecycle hooks. If your team already knows one framework, the router choice isn’t really a choice at all. If you’re evaluating both frameworks fresh, current usage data from the Stack Overflow Developer Survey shows React still leading in overall adoption, but the routing layer alone probably shouldn’t be the deciding factor — state management patterns, ecosystem size, and team familiarity matter more for that decision.

Related Coverage

Frequently Asked Questions

Is react-router-dom still a valid package to install in 2026?
No. React Router v8 merged react-router-dom into the single react-router package. Installing the old name pulls a deprecated version and can create duplicate router instances in your app.

Does Vue Router support Vue 2 projects?
No. Vue Router 4 targets Vue 3 exclusively. Vue 2 projects need to stay on Vue Router 3, which is no longer actively developed.

What’s the minimum Node.js version for React Router v8?
Node 22.22.0 or newer. Installing on an older Node version will produce unsupported-engine warnings and, in some cases, obscure build-time failures.

Can I use React Router’s loader pattern outside of createBrowserRouter?
No. Loaders only run within the data router APIs (createBrowserRouter, createMemoryRouter, createHashRouter). The older <BrowserRouter> component-based API does not support loaders at all.

Why doesn’t my Vue Router catch-all route work?
Vue Router 4 requires path: "/:pathMatch(.*)*" for a wildcard match. The Vue Router 3 syntax of path: "*" does not work in version 4 and fails silently, with no error thrown.

Should I use Next.js or Nuxt instead of the routers directly?
For apps needing server-side rendering, static generation, or file-based routing at scale, yes. Next.js 16.3 and Nuxt 4.5.x both wrap React Router’s and Vue Router’s underlying mechanics into a more automated, convention-based system. For smaller SPAs or when you need fine-grained control over routing behavior, plain React Router or Vue Router remains simpler.

Which testing library should I use for routed components?
React Testing Library paired with Vitest for React apps, and Vue Test Utils paired with Vitest for Vue apps. Both are the current standard as of August 2026 and both integrate cleanly with Vite-based projects.

Is Vue Router’s search volume declining because Vue is losing popularity?
Not necessarily. Both “react router” and “vue router” search volumes dropped year over year (-64% and -62% respectively) in 2026 keyword data, which more likely reflects that file-based routing in Next.js and Nuxt is absorbing searches that used to go toward manual router setup, rather than declining interest in either framework overall.

Nadia Dubois

Nadia Dubois

AI & Innovation Editor

Nadia Dubois is the AI & Innovation Editor at Tech Insider, where she tracks the rapid evolution of artificial intelligence, from foundation models to real-world enterprise deployment. She previously covered AI and startups for La Tribune and contributed to MIT Technology Review's European coverage. Nadia specializes in generative AI, AI regulation, and the intersection of technology and European industrial policy. She holds a dual degree in Computational Linguistics and Journalism from Sciences Po Paris.

View all articles