React Router v7 (Framework Usage) Practical Guide: Learn the Latest Routing by Building a Blog Site

  • vite
    vite
  • remix
    remix
Published on 2025/01/23

Introduction

In this article, we’ll cover a wide range of topics from the basics of building a blog site using the framework features of React Router v7 to more complex routing examples. By reading this article, you’ll learn everything from simple page transitions to dynamic routing and even how to configure nested routes.
Whether you’re a beginner or someone looking to level up, let’s explore the world of React Router v7 together.

What is React Router?

React Router is a routing library for React applications. Routing is the mechanism that controls which component is displayed when a user accesses a specific URL.

https://reactrouter.com/home

Starting from v7, React Router can be used either as a full-fledged React framework or as a minimal library that fits into your own architecture.

What does “framework usage” mean?

From v7 onward, React Router can be used as a React framework.

Specifically, it provides features like the following:

  • Integration with the Vite bundler and development server
  • Hot Module Replacement
  • Code splitting
  • Type-safe file-system or config-based routing
  • Type-safe data loading
  • Type-safe actions
  • Automatic revalidation of page data after actions
  • SSR, SPA, and static rendering strategies
  • Pending states and optimistic UI
  • Deployment adapters

The features we’ll focus on in this article are mainly these.

We’ll walk through in detail how to leverage these features under the assumption that we’re building a blog site.

What does “library usage” mean?

In previous versions (v6 and earlier), React Router can be used as a simple, declarative routing library.
You match URL–component pairs, provide access to URL data, and navigate within the app.

If you’ve been using v6, you’ll likely continue to use React Router as a library even after upgrading to v7.

If you’d like to learn more about using React Router v7 as a library, please refer to the following:

https://shinagawa-web.com/en/blogs/react-router-v7-library-blog-setup-guide

Goal of this article

Using the framework features of React Router v7, we’ll focus on implementing routing so that we can navigate to the following pages:

  • Top page

  • Post

  • My Page

    • Account

    • Settings page

We’ll also create navigation so that each page is easy to access.

Image from Gyazo

Setup

We’ll create a project using the template provided by React Router.

mkdir react-router-v7-framework-tutorial
cd react-router-v7-framework-tutorial
npx create-react-router@latest .

Image from Gyazo

This completes both the code setup and downloading of dependency packages.

Start the app with the following command:

npm run dev

Image from Gyazo

Let’s take a look at the README.md in the project.

