Jotai works seamlessly with Next.js for both the Pages Router and App Router. This guide covers integration patterns, SSR considerations, and best practices.
Quick Start
Add Provider
For SSR, wrap your app with a Provider to scope the store per request:// pages/_app.tsx (Pages Router)
import { Provider } from 'jotai'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return (
<Provider>
<Component {...pageProps} />
</Provider>
)
}
export default MyApp
Create atoms
// store/index.ts
import { atom } from 'jotai'
export const countAtom = atom(0)
export const userAtom = atom(null)
Why Use a Provider?
By default, Jotai uses an implicit global store (“provider-less” mode). In SSR scenarios, this global store persists between requests, which can cause:
- Memory leaks: State accumulates across requests
- Data leaks: User data might bleed between requests
- Stale data: Old values persist when they shouldn’t
Using a Provider creates a store scoped to each request, ensuring clean state per render.
Hydration
Jotai supports hydrating atoms with server-side data using useHydrateAtoms:
import { useHydrateAtoms } from 'jotai/utils'
import { Provider } from 'jotai'
function HydrateAtoms({ initialValues, children }) {
useHydrateAtoms(initialValues)
return children
}
export default function Page({ initialCount }) {
return (
<Provider>
<HydrateAtoms initialValues={[[countAtom, initialCount]]}>
<Counter />
</HydrateAtoms>
</Provider>
)
}
export async function getServerSideProps() {
const initialCount = await fetchCount()
return { props: { initialCount } }
}
SSR Considerations
No Promises in SSR
You cannot return promises during server-side rendering. Guard against it in atom definitions:
const postDataAtom = atom((get) => {
const id = get(postIdAtom)
// Guard against SSR
if (typeof window === 'undefined' || prefetchedData[id]) {
return prefetchedData[id] || EMPTY_DATA
}
return fetchData(id) // Returns promise only on client
})
Hydrate Server Data
Instead of fetching in atoms, hydrate with server data:
export default function PostPage({ post }) {
return (
<Provider>
<HydrateAtoms initialValues={[[postAtom, post]]}>
<Post />
</HydrateAtoms>
</Provider>
)
}
export async function getServerSideProps({ params }) {
const post = await fetchPost(params.id)
return { props: { post } }
}
App Router (Next.js 13+)
Client Components
Atoms must be used in Client Components:
'use client'
import { useAtom } from 'jotai'
import { countAtom } from '@/store'
export function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
)
}
Provider in Layout
// app/layout.tsx
import { Provider } from 'jotai'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Provider>
{children}
</Provider>
</body>
</html>
)
}
Router Synchronization
Sync Jotai with Next.js router using atomWithHash:
import { atomWithHash } from 'jotai-location'
import Router from 'next/router'
const pageAtom = atomWithHash('page', 1, {
replaceState: true,
subscribe: (callback) => {
Router.events.on('routeChangeComplete', callback)
window.addEventListener('hashchange', callback)
return () => {
Router.events.off('routeChangeComplete', callback)
window.removeEventListener('hashchange', callback)
}
},
})
In Next.js 13+ App Router, Router.events is not available. Hash handling is planned but not yet implemented. Use replaceState option for better browser back button support.
SWC Plugins
Jotai provides SWC plugins for better DX:
// next.config.js
module.exports = {
experimental: {
swcPlugins: [
['@swc-jotai/debug-label', {}],
['@swc-jotai/react-refresh', {}],
],
},
}
These plugins automatically:
- Add debug labels to atoms
- Enable React Fast Refresh for atoms
Examples
Pages Router Example
npx create-next-app --example with-jotai my-app
Example Repository
Check out the official Next.js example.
Tips
Always use a Provider in SSR applications to prevent memory leaks and data bleeding between requests.
Use useHydrateAtoms to initialize atoms with server-side data instead of fetching in atoms during SSR.
Place at least one Suspense boundary inside your Provider to avoid endless loops with async atoms.