iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🧩

Using Client Middleware in React Router v7 SPA Mode

に公開

I refactored authentication guards using the "Client Middleware" feature introduced in React Router v7.3.0. This article explains the implementation based on a real-world example from a Firebase-authenticated SPA called "Shizukana Remix SPA Example".

TL;DR

  • unstable_clientMiddleware has been supported since React Router v7.3.0 (Release Notes)
  • Using middleware allows you to centralize authentication checks that were previously scattered across individual routes
  • You can share user information using context, eliminating the need for redundant authentication processing in route loaders
  • While it is still an unstable feature, I recommend it as it cleans up the code and improves readability

Issues with Conventional Authentication Implementation

In SPA apps using Firebase Authentication, you typically need to perform an authentication check for every protected route. For example:

export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
  const user = await requireAuth(request, { failureRedirect: href('/') })
  if (user.handle) {
    return redirect(href('/:handle', { handle: user.handle }))
  }
  return null
}

This needed to be written for every protected route, making authentication logic scattered and difficult to manage. Similar code was also required even when only retrieving user information.

Improvements Using Middleware

React Router v7.3.0 introduced unstable_clientMiddleware, which can solve these issues.

1. Setup

First, enable the middleware feature in react-router.config.ts:

// react-router.config.ts
import type { Config } from '@react-router/dev/config'

export default {
  ssr: false,
  future: {
    unstable_middleware: true,
  },
} satisfies Config

2. Creating a Context

Create a context to share authenticated user information:

// middlewares/user-context.ts
import { unstable_createContext } from 'react-router'
import type { AppUser } from '~/services/auth'

export const userContext = unstable_createContext<AppUser | null>()

3. Implementing Authentication Middleware

I created two types of middleware:

Required Authentication Middleware (for Onboarding)

// middlewares/on-boarding-auth-middleware.ts
import {
  href,
  redirect,
  type unstable_MiddlewareFunction as MiddlewareFunction,
} from 'react-router'
import { requireAuth } from '~/services/auth'
import { userContext } from './user-context'

export const onBoardingAuthMiddleware: MiddlewareFunction = async ({
  request,
  context,
}) => {
  // Onboarding procedures cannot be accessed without being logged in
  const user = await requireAuth(request, { failureRedirect: href('/') })
  context.set(userContext, user)

  // If already onboarded, redirect to the profile page
  if (user.handle) {
    throw redirect(href('/:handle', { handle: user.handle }))
  }
}

Optional Authentication Middleware (for General Pages)

// middlewares/optional-auth-middleware.ts
import type { unstable_MiddlewareFunction as MiddlewareFunction } from 'react-router'
import { isAuthenticated } from '~/services/auth'
import { userContext } from './user-context'

export const optionalAuthMiddleware: MiddlewareFunction = async ({
  request,
  context,
}) => {
  // Set user information if logged in
  const user = await isAuthenticated(request)
  context.set(userContext, user)
}

4. Applying Middleware

By setting middleware on a layout route, it is applied to all routes under that layout:

// routes/welcome+/_layout/route.ts
import { onBoardingAuthMiddleware } from '~/middlewares/on-boarding-auth-middleware'

// Set Middleware
export const unstable_clientMiddleware = [onBoardingAuthMiddleware]
// routes/$handle+/_layout/route.tsx
import { Outlet } from 'react-router'
import { AppFooter } from '~/components/AppFooter'
import { optionalAuthMiddleware } from '~/middlewares/optional-auth-middleware'

export const unstable_clientMiddleware = [optionalAuthMiddleware]

export default function UserPageLayout() {
  return (
    <>
      <Outlet />
      <AppFooter />
    </>
  )
}

5. Using in Route Loaders

After setting up the middleware, you can retrieve user information from the context in your route loaders:

export const clientLoader = async ({
  params: { handle },
  context,
}: Route.ClientLoaderArgs) => {
  const isExist = await isAccountExistsByHandle(handle)
  if (!isExist) throw data(null, { status: 404 })

  // Get user information set by the middleware
  const user = context.get(userContext)
  const posts = await listUserPosts(handle)

  return { handle, user, posts, isAuthor: handle === user?.handle }
}

Comparison Before and After Refactoring

Before: Authentication check in each route

// Required authentication route
export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
  const user = await requireAuth(request, { failureRedirect: href('/') })
  if (user.handle) {
    return redirect(href('/:handle', { handle: user.handle }))
  }
  return null
}

// Route needing only user information
export const clientLoader = async ({ request, params }: Route.ClientLoaderArgs) => {
  // ...
  const user = await isAuthenticated(request)
  // ...
  return { /* ... */ }
}

After: Middleware and Context

// Set middleware on the layout route
export const unstable_clientMiddleware = [onBoardingAuthMiddleware]

// Retrieve user information from context in the route loader
export const clientLoader = async ({ context }: Route.ClientLoaderArgs) => {
  const user = context.get(userContext)
  // ...
  return { /* ... */ }
}

Benefits

  1. Centralization of authentication logic: Authentication logic that was scattered across various routes can be consolidated in one place.
  2. Reduction of redundant code: There is no longer a need to write the same authentication check multiple times.
  3. Sharing user information: User information can be easily shared through the context.
  4. Separation of concerns: Authentication can be separated from the original functionality of the routes.

Points to Note

  • As indicated by the unstable_ prefix, this feature is not yet stable.
  • Depending on your project structure, you may need to set middleware on multiple layout routes.
  • Since the API may change in the future, care should be taken when updating.

Conclusion

By using the client middleware feature in React Router v7.3.0, I was able to centralize authentication logic and significantly improve code readability and maintainability. Although it is still an unstable feature, its significant benefits make it a feature worth actively utilizing in small to medium-sized SPA projects.

The code introduced in this article is implemented in a sample app called "Shizukana Remix SPA Example", a React Router v7 application using Firebase Authentication. If you want to see how it works in practice, please check it out.

I hope the middleware feature in React Router v7 helps improve your code quality when developing SPA applications with Firebase Authentication.

GitHubで編集を提案

Discussion