TL;DR: React Router 7 introduces framework mode for SSR, streamlined data fetching with loaders and actions, enhanced routing, and better error handling.
Single-page application (SPA) frameworks, and client-side rendered frameworks like React, have always relied on external libraries to manage routing and navigation. React Router has been at the forefront of this approach since its inception.
For years, it has been the fundamental tool in the React ecosystem for navigation, continuously evolving to meet the ever-changing demand of modern web development.
React Router version 7 has taken this library to a new height by bringing in extensive new features and capabilities like a framework mode, improved routing, data management, error handling, and streamlined APIs.
Note: The Syncfusion React JS UI components library is the only suite that you will ever need to build an application since it contains over 90 high-performance, lightweight, modular, and responsive UI components in a single package.
What has improved in React Router 7?
Mutations and data handling:
React Router 7 enhances data handling and mutations by introducing built-in loader and action functions for both server-side and client-side rendering. The server-side function takes precedence for loaders, while the client-side function takes precedence for actions. They enable developers to load the essential data on the server efficiently while fetching additional data on the client, improving overall performance.
Loaders
Data fetching is streamlined through loader functions that operate on both the client and the server side. These functions can be defined directly within your route definitions. Loaders prefetch data from the API before rendering the associated route components, eliminating the need for multiple API calls and unnecessary component re-rendering.
By adopting this method, you can maintain a clean architecture that clearly separates routing and data operations.
Example: Defining a client-side loader function in a declarative way
import { useLoaderData } from "react-router-dom";
export async function profileLoader({ params }) {
const response = await fetch(`/api/v1/users/${params.id}`);
return response.json();
}
function ProfilePage() {
const userData = useLoaderData();
return (
<div>
<h1>User: {userData.name}</h1>
</div>
);
}
// Route Configuration
<Route path="/users/:id" element={<ProfilePage />} loader={profileLoader} />;
React Router 7 comes with enhanced typed safety, allowing us to provide first-class types for loaders, actions, and route parameters, which helps reduce runtime errors.
import { useLoaderData } from "react-router-dom";
type ProfileData = {
user: User,
};
export async function profileLoader({ params }) {
const response = await fetch(`/api/v1/users/${params.id}`);
return response.json();
}
function ProfilePage() {
const userData = useLoaderData() as ProfileData;
return (
<div>
<h1>User: {userData.name}</h1>
</div>
);
}
// Route Configuration
<Route
path="/users/:id"
element={<ProfilePage />}
loader={profileLoader}
/>;
In framework mode with SSR enabled, we define server-side and client-side loaders differently.
// route("blog/:id", "./blog.tsx");
import type { Route } from "./+types/blog";
import { mockDb } from "../db";
export async function loader({ params }: Route.LoaderArgs) {
return mockDb.getArticle(params.pid);
}
export async function clientLoader({
serverLoader,
params,
}: Route.ClientLoaderArgs) {
const res = await fetch(`/api/blog/${params.pid}`);
const serverData = await serverLoader();
return { ...serverData, ...res.json() };
}
// This component is rendered while the client loader is running
export function HydrateFallback() {
return <div>Loading...</div>;
}
export default function Blog({ loaderData }: Route.ComponentProps) {
const { title, article } = loaderData;
return (
<div>
<h1>{title}</h1>
<p>{article}</p>
</div>
);
}
Actions
We have built-in methods to handle form submissions and state mutation. Form submissions can be processed on the server-side, where the form action URL is added to the browser history, or on the client side, where navigation can be disabled to handle the form without a full submission.
Client actions
This function runs only in the browser and takes priority over the server actions.
// route('/todo/:todoId', './todo.tsx')
import type { Route } from "./+types/todo";
import { Form } from "react-router";
import { dummyApi } from "./api";
export async function clientAction({
request,
}: Route.ClientActionArgs) {
let formData = await request.formData();
let title = formData.get("title");
let todo = await dummyApi.updateTodo({ title });
return todo;
}
export default function Todos({
actionData,
}: Route.ComponentProps) {
return (
<div>
<h1>Todos</h1>
<Form method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
{actionData ? (
<p>{actionData.title} updated</p>
) : null}
</div>
);
}
Server actions
This function runs only on the server, and the action URL is updated in the browser history. Because these functions run on the server, they are not included in the client bundle.
// route('/todo/:todoId', './todo.tsx')
import type { Route } from "./+types/todo";
import { Form } from "react-router";
import { dummyApi } from "./api";
export async function action({ request }: Route.ActionArgs) {
let formData = await request.formData();
let title = formData.get("title");
let todo = await dummyApi.updateTodo({ title });
return todo;
}
export default function Todos({ actionData }: Route.ComponentProps) {
return (
<div>
<h1>Todos</h1>
<Form method="post" action="/todo/123">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
{actionData ? <p>{actionData.title} updated</p> : null}
</div>
);
}
Note: Omitting the form action prevents that URL from being added to the browser history, even on the server-side.
Route-level error boundaries
With React Router 7, we can define error boundaries more granularly at the route level. This approach keeps errors isolated to individual routes, ensuring that they don’t impact the entire application.
Using the useRouterError() hook, we can also display more contextual error messages based on the type of error encountered.
import { useRouteError } from "react-router-dom";
function CustomErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<>
<h1>
{error.status} {error.statusText}
</h1>
<div>{error.data}</div>
</>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<div>{error.message}</div>
<div>Stack trace is:</div>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
<Route
path="/users/:id"
element={<UserPage />}
errorElement={<CustomErrorBoundary />}
/>;
Shared UI and nested routing with layout routes
With React Router 7, we can create a layout that includes common UI components and a placeholder provided by React Router. The is replaced with the actual component on that route.
import { Outlet } from "react-router";
const PrimaryDashboardLayout = () => {
const newNav = useFeatureFlag('showNewNav');
return (
<body>
<main>
<Outlet />
</main>
{newNav ? <SidebarNew /> : <Sidebar />}
</body>
);
}
// Nested routings
<Route path="dashboard" element={<PrimaryDashboardLayout />}>
<Route path="overview" element={<Overview />} />
<Route path="settings" element={<Settings />} />
</Route>
This structure helps create different layouts for various screen sizes and devices while avoiding code redundancy and promoting re-usability.
Code splitting and asynchronous data loading with React 18’s Suspense
React Router 7 supports React 18’s Suspense and Await features, enabling efficient data loading, concurrent rendering, and improved performance by reducing UI blocking during asynchronous operations.
Example: Using suspense for data loading
In this example, a loading message is displayed as a fallback while data is being fetched.
import { Suspense } from "react";
import { Await, useLoaderData } from "react-router-dom";
const ProductPage = () => {
const { product } = useLoaderData();
return (
<Suspense fallback={<div>Loading products details...</div>}>
<Await resolve={product}>
{(resolvedProduct) => <ProductDetails product={resolvedProduct} />}
</Await>
</Suspense>
);
};
You can further optimize by differentiating between the critical and non-critical data, streaming the critical data on demand.
Example: Using suspense for lazy-loading components
Lazy-loading components help with code splitting and reduce the initial bundle size.
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const ProductPage = lazy(() => import('./ProductPage'));
const ContactUsPage = lazy(() => import('./ContactUsPage'));
const App = () => {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<ProductPage />} />
<Route path="contact" element={<ContactUsPage />} />
</Routes>
</Suspense>
);
};
Code-splitting leads to smaller bundle sizes, faster initial load time, and an enhanced user experience.
Framework mode
React Router 7 introduces Framework mode, allowing you to build full-stack applications (including server-side rendering) with the same familiar API, eliminating the need for an additional SSR framework like Next.js.
Framework mode enables features such as:
- Server-side rendering (SSR).
- Pre-rendering or static content generation.
- Server-side form submission.
- Optimization techniques like code-splitting and tree-shaking.
- File-based routing.
Since you use one tool for all your needs, React Router 7 reduces the learning curve and lets developers focus on building features, enhancing overall productivity.
Note: Next.js recently experienced a significant vulnerability, prompting exploration of alternatives that offer similar SSR capabilities within a familiar environment. React Router is one to consider.
Setup framework mode
React Router 7 abstracts many APIs into a single export while remaining completely backward compatible. To set up Framework mode, first install the latest React Router library.
npm install react-router
This package includes all the APIs and methods required for client-side navigation and server-side rendering.
In the root directory of your project, create a file named react-router.config.ts to hold the configuration for enabling server-side rendering. Add the following configuration to this file.
import type { Config } from "react-router";
export default {
ssr: true, // Enables Server-Side Rendering
} satisfies Config;
Note: Server-side rendering requires a deployment strategy that supports it, so you’ll need to configure your server accordingly. Although this setting is applied globally to all the routes, individual routes can be pre-rendered.
import type { Config } from "@react-router/dev/config";
export default {
// Return a list of routes or paths that you want to prerender at build time
async prerender() {
return ["/", "/privacy-policy", "/about-us"];
},
} satisfies Config;
You can further optimize by doing client-side hydration by loading data on the client side using the clientLoader methods for the partial UI content.
We can also enable file-based routing by setting the app directory in the configuration.
import type { Config } from "react-router";
export default {
appDirectory: "src",
ssr: true, // Enables Server-Side Rendering
} satisfies Config;
This will tell the React-router that each file in the routes folder corresponds to a defined route under the src directory.
Example
src/
├── routes/
│ ├── index.tsx // Maps to "/"
│ ├── about-us.tsx // Maps to "/about-us"
│ └── contact-us.tsx // Maps to "/contact-us"
The dot delimiters in the file name will treat the parts after the dot as the sub-routes.
src/
├── routes/
│ ├── index.tsx // Maps to "/"
│ ├── about-us.in.tsx // Maps to "/about-us/in"
│ └── contact-us.eu.tsx // Maps to "/contact-us/eu"
If you want to have dynamic sub-routes, you can prefix the file with the $ symbol, which you can then access in your loader or action functions as a parameter.
src/
├── routes/
│ ├── index.tsx // Maps to "/"
│ ├── about-us.$city.tsx // Maps "/about-us/in" to src/routes/about-us.$city.tsx
│ └── contact-us.$city.tsx // Maps "/contact-us/eu" to src/routes/contact-us.$city.tsx
export async function serverLoader({ params }) {
return fakeDb.getAllConcertsForCity(params.city); // in | eu
}
You can also do nested routing following this structure. Read more about it in the official React Router 7 document.
You can partially or completely prerender all the routes on the server. This will improve the performance as well as the SEO.
import { createStaticRouter } from "react-router";
import routes from "./routes";
const router = createStaticRouter(routes);
const preRenderedHtml = renderToString(<RouterProvider router={router} />);
Set the SSR to false for the client-side rendering.
import type { Config } from "react-router";
export default {
ssr: false, // Enables Client-Side Rendering
} satisfies Config;
Apart from these changes, React Router 7 has also introduced some major enhancements:
- HMR (Hot Module Replacement): React Router 7 comes with an enhanced and integrated development server with HMR that instantly reflects the changes in the directory to the codebase in the browser without the need for the manual reload. The enhanced HMR comes with better error handling and real time logs, improving the developer experience and making it easier for them to do debugging.
- Optimized asset building: With the help of modern bundlers like Webpack and Vite, React Router 7 does advanced asset building by intelligently splitting bundles and caching the static assets, improving the load time. React Router achieves this optimization with the help of techniques such as tree-shaking, code-splitting, and lazy-loading. These help load only necessary assets and lazy load the remaining on demand, reducing the bandwidth usage and improving the performance.
Why is Framework mode a blessing?
As we can use one tool for all our needs, React Router 7 means we don’t need a new framework like Next.js for SSR. It reduces the learning curve and enables developers to focus on the actual development, enhancing their productivity.
What has changed in React Router 7?
Removed json() function
No more json() utility function. React Router 6 introduced the json() utility function that helped convert the response to JSON in the loader function.
In React Router V6 using the json() function:
import { json } from "react-router-dom";
export async function loader() {
const res = await fetch(`/api/blog/${params.pid}`);
return json(res);
}
In React Router 7:
export async function loader() {
const res = await fetch(`/api/blog/${params.pid}`);
return await res.json();
}
This way, developers will write more native code and have more control over the flow of the response.
Removed defer function
The defer method, which was used to do the data loading when only the component was mounted on the router, has been removed. There is no need for this extra wrapper, as the promises can be directly passed to the component through the loader function and can be handled natively.
In React Router 6 using the defer() function:
import { defer } from "react-router-dom";
export async function loader() {
return defer({
data: makeApiCall(),
});
}
In React Router 7:
export async function loader() {
return {
data: makeApiCall(),
};
}
Migrating from version 6 to 7
The React Router 7 is backward compatible, as all the methods used in version 6 are still available; you can update the package, and things should work fine.
But it is advised to update the imports, change the methods, and then test the codebase for proper migration.
- Modify the code base and update from react-router-dom to react-router for v7
- Update the imports through the codebase
- Test thoroughly
Also, React Router supports Future flags, where you can manually enable flags and then do the migration part by part.
<BrowserRouter
future={{
v7_relativeSplatPath: true,
}}
/>
For more details, follow the official migration document.
Conclusion
React Router version 7 is a promising leap in the routing capabilities of the React ecosystem. With it, you can use modern features from the SSR frameworks with the same developer-friendly and already-familiar API structure.
This version significantly improves the data loading and handling, type safety, error handling, and server-side capabilities, taking front-end application development in React to newer heights.
Syncfusion React UI components library is the only suite that you will ever need to build an app since it contains over 90 high-performance, lightweight, modular, and responsive UI components in a single package.
If you’re an existing customer, you can download the latest version of Essential Studio® from the License and Downloads page. For those new to Syncfusion, try our 30-day free trial to explore all our features.
If you have questions, contact us through our support forum, support portal, or feedback portal. As always, we are happy to assist you!
Top comments (0)