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.
Top comments (2)
Yes, but the flickering disappear only because when a user hits the route, the loader is executed and will await for the component to load; this means that the navigation will have a bit of delay, so this is NOT a solution
In The Initial Loading It Is Flickering