Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
Hydration Strategies
Hydration is the process of attaching React event handlers to server-rendered HTML, making it interactive. React Router provides several strategies to optimize this process.
Basic Hydration
By default, React Router hydrates immediately with server data:
// Client entry
import { HydratedRouter } from "react-router/dom";
ReactDOM.hydrateRoot(
document.getElementById("root"),
<HydratedRouter />
);
This uses the hydration data embedded in the HTML:
<!-- Server-rendered -->
<script>
window.__staticRouterHydrationData = {
loaderData: { root: {...}, about: {...} },
errors: null
};
</script>
Client Loaders
Run additional logic on the client during hydration:
export async function clientLoader({ serverLoader }) {
// Get server data
const serverData = await serverLoader();
// Augment with client-only data
const clientData = localStorage.getItem("preferences");
return {
...serverData,
preferences: JSON.parse(clientData),
};
}
export async function loader() {
return { user: await getUser() };
}
export function Component() {
const data = useLoaderData();
// data includes both server and client data
return <div>Welcome {data.user.name}</div>;
}
Hydrate Flag
Control whether client loaders run on hydration:
export async function clientLoader({ serverLoader }) {
return await serverLoader();
}
// Don't run on hydration - use server data only
clientLoader.hydrate = false;
export async function clientLoader() {
return await getClientOnlyData();
}
// Always run on hydration
clientLoader.hydrate = true;
Default Behavior
hydrate = true: When there’s no server loader
hydrate = false: When calling serverLoader()
hydrate = true: When not calling serverLoader()
HydrateFallback
Show a loading state during client loader execution:
export function HydrateFallback() {
return <div>Loading preferences...</div>;
}
export async function clientLoader() {
// This runs on hydration
const data = await fetch("/api/user").then(r => r.json());
return data;
}
clientLoader.hydrate = true;
export function Component() {
const data = useLoaderData();
return <div>Welcome {data.name}</div>;
}
HydrateFallback Behavior
- Only runs on initial hydration: Not on client-side navigations
- Requires clientLoader: With
hydrate = true
- Bubbles up: Renders parent’s
HydrateFallback if none provided
- No Outlet: Cannot render children (they may not have data yet)
Partial Hydration
Hydrate different parts of your app at different times:
// Root route - hydrates immediately
export function Component() {
return (
<div>
<header>Instant header</header>
<Outlet />
</div>
);
}
// Dashboard route - deferred hydration
export function HydrateFallback() {
return <div>Loading dashboard...</div>;
}
export async function clientLoader() {
// Heavy client-side initialization
await loadAnalytics();
await loadCharts();
return { ready: true };
}
clientLoader.hydrate = true;
export function Component() {
return <div>Dashboard ready!</div>;
}
Streaming Hydration
Combine with deferred data for progressive hydration:
import { defer } from "react-router";
export async function loader() {
return defer({
critical: await getCriticalData(),
deferred: getDeferredData(), // Promise
});
}
export function Component() {
const data = useLoaderData();
return (
<div>
<h1>{data.critical.title}</h1>
<Suspense fallback={<Spinner />}>
<Await resolve={data.deferred}>
{(deferred) => <Content data={deferred} />}
</Await>
</Suspense>
</div>
);
}
The page hydrates with critical data, then streams in deferred data.
Optimistic Hydration
Assume server data is available and hydrate eagerly:
// Server
export async function loader() {
return { count: await getCount() };
}
// Client
export async function clientLoader({ serverLoader }) {
// Optimistically return server data
const data = await serverLoader();
// Then update from API in the background
fetch("/api/count")
.then(r => r.json())
.then(fresh => {
// Update state with fresh data
});
return data;
}
clientLoader.hydrate = false; // Use server data immediately
Selective Hydration
Only hydrate interactive components:
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
// Hydrate interactive parts only
const root = document.getElementById("root");
const interactive = root.querySelector("[data-interactive]");
if (interactive) {
hydrateRoot(interactive, <HydratedRouter />);
} else {
// Just static content, no hydration needed
}
Island Architecture
Hydrate isolated interactive regions:
// Static layout, no hydration
export function Layout() {
return (
<div>
<nav>Static navigation</nav>
<Outlet />
</div>
);
}
// Interactive island
export function Component() {
const [count, setCount] = useState(0);
return (
<div data-island>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
</div>
);
}
Custom Hydration
Full control over the hydration process:
import { matchRoutes } from "react-router";
// Find routes that need hydration
const lazyMatches = matchRoutes(
routes,
window.location
)?.filter(m => m.route.lazy);
// Preload lazy routes
if (lazyMatches?.length) {
await Promise.all(
lazyMatches.map(async (m) => {
const module = await m.route.lazy();
Object.assign(m.route, { ...module, lazy: undefined });
})
);
}
// Create router with hydration data
const router = createBrowserRouter(routes, {
hydrationData: window.__staticRouterHydrationData,
});
// Hydrate
ReactDOM.hydrateRoot(
document.getElementById("root"),
<RouterProvider router={router} />
);
Hydration Errors
Debug mismatches between server and client:
if (import.meta.env.DEV) {
const root = document.getElementById("root");
const originalError = console.error;
console.error = (...args) => {
if (args[0]?.includes?.("Hydration")) {
console.warn("Hydration mismatch:", args);
// Log server HTML
console.log("Server HTML:", root.innerHTML);
}
originalError(...args);
};
}
Track hydration performance:
export async function clientLoader({ serverLoader }) {
const start = performance.now();
const data = await serverLoader();
const duration = performance.now() - start;
// Send to analytics
analytics.timing("hydration", duration);
return data;
}
Best Practices
- Use server data by default: Set
hydrate = false when possible
- Show loading states: Use
HydrateFallback for better UX
- Minimize client loaders: Keep hydration fast
- Test without JavaScript: Ensure server-rendered content works
- Monitor hydration time: Track performance metrics
Common Patterns
User Preferences
export async function clientLoader({ serverLoader }) {
const [serverData, theme] = await Promise.all([
serverLoader(),
getLocalTheme(),
]);
return { ...serverData, theme };
}
Authentication State
export async function clientLoader({ serverLoader }) {
const serverData = await serverLoader();
const token = localStorage.getItem("token");
return {
...serverData,
isAuthenticated: !!token,
};
}
Feature Flags
export async function clientLoader({ serverLoader }) {
const [serverData, flags] = await Promise.all([
serverLoader(),
getFeatureFlags(),
]);
return { ...serverData, features: flags };
}