Introduction to Server-Side Rendering with Jotai
Jotai is a primitive and flexible state management library for React. While it shines in client-side applications, integrating it with modern rendering strategies like Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR) in frameworks like Next.js requires a specific approach. This tutorial will guide you through the process of safely hydrating Jotai state from the server to the client.
What is Server-Side Rendering with Jotai?
Server-Side Rendering with Jotai involves computing the initial state of your Jotai atoms on the server and passing that state down to the client. This ensures that the HTML generated on the server matches the initial HTML rendered on the client, preventing hydration mismatches. Jotai provides a utility called useHydrateAtoms specifically designed to inject server-fetched data into your Jotai store before the client renders.
Why It Matters
State management libraries often rely on global singletons, which can cause cross-request data contamination when running on a Node.js server. Furthermore, if your client-side state initializes empty and fetches data after mounting, you lose the SEO and performance benefits of SSR. By properly hydrating Jotai atoms, you achieve:
- SEO Optimization: Search engines can crawl fully rendered pages with data already present.
- Performance: Users see the complete UI immediately without waiting for client-side fetching.
- Hydration Safety: You avoid React hydration errors caused by server and client DOM mismatches.
Setting Up Jotai for SSR in Next.js
To use Jotai safely in a server-rendered environment, you must create a unique store for every request and hydrate it with the data fetched on the server. We will use the Next.js App Router for this example, but the concepts apply similarly to the Pages Router.
Creating a Custom Provider
First, we need a custom provider component. This component will instantiate a new Jotai store and use useHydrateAtoms to populate it with initial values passed from the server.
'use client'
import { Provider, createStore } from 'jotai'
import { useHydrateAtoms } from 'jotai/utils'
import { ReactNode } from 'react'
type InitialValues = [any, any][]
function HydrateAtoms({ initialValues, children }: { initialValues: InitialValues, children: ReactNode }) {
useHydrateAtoms(initialValues)
return children
}
export function JotaiProvider({ initialValues, children }: { initialValues: InitialValues, children: ReactNode }) {
const store = createStore()
return (
<Provider store={store}>
<HydrateAtoms initialValues={initialValues}>
{children}
</HydrateAtoms>
</Provider>
)
}
Defining Your Atoms
Next, define the atoms that will hold your server-fetched data. These can be standard Jotai atoms.
import { atom } from 'jotai'
export interface User {
id: number
name: string
email: string
}
export const userAtom = atom<User | null>(null)
Implementing SSR, SSG, and ISR
With our provider and atoms ready, we can now implement different rendering strategies in our Next.js Server Components. The beauty of this approach is that the data fetching logic determines the rendering strategy, while the Jotai hydration mechanism remains exactly the same.
Server-Side Rendering (SSR)
For SSR, data is fetched on every request. In the Next.js App Router, this is the default behavior when you use fetch without caching options, or when you use cache: 'no-store'.
import { JotaiProvider } from './JotaiProvider'
import { userAtom } from './atoms'
import ClientProfile from './ClientProfile'
export default async function SSRPage() {
// Fetches fresh data on every request
const res = await fetch('https://api.example.com/user/1', { cache: 'no-store' })
const userData = await res.json()
return (
<JotaiProvider initialValues={[[userAtom, userData]]}>
<main>
<h1>SSR Profile</h1>
<ClientProfile />
</main>
</JotaiProvider>
)
}
Static Site Generation (SSG)
For SSG, the page is generated at build time. You can achieve this by using cache: 'force-cache' in your fetch call. The Jotai provider setup remains identical.
import { JotaiProvider } from './JotaiProvider'
import { userAtom } from './atoms'
import ClientProfile from './ClientProfile'
export default async function SSGPage() {
// Fetches data at build time
const res = await fetch('https://api.example.com/user/1', { cache: 'force-cache' })
const userData = await res.json()
return (
<JotaiProvider initialValues={[[userAtom, userData]]}>
<main>
<h1>SSG Profile</h1>
<ClientProfile />
</main>
</JotaiProvider>
)
}
Incremental Static Regeneration (ISR)
ISR allows you to create or update static pages after the site has been built. In the App Router, you configure this using the next.revalidate option in your fetch call.
import { JotaiProvider } from './JotaiProvider'
import { userAtom } from './atoms'
import ClientProfile from './ClientProfile'
export default async function ISRPage() {
// Fetches data at build time, and revalidates every 60 seconds
const res = await fetch('https://api.example.com/user/1', { next: { revalidate: 60 } })
const userData = await res.json()
return (
<JotaiProvider initialValues={[[userAtom, userData]]}>
<main>
<h1>ISR Profile</h1>
<ClientProfile />
</main>
</JotaiProvider>
)
}
Best Practices
- Never use a global store on the server: Always instantiate a new store using
createStore()inside your Provider component. Sharing a single store across requests will leak state between different users. - Keep hydration at the top level: Call
useHydrateAtomsas close to the Provider as possible. This ensures that all child components have access to the hydrated state immediately during their initial render. - Handle async atoms carefully: If you are using Jotai's async atoms (atoms that return a Promise), ensure the Promise is resolved on the server before passing the resolved value to
initialValues. Passing unresolved Promises touseHydrateAtomscan lead to unexpected hydration behavior. - Use 'use client' appropriately: Your Jotai Provider and any components that read/write to atoms must be Client Components. However, the data fetching and the rendering of the Provider itself can happen in a Server Component.
Conclusion
Integrating Jotai with Server-Side Rendering, Static Site Generation, and Incremental Static Regeneration is straightforward once you understand the importance of store isolation and atom hydration. By leveraging createStore and useHydrateAtoms, you can seamlessly pass server-fetched data into your client-side state management flow. This approach not only preserves the performance and SEO benefits of modern rendering strategies but also maintains the ergonomic, atomic state management experience that makes Jotai so powerful.