Code-Splitting in React Router v6.4

11/02/2023
3 min read

Before React Router v6.4 introduced, we used to setup our routes using <BrowserRouter /> like this.

// main.jsx
 
import { BrowserRouter, Routes, Route } from "react-router-dom"
 
// code splitting using React.lazy
const Dashboard = React.lazy(() => import("./dashboard"))
 
function App() {
  return (
    <React.Suspense fallback="loading...">
      <BrowserRouter>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </BrowserRouter>
    </React.Suspense>
  )
}
 
ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

We do code splitting to avoid loading the JavaScript code of the entire app. However, by using this setup, Content Layout Shift (CLS) occurs when user changes route because of the React.lazy ("loading..." will show for a brief second, causing the page to flicker). With the introduction of Data APIs in React Router v6.4, we can avoid this flicker. But first, let's migrate to the new Data API router.

Use the new Data API router

Since the React Router v6.4 Data APIs has been released, we can now setup our routers using createBrowserRouter together with createRoutesFromElements function as follows

// main.jsx
 
import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
  RouterProvider,
} from "react-router-dom"
 
const Dashboard = React.lazy(() => import("./dashboard"))
 
const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      path="/dashboard"
      element={
        <React.Suspense fallback="loading...">
          <Dashboard />
        </React.Suspense>
      }
    />
  )
)
 
ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
)

Now we can introduce a loader function to import <Dashboard /> and avoid the CLS issue.

Use loader to eliminate Content Layout Shift

Since we are using React Router Data APIs, we can provide our loader function. We can import <Dashboard /> inside the loader and await for the import to be finished before rendering the component. By doing so, we can skip the React.Suspense behaviour and therefore remove CLS at the same time.

// dashboard-loader.jsx
 
import React from "react"
 
let Dashboard = React.lazy(() => import("./dashboard"))
 
export async function lazyDashboardLoader() {
  const componentModule = await import("./dashboard")
  // This avoid flicker from React.lazy by using the component directly
  Dashboard = componentModule.default
 
  return null
}
 
export function LazyDashboard() {
  return (
    <React.Suspense fallback="loading...">
      <Dashboard />
    </React.Suspense>
  )
}
// main.tsx
 
import {
  createBrowserRouter,
  createRoutesFromElements,
  Route,
  RouterProvider,
} from "react-router-dom"
import { LazyDashboard, lazyDashboardLoader } from "./dashboard-loader"
 
const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      path="/dashboard"
      element={<LazyDashboard />}
      loader={lazyDashboardLoader}
    />
  )
)
 
ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
)

Now, our app can be as lightweight as we want and we will not see the flicker caused by React.Suspense.

This article is inspired by React Router 6.4 Code-Splitting by Ruben Cases and Matt's code snippet but adjusted to make it suitable for a project not currently using Data Loader.