- 🚀 Server-side rendering
- ⚡️ Hot Module Replacement (HMR)
- 📦 Asset bundling and optimization
- 🔄 Data loading and mutations
- 🔒 TypeScript by default
- 🎉 TailwindCSS for styling
- 📖 [React Router docs](https://reactrouter.com/)

All of these features are already available out of the box.

Also, if you check package.json, you’ll see that the React version is the latest 19, and the framework is designed with React 19’s features in mind.

package.json
  "dependencies": {
    "@react-router/node": "^7.1.3",
    "@react-router/serve": "^7.1.3",
    "isbot": "^5.1.17",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-router": "^7.1.3"
  },

Routing

As you saw when you started the project and the screen appeared, the top page routing is already set up at the template stage.

app/routes.ts
import { type RouteConfig, index } from "@react-router/dev/routes";

export default [index("routes/home.tsx")] satisfies RouteConfig;

This configuration renders routes/home.tsx when the top-level path is accessed.
When using React Router as a framework, this app/routes.ts file is the main file responsible for routing.

routes/home.tsx
import type { Route } from "./+types/home";
import { Welcome } from "../welcome/welcome";

export function meta({}: Route.MetaArgs) {
  return [
    { title: "New React Router App" },
    { name: "description", content: "Welcome to React Router!" },
  ];
}

export default function Home() {
  return <Welcome />;
}

There are two exports, but the default export is the part that actually renders.

export default function Home() {
  return <Welcome />;
}

The actual component is ../welcome/welcome.tsx.

Now that you have a rough idea of the structure of the app directory, let’s first modify the top page.

routes/home.tsx
import type { Route } from "./+types/home";
- import { Welcome } from "../welcome/welcome";

export function meta({}: Route.MetaArgs) {
  return [
    { title: "New React Router App" },
    { name: "description", content: "Welcome to React Router!" },
  ];
}

export default function Home() {
- return <Welcome />;

+ return (
+   <h1>
+     Top
+   </h1>
+ )
}

Delete welcome and display the text Top.

Image from Gyazo

The top page alone is a bit plain, so let’s display some blog posts.

app/const/posts.ts
export const posts = [
  {
    id: 1,
    title: 'Setting up React Router 7',
    description: 'Introduction to setup and various features',
  },
  {
    id: 2,
    title: 'State Management in React',
    description: 'Introduction to useState, useContext, and more',
  },
  {
    id: 3,
    title: 'Component Design Best Practices',
    description: 'Introduction to directory structure and more',
  },
]

export const getPostById = (id: number) => {
  return posts.find((post) => post.id === id)
}

In a real application, you would manage and fetch blog information from a database or CMS, but that would be a big detour from the topic of this article, so we’ll prepare some simple data in a file.

We’ve prepared three blog entries and a function that retrieves a blog entry by its ID.

This uses a different architecture, but I’ve also created tutorials in the past on fetching data from a database or CMS, so if you’re interested, please refer to these as well:

https://shinagawa-web.com/en/blogs/nextjs-microcms-blog-tutorial

https://shinagawa-web.com/en/blogs/express-mongodb-rest-api-development-with-typescript

Back to the main topic.

Now that we have the blog data, let’s display it on the top page.

routes/home.tsx
+ import { posts } from '~/const/posts'
import type { Route } from './+types/home'

export function meta({}: Route.MetaArgs) {
  return [
    { title: 'New React Router App' },
    { name: 'description', content: 'Welcome to React Router!' },
  ]
}

export default function Home() {
  return (
   <>
      <h1>Top</h1>
+     <ul>
+       {posts.map((post) => (
+         <li key={post.id}>{post.title}</li>
+       ))}
+     </ul>
+   </>
  )
}

The titles of the blog data we just created are now displayed on the top page.

Image from Gyazo

Dynamic segments

Next, we’ll set up the configuration for viewing individual blog posts.

The idea is that when you click a blog title on the home page, you’ll navigate to that blog’s individual post page.

First, let’s configure the routing.

app/routes.ts
- import { type RouteConfig, index } from '@react-router/dev/routes'
+ import { type RouteConfig, index, route } from '@react-router/dev/routes'

export default [
  index('routes/home.tsx'),
+ route('post/:postId', './routes/post.tsx'),
] satisfies RouteConfig

We’ve configured post/:postId to display an individual blog post. postId is intended to be the ID of the blog data. This is what’s called a dynamic segment.

Next, create the component that will handle the display.

app/routes/post.tsx
import { getPostById } from '~/const/posts'
import type { Route } from './+types/post'

export default function Post({ params }: Route.ComponentProps) {
  const post = getPostById(Number(params.postId))

  if (!post) {
    return <div>Post not found.</div>
  }

  return (
    <div>
      <h1>{post.title}</h1>
      {post.description}
    </div>
  )
}
import type { Route } from './+types/post'

This is the type definition that’s automatically generated when you configure the route.

export default function Post ({
  params,
}: Route.ComponentProps) {

This makes params available.

You can also confirm in your editor that params.postId gives you the postId passed in the URL.

Image from Gyazo

  const post = getPostById(Number(params.postId))

We use postId to fetch a single blog entry.

  if (!post) {
    return <div>Post not found.</div>
  }

  return (
    <div>
      <h1>{post.title}</h1>
      {post.description}
    </div>
  )

We render different content depending on the fetch result.

We also handle the case where a request is made with an ID that doesn't exist in the blog data by displaying the message Post not found..

If the blog data is found, we display its title and description.

Now let’s actually test it.

When the blog data exists:

Image from Gyazo

When the blog data does not exist:

Image from Gyazo

Finally, let’s configure navigation from the blog list to the individual blog posts.

routes/home.tsx
import { posts } from '~/const/posts'
import type { Route } from './+types/home'
+ import { Link } from 'react-router'

export function meta({}: Route.MetaArgs) {
  return [
    { title: 'New React Router App' },
    { name: 'description', content: 'Welcome to React Router!' },
  ]
}

export default function Home() {
  return (
   <>
      <h1>Top</h1>
      <ul>
        {posts.map((post) => (
-         <li key={post.id}>{post.title}</li>
+         <li key={post.id}>
+           <Link to={`/post/${post.id}`}>{post.title}</Link>
+         </li>
        ))}
      </ul>
    </>
  )
}

Using Link from React Router, you can navigate to the URL specified in to when the link is clicked.

Now that everything is set up, let’s check that we can navigate from the home screen.

Image from Gyazo

It’s starting to look a bit more like a blog site.

Nested routes

Next, let’s create a My Page section for the blog site.

On My Page, we’ll create a screen to view account information and another to change settings.

app/routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes'

export default [
  index('routes/home.tsx'),
  route('post/:postId', './routes/post.tsx'),
+ route('mypage', './routes/mypage/index.tsx', [
+   route('account', './routes/mypage/account.tsx'),
+   route('settings', './routes/mypage/settings.tsx'),
+ ]),
] satisfies RouteConfig

Routes can be nested.

Accessing /mypage will display ./routes/mypage/index.tsx.

Let’s create the app/routes/mypage/index.tsx file.

app/routes/mypage/index.tsx
import { Outlet } from 'react-router'

export default function MyPage() {
  return (
    <>
      <h1>My Page</h1>
      <Outlet />
    </>
  )
}

Outlet is a React Router component that acts as a placeholder for rendering the content of nested routes.

Concretely, you define shared layout or components (e.g., header or sidebar) in the parent route, and use Outlet to embed the child route’s content within that layout.

Now let’s create the child routes, account.tsx and settings.tsx.

app/routes/mypage/account.tsx
export default function Account() {
  return <h2>Account</h2>
}
app/routes/mypage/settings.tsx
export default function Settings() {
  return <h2>Settings</h2>
}

With this setup, you get a structure where Account and Settings screens belong to a single group called My Page.

Now let’s check that we can access the nested routes.

First, /mypage

You should see the text “My Page”.

Image from Gyazo

Next, /mypage/account

You can confirm that both the parent route text "My Page" and the child route text "Account" are displayed.

This shows that Outlet is working correctly and rendering the child route.

Image from Gyazo

This routing configuration works as is, but there are a few convenient features, so let’s introduce them as well.

app/routes.ts
- import { type RouteConfig, index, route } from '@react-router/dev/routes'
+ import { type RouteConfig, index, layout, prefix, route } from '@react-router/dev/routes'

export default [
  index('routes/home.tsx'),
  route('post/:postId', './routes/post.tsx'),
- route('mypage', './routes/mypage/index.tsx', [
-   route('account', './routes/mypage/account.tsx'),
-   route('settings', './routes/mypage/settings.tsx'),
- ]),
+ ...prefix('mypage', [
+   index('./routes/mypage/index.tsx'),
+   layout('./routes/mypage/layout.tsx', [
+     route('account', './routes/mypage/account.tsx'),
+     route('settings', './routes/mypage/settings.tsx'),
+   ])
+ ])
] satisfies RouteConfig

By using prefix, you can add a path prefix to a set of routes without creating a parent route file.

This is useful when you want nested routes but don’t need a dedicated component for the parent.

...prefix('mypage', [

An index route is rendered into its parent’s Outlet at the parent’s URL (like a default child route).

  index('./routes/mypage/index.tsx'),

However, since the parent uses a prefix, the result of rendering ./routes/mypage/index.tsx is displayed directly instead of using an Outlet.

In other words, with this configuration, accessing /mypage will display ./routes/mypage/index.tsx.

  layout('./routes/mypage/layout.tsx', [

This is layout, which lets you define a layout-only component.

You use it when you don’t want to nest URLs, but you do want to apply a common layout to child routes like account.tsx and settings.tsx.

With this setup, you can more finely separate the relationship between parent and child routes.

Let’s create and edit some components to confirm the behavior.

First, the layout component:

app/routes/mypage/layout.tsx
import { Outlet } from 'react-router'

export default function MyPage() {
  return (
    <>
      <h1>My Page</h1>
      <Outlet />
    </>
  )
}

With this configuration, the text "My Page" will be displayed for child routes where this layout is applied.

Next is the component displayed when /mypage is accessed.

app/routes/mypage/index.tsx
- import { Outlet } from 'react-router'

export default function MyPage() {
  return (
    <>
      <h1>My Page Top</h1>
-    <Outlet />
    </>
  )
}

When /mypage is accessed, it will display “My Page Top”.

Checking the behavior

First, /mypage

You should see the text “My Page Top”.

Image from Gyazo

Next, /mypage/account

You can confirm that both the layout text "My Page" and the child route text "Account" are displayed.

Image from Gyazo

With the initial nested route setup, the parent route’s content directly affected the child routes, but with this configuration, we’ve separated them so they can operate independently.

So far, we’ve defined several URLs.

At this volume, it’s getting cumbersome to type URLs directly into the browser each time, so let’s create navigation to make each page easier to access.

Before that, let's organize the current page transitions.

  • Top page
    /routes/home.tsx

  • Post
    /post/:postIdroutes/post.tsx (:postId is the post ID)

  • My Page
    /mypageroutes/mypage/index.tsx

    • Account
      /mypage/accountroutes/mypage/account.tsx

    • Settings page
      /mypage/settingsroutes/mypage/settings.tsx

Based on this, we’ll prepare two types of navigation:

  • Navigate from the top page to My Page
  • Navigate between Account and Settings within My Page

We’ll create a navigation component to display in the header.

app/components/home-navigation.tsx
import { NavLink } from 'react-router'

export function HomeNavigation() {
  return (
    <nav className='pb-5 mb-5 border-b flex gap-4'>
      <NavLink to="/" end>
        Top
      </NavLink>
      <NavLink to="/mypage">
        My Pagege
      </NavLink>
    </nav>
  )
}

We’ll use NavLink from React Router.

NavLink is for navigation links that need to render an active state.

Specifically, when the corresponding URL is accessed, an active class is applied.

You can then use CSS to change the text color or add a background color for elements with the active class.

app.css
@tailwind base;
@tailwind components;
@tailwind utilities;

html,
body {
  padding: 20px;
}

h1 {
  @apply font-bold text-xl
}

h2 {
  @apply font-semibold text-lg
}

.active {
  @apply text-sky-600;
}

In this example, we set the text color to blue for elements with the active class.

Now that we’ve created the navigation component, let’s apply it to the top page.

routes/home.tsx
import { posts } from '~/const/posts'
import type { Route } from './+types/home'
import { Link } from 'react-router'
+ import { HomeNavigation } from '~/components/home-navigation'

export function meta({}: Route.MetaArgs) {
  return [
    { title: 'New React Router App' },
    { name: 'description', content: 'Welcome to React Router!' },
  ]
}

export default function Home() {
  return (
    <>
-     <h1>Top</h1>
+     <HomeNavigation />
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link to={`/post/${post.id}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </>
  )
}

While we’re at it, let’s also apply the navigation to the My Page top.

app/routes/mypage/index.tsx
+ import { HomeNavigation } from "~/components/home-navigation";

export default function MyPage() {
  return (
    <>
+     <HomeNavigation />
      <h1>My Page Top</h1>
    </>
  )
}

Now let’s check that we can access both the top page and the My Page top using the navigation.

Image from Gyazo

As expected, we can switch pages, and the text color turns blue to indicate where we currently are.

Next, we’ll create navigation within My Page to make it easy to move between the Account and Settings screens.

app/components/mypage-navigation.tsx
import { NavLink } from 'react-router'

export function MyPageNavigation() {
  return (
    <nav className="mb-5 flex gap-4 border-b pb-5">
      <NavLink to="/mypage/account" end>
        Account
      </NavLink>
      <NavLink to="/mypage/settings">
        Settings
      </NavLink>
    </nav>
  )
}

This is basically the same structure as the navigation we created for the top page earlier, except that the link destinations are set for the Account and Settings screens.

Now let’s apply the navigation to each screen.

First, the layout for My Page’s child routes:

app/routes/mypage/layout.tsx
import { Outlet } from 'react-router'
+ import { HomeNavigation } from '~/components/home-navigation'
+ import { MyPageNavigation } from '~/components/mypage-navigation'

export default function MyPage() {
  return (
    <>
+     <HomeNavigation />
+     <MyPageNavigation />
      <Outlet />
    </>
  )
}

In addition to the new My Page navigation, we’ve also added navigation to go back to the top page.
This allows navigation from Account or Settings back to the top page.

Next, the My Page top:

app/routes/mypage/index.tsx
import { HomeNavigation } from '~/components/home-navigation'
+ import { MyPageNavigation } from '~/components/mypage-navigation'

export default function MyPage() {
  return (
    <>
      <HomeNavigation />
+     <MyPageNavigation />
      <h1>My Page Top</h1>
    </>
  )
}

We’ve added navigation so that you can move from the My Page top to the Account or Settings screens.

Finally, let’s adjust the navigation that leads to the top page.

app/components/home-navigation.tsx
import { NavLink } from 'react-router'

export function HomeNavigation() {
  return (
    <nav className="mb-5 flex gap-4 border-b pb-5">
      <NavLink to="/" end>
        Top
      </NavLink>
-     <NavLink to="/mypage" end>
+     <NavLink to="/mypage">
        My Page
      </NavLink>
    </nav>
  )
}

We removed end.
When end is present, the active class is applied only when the URL exactly matches the one specified in to.
When it’s omitted, the active class is applied to all URLs that start with the URL specified in to.

Now let’s test the behavior.

Accessing / works as before.

Image from Gyazo

Clicking My Page navigates to the My Page top and shows the My Page navigation.

Image from Gyazo

Clicking Settings navigates to the Settings screen, and both "My Page" and "Settings" are shown in blue.

This makes it clear that you’re currently on the Settings screen within My Page. (In a real UI, you’d probably want to make this even clearer with icons or different font sizes.)

From here, clicking "Top" will take you back to the top page.

Image from Gyazo

One thing we forgot: let’s also add navigation to the component that displays individual blog posts.

import { getPostById } from '~/const/posts'
import type { Route } from './+types/post'
+ import { HomeNavigation } from '~/components/home-navigation'

export default function Post({ params }: Route.ComponentProps) {
  const post = getPostById(Number(params.postId))

  if (!post) {
    return (
+     <>
+       <HomeNavigation />
+       <div>Post not found.</div>
+     </>
    )
  }

  return (
    <div>
+     <HomeNavigation />
      <h1>{post.title}</h1>
      {post.description}
    </div>
  )
}

Conclusion

In this article, we explored various routing techniques for building a blog site using the framework features of React Router v7. By understanding the concepts covered here, you should now be able to design flexible routing with React Router v7 and have gained some practical skills.
Application development is all about accumulating small improvements to enhance the user experience. I hope this content serves as a helpful step forward for your own projects.

Next blog post

In the next article, we’ll use JSON Placeholder, a sample server that returns json, to demonstrate how to fetch post data on both the server side and client side.

We’ll also implement and explain how to display a spinner or similar indicator while processing when the response is slow.

https://shinagawa-web.com/en/blogs/react-router-v7-framework-blog-site-rendering-example

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form