← Back to DevBytes

Server-Side Rendering with Jotai: SSR, SSG, ISR

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:

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles