Jotai utilities for server-side rendering (SSR)
This package provides a new SSR utility designed to replace useHydrateAtoms
, offering a safer and more flexible way to use Jotai in SSR environments.
This package provides comprehensive support for React Server Components, particularly with the Next.js App Router, addressing several limitations and issues that arise when using Jotai in server-side rendering contexts.
This is a new package and we would love to hear your feedback. Related discussion: pmndrs/jotai#2692
This package provides 3 boundary components and 1 atom for server-side rendering with Jotai:
RenderingBoundary
- A boundary component that should be used at the root of components that will be rendered on the server during navigation, such as Next.js'spage.tsx
,layout.tsx
, etc.SuspenseBoundary
- A boundary component that should be used at the root of async components wrapped withReact.Suspense
, but only when the component subtree references atoms that have the potential to change while suspended.HydrationBoundary
- A boundary component for hydrating atoms (synchronizing the server-rendered HTML with the client state) on the client side.isHydratingAtom
- An atom that can be used to check if atom hydration is in progress.
npm install jotai-ssr
# or
yarn add jotai-ssr
# or
pnpm add jotai-ssr
This is a Next.js App Router example:
Wrap the components that will be rendered on the server during navigation with RenderingBoundary
.
layout.tsx:
import { RenderingBoundary } from 'jotai-ssr'
export default function Layout({ children }) {
return (
<html>
<body>
<RenderingBoundary>
<LayoutComponent>
{children}
</LayoutComponent>
</RenderingBoundary>
</body>
</html>
)
}
page.tsx:
import { RenderingBoundary } from 'jotai-ssr'
export default function Page() {
return (
<RenderingBoundary>
<Component />
</RenderingBoundary>
)
}
Note:
RenderingBoundary
is used in both React Server Components and React Client Components.RenderingBoundary
itself is a React Client Component.
RenderingBoundary
creates a new store for subtree that is isolated from the parent store. For example, above LayoutComponent
and Component
will have their own store. In other words, the store is not shared between layout.tsx
and page.tsx
.
The store also independent for each request: the store will never be shared between requests.
If you need to share stores between layout.tsx
and page.tsx
, you can use the performanceImpactingUseUpperStore
option in RenderingBoundary
, which will cause an additional re-render after initial hydration.
In Next.js, when navigating pages using the Link
component, layout.jsx
is not re-rendered, but page.jsx
is. Because of this structure, if page.jsx
references a Store that is in layout.jsx
or is global, there's a possibility of errors occurring during page transitions due to mismatches between server-side rendered HTML and hydration.
Think following case:
- Create
const sampleAtom = atom(0)
- Set up a Provider in
layout.jsx
, or render without setting up a Provider anywhere - Change sampleAtom to 1 in
page.jsx
- Navigate to another page using
Link
component
In this case, the value of sampleAtom on server-side rendered HTML will be 0, but the value of sampleAtom on the client side will be 1. This will cause a mismatch between server-side rendered HTML and hydration, resulting in an error. To prevent this, use RenderingBoundary
to create a new store for each page.
Even if performanceImpactingUseUpperStore
is set to true
, RenderingBoundary
will still create a new store for each request. However, after hydration, it will re-render the subtree once to use the store of the parent component. Because of this re-render, it may impact performance.
Only when the async component wrapped by React.Suspense
subtree references atoms that have the potential to change while suspended, use SuspenseBoundary
. If there are no atoms that have the potential to change while suspended, you don't need to use SuspenseBoundary
.
import { Suspense } from 'react'
import { SuspenseBoundary } from 'jotai-ssr'
function Component() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SuspenseBoundary>
<AsyncComponent />
</SuspenseBoundary>
</Suspense>
)
}
Note:
SuspenseBoundary
itself is React Server Component.
When using React.Suspense
, if the component subtree references atoms that have the potential to change while suspended, there is a possibility of errors occurring during page transitions due to mismatches between server-side rendered HTML and hydration. To prevent this, use SuspenseBoundary
to create a new store for the suspended subtree.
Use HydrationBoundary
to hydrate atoms at specific points in your component tree. hydrateAtoms
props is an array of atom and values to hydrate.
With React Server Components:
import { HydrationBoundary } from 'jotai-ssr'
import { dataAtom } from './atoms'
async function Component() {
const data = await fetch('https://api.example.com/data').then((res) => res.json())
return (
<HydrationBoundary hydrateAtoms={[[dataAtom, data]]}>
<ChildComponent />
</HydrationBoundary>
)
}
Note: if you use
HydrationBoundary
in React Server Components, the file that defines a hydrated atom must include the'use client'
directive, like this:'use client' import { atom } from 'jotai'; export const someAtom = atom(0);Detailed explanation is here.
With React Client Components:
'use client'
import { HydrationBoundary } from 'jotai-ssr'
import { idAtom } from './atoms'
function Component({ id }) {
return (
<HydrationBoundary hydrateAtoms={[[idAtom, id]]}>
<ChildComponent />
</HydrationBoundary>
)
}
You should use hydrated atoms within the HydrationBoundary
component. If you use hydrated atoms outside of the HydrationBoundary
component, it may cause mismatches between server-side rendered HTML and hydration.
You can use multiple HydrationBoundary
components.
import { HydrationBoundary } from 'jotai-ssr'
import { dataAAtom, dataBAtom } from './atoms'
async function HydrationDataABoundary({ children }) {
const dataA = await fetch('https://api.example.com/dataA').then((res) => res.json())
return (
<HydrationBoundary hydrateAtoms={[[dataAAtom, dataA]]}>
{children}
</HydrationBoundary>
)
}
async function HydrationDataBBoundary({ children }) {
const dataB = await fetch('https://api.example.com/dataB').then((res) => res.json())
return (
<HydrationBoundary hydrateAtoms={[[dataBAtom, dataB]]}>
{children}
</HydrationBoundary>
)
}
function Component() {
return (
<div>
<HydrationDataABoundary>
<HydrationDataBBoundary>
<ChildComponent />
</HydrationDataBBoundary>
</HydrationDataABoundary>
</div>
)
}
You can use the isHydratingAtom
to check if atom hydration is in progress:
import { atom } from 'jotai'
import { isHydratingAtom } from 'jotai-ssr'
function Component() {
const samplePrimitiveAtom = atom(0)
const sampleAtom = atom(
(get) => get(samplePrimitiveAtom),
(get, set, update) => {
set(samplePrimitiveAtom, update)
if (get(isHydratingAtom)) {
// Do something when atom is hydrating
} else {
// Do something when atom is not hydrating
}
}
)
}