diff --git a/README.md b/README.md
index 89a91e0..60cbb5e 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,33 @@
# jotai-ssr
-Jotai utilities for server-side rendering (SSR)
-## Motivation
-This package provides a new SSR utility designed to replace [`useHydrateAtoms`](https://jotai.org/docs/utilities/ssr), offering a safer and more flexible way to use Jotai in SSR environments.
+**jotai-ssr** is a utility library for [Jotai](https://jotai.org/) to facilitate server-side rendering (SSR). It provides helpers for:
-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.
+- Creating an **isolated store per request** (to avoid data leak between users).
+- **Hydrating** atom values from server to client (and optionally re-hydrating).
+- Handling SSR scenarios, including **React Server Components** and soft navigations in frameworks like Next.js, Remix, and Waku.
-## Feedback
-This is a new package and we would love to hear your feedback.
-Related discussion: https://github.com/pmndrs/jotai/discussions/2692
+This library extends or wraps the existing Jotai SSR utilities to provide a more seamless integration with modern SSR setups.
-## Overview
-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's `page.tsx`, `layout.tsx`, etc.
-- `SuspenseBoundary` - A boundary component that should be used at the root of async components wrapped with `React.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.
+## Table of Contents
+
+1. [Installation](#installation)
+2. [Creating a Safe Store for Each Request](#creating-a-safe-store-for-each-request)
+ 1. [`useCreateStore`](#usecreatestore)
+ 2. [`SSRProvider`](#ssrprovider)
+3. [Hydration](#hydration)
+ 1. [What is Hydration?](#what-is-hydration)
+ 2. [How to Hydrate an Atom](#how-to-hydrate-an-atom)
+ 3. [`HydrationBoundary`](#hydrationboundary)
+ 4. [Important Notes on Hydration Logic](#important-notes-on-hydration-logic)
+4. [Soft Navigation in SSR Frameworks](#soft-navigation-in-ssr-frameworks)
+5. [Re-Hydration](#re-hydration)
+
+---
## Installation
+
```bash
npm install jotai-ssr
# or
@@ -27,189 +36,288 @@ yarn add jotai-ssr
pnpm add jotai-ssr
```
-## Usage
-This is a Next.js App Router example:
+---
-### Define Rendering Boundary
-Wrap the components that will be rendered on the server during navigation with `RenderingBoundary`.
+## Creating a Safe Store for Each Request
-layout.tsx:
-```jsx
-import { RenderingBoundary } from 'jotai-ssr'
+When using Jotai in an SSR environment, **you must ensure each request has its own store**. Relying on a shared, global store (e.g. `defaultStore`) across requests can lead to data leakage between different users.
-export default function Layout({ children }) {
- return (
-
-
-
-
- {children}
-
-
-
-
- )
-}
-```
+### Using the `useCreateStore` Hook
-page.tsx:
-```jsx
-import { RenderingBoundary } from 'jotai-ssr'
+If you create a store manually, you typically do it like this in a Client Component:
-export default function Page() {
- return (
-
-
-
- )
-}
+```tsx
+'use client';
+import { createStore, Provider } from 'jotai';
+import { useState } from 'react';
+
+const Page = () => {
+ // Ensure a new store is created per request
+ const [store] = useState(() => createStore());
+ return {/* your content */};
+};
```
-> Note: `RenderingBoundary` is used in both React Server Components and React Client Components. `RenderingBoundary` itself is a React Client Component.
+**jotai-ssr** offers a convenient `useCreateStore` hook to simplify this pattern:
-`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`.
+```tsx
+'use client';
+import { Provider } from 'jotai';
+import { useCreateStore } from 'jotai-ssr';
-The store also independent for each request: the store will never be shared between requests.
+const Page = () => {
+ const store = useCreateStore();
+ return {/* your content */};
+};
+```
+
+> **Note:** `useCreateStore` internally uses `useState`, so the component that calls `useCreateStore` must be a React Client Component (i.e., have the `'use client'` directive if you're in an RSC setup).
-#### Sharing Stores between layout and page (Advanced Usage)
-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.
+### Using the `SSRProvider`
-#### Why do we need `RenderingBoundary`?
-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:
-1. Create `const sampleAtom = atom(0)`
-2. Set up a Provider in `layout.jsx`, or render without setting up a Provider anywhere
-3. Change sampleAtom to 1 in `page.jsx`
-4. Navigate to another page using `Link` component
+Alternatively, you can use the higher-level `SSRProvider` component from **jotai-ssr**. It can be used in either a React Client Component or a React Server 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.
+```tsx
+import { SSRProvider } from 'jotai-ssr';
-#### How `performanceImpactingUseUpperStore` option works
-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.
+const Page = () => {
+ return {/* your content */};
+};
+```
-### Using SuspenseBoundary
-**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`.
+Internally, `SSRProvider` will create an isolated store for each request. You can also supply your own store:
-```jsx
-import { Suspense } from 'react'
-import { SuspenseBoundary } from 'jotai-ssr'
+```tsx
+'use client';
+import { SSRProvider, useCreateStore } from 'jotai-ssr';
-function Component() {
+const Page = () => {
+ const store = useCreateStore();
return (
- Loading...}>
-
-
-
-
- )
-}
+
+ {/* your content */}
+
+ );
+};
```
-> Note: `SuspenseBoundary` itself is React Server Component.
-#### Why do we need `SuspenseBoundary`?
-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.
+---
-### Hydrating Atoms
-Use `HydrationBoundary` to hydrate atoms at specific points in your component tree. `hydrateAtoms` props is an array of atom and values to hydrate.
+## Hydration
-With React Server Components:
-```jsx
-import { HydrationBoundary } from 'jotai-ssr'
-import { dataAtom } from './atoms'
+When data is fetched on the server and passed to the client, you may want to initialize Jotai atoms with those server-side values. This process is called **hydration**.
-async function Component() {
- const data = await fetch('https://api.example.com/data').then((res) => res.json())
- return (
-
-
-
- )
+### What is Hydration?
+
+Hydration sets up atoms with initial values so that:
+
+- The server-rendered HTML uses the correct initial state.
+- Once the client side finishes React hydration, the atom is already in the correct state without causing extra re-renders.
+
+### How to Hydrate an Atom
+
+When dealing with SSR, you often have data fetched on the server that needs to be passed to your components and stored in Jotai atoms. To accomplish this, **jotai-ssr** provides a `useHydrateAtoms` hook similar to the one in Jotai’s [`jotai/utils`](https://jotai.org/docs/utilities/ssr#usage), with a few small differences:
+
+1. **No** `dangerouslyForceHydrate` **option**
+2. Exported from **`jotai-ssr`** rather than `jotai/utils`
+
+Apart from these differences, the usage is nearly the same as the official Jotai version. This means you can hydrate your atoms with data like so:
+
+```tsx
+'use client'; // If you're using React Server Components (RSC), ensure the file is a Client Component
+
+import { atom, useAtom } from 'jotai';
+import { useHydrateAtoms } from 'jotai-ssr';
+
+// Example atom
+export const countAtom = atom(0);
+
+interface PageProps {
+ countFromServer: number;
}
-```
-> Note: if you use `HydrationBoundary` in React Server Components, the file that defines a hydrated atom must include the `'use client'` directive, like this:
-> ```jsx
-> 'use client'
->
-> import { atom } from 'jotai';
->
-> export const someAtom = atom(0);
-> ```
-> Detailed explanation is [here](https://github.com/pmndrs/jotai/discussions/2692#discussioncomment-10316130).
-
-With React Client Components:
-```jsx
-'use client'
-
-import { HydrationBoundary } from 'jotai-ssr'
-import { idAtom } from './atoms'
-
-function Component({ id }) {
- return (
-
-
-
- )
+
+export function Page({ countFromServer }: PageProps) {
+ // 1. Hydrate the atom with a value fetched on the server
+ useHydrateAtoms([[countAtom, countFromServer]]);
+
+ // 2. Now you can safely use the atom in your component
+ const [count] = useAtom(countAtom);
+
+ return Count: {count}
;
}
```
-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.
+Here’s what you need to know:
+
+1. **Client-Side Usage**:
+ Despite the term “SSR” in its name, `useHydrateAtoms` must be called in client code (i.e., a component with `'use client'` at the top if you’re using React Server Components).
-You can use multiple `HydrationBoundary` components.
-```jsx
-import { HydrationBoundary } from 'jotai-ssr'
-import { dataAAtom, dataBAtom } from './atoms'
+2. **Optional `store` Parameter**:
+ Just like the Jotai version, you can target a specific store by providing the `store` option. For example:
+ ```tsx
+ import { createStore } from 'jotai';
-async function HydrationDataABoundary({ children }) {
- const dataA = await fetch('https://api.example.com/dataA').then((res) => res.json())
+ const myStore = createStore();
+ useHydrateAtoms([[countAtom, 42]], { store: myStore });
+ ```
+
+3. **No `dangerouslyForceHydrate`**:
+ Unlike the original Jotai hook, the **jotai-ssr** version does **not** provide a `dangerouslyForceHydrate` option. If you need more advanced re-hydration behavior, see [Re-Hydration](#re-hydration).
+
+> **Tip:** Hydrating an atom **does not** cause additional re-renders if you do it **before** using the atom in your component. Make sure to call `useHydrateAtoms` at the top level of your component (or inside its parent) so that the initial render already has the right atom values.
+
+### `HydrationBoundary`
+
+For React Server Components (RSC) and for a clearer boundary-based approach, **jotai-ssr** provides a `HydrationBoundary` component:
+
+```tsx
+import { HydrationBoundary } from 'jotai-ssr';
+import { countAtom } from './atoms'; // "use client" inside this file
+
+const ServerComponent = async () => {
+ const countFromServer = await fetchCount();
return (
-
- {children}
+
+ {/* Components that consume countAtom */}
- )
-}
+ );
+};
+```
+
+You can pass an optional `options` prop, such as `{ store: myStore }`, if you want to hydrate into a specific store.
+
+> **Note**: `HydrationBoundary` can be used in both Client and Server Components. However, when using it in a Server Component, the atom definitions must be marked with `'use client'`. Also, any value you pass for hydration **must be serializable**. This is because `HydrationBoundary` itself is a React Client Component.
+
+---
-async function HydrationDataBBoundary({ children }) {
- const dataB = await fetch('https://api.example.com/dataB').then((res) => res.json())
+## Important Notes on Hydration Logic
+
+### 1. Hydrate Before Using the Atom
+
+Hydration sets atom **initial values**. Thus, you should not use the atom in a component **before** calling `useHydrateAtoms`. Instead, do something like this:
+
+**Correct usage**:
+```tsx
+const Component = ({ countFromServer }) => {
+ useHydrateAtoms([[countAtom, countFromServer]]);
+ const [count] = useAtom(countAtom);
+ return {count}
;
+};
+```
+or
+```tsx
+const Component = async ({ countFromServer }) => {
return (
-
- {children}
+
+
- )
-}
+ );
+};
+```
-function Component() {
+**Incorrect usage** (atom used before hydration):
+```tsx
+// Don't do this
+const Component = ({ countFromServer }) => {
+ const [count] = useAtom(countAtom);
+ useHydrateAtoms([[countAtom, countFromServer]]);
+ return {count}
;
+};
+```
+or
+```tsx
+// Don't do this
+const Component = async ({ countFromServer }) => {
+ const [count] = useAtom(countAtom);
return (
-
-
-
-
-
-
-
- )
-}
+
+ {count}
+
+ );
+};
```
-#### Checking Hydration Status
-You can use the `isHydratingAtom` to check if atom hydration is in progress:
-
-```js
-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
- }
- }
- )
-}
-```
+### 2. Do Not Hydrate the Same Atom in Multiple Places Within the Same Provider
+
+A single Jotai `Provider` shares atom states across its entire tree. Hydrating the same atom in multiple child components can lead to unexpected re-renders. Instead, hydrate each atom once. If you really need separate hydration for the same atom, place them in **different** providers or use [jotai-scope](https://jotai.org/docs/extensions/scope#jotai-scope) to scope them.
+
+### 3. Hydration Only Occurs on Initial Mount
+
+Hydration sets the atom value **only** on the first render (just like a `useState` initial value in React). Subsequent props changes do **not** cause re-hydration. If a component unmounts and remounts, it will re-hydrate at that time.
+
+---
+
+## Soft Navigation in SSR Frameworks
+
+In frameworks like **Next.js**, **Remix**, and **Waku**, soft navigation means some part of your layout or component tree does **not** unmount between page transitions. For instance:
+
+- **Next.js App Router**: `layout.jsx` might persist across routes.
+- **Remix**: `root.jsx` can persist across routes.
+- **Waku**: `layout.jsx` can persist across pages.
+
+> **Note**: A similar example is when the path includes a slug, and navigation occurs between pages with different slugs. In such cases, particularly in Remix and Waku, the page component itself tends to persist.
+
+When using soft navigation:
+
+- If your Jotai `Provider` is in a **layout** that persists, its store does **not** get recreated on page transitions. Data from previous pages is carried over.
+- If your Jotai `Provider` is placed in a **page** component, it will be recreated on each navigation, effectively isolating state per page.
+
+Therefore, be mindful where you place the `Provider` or the hydration logic. If a persistent layout hydrates the same atom across different routes, you could trigger unwanted re-renders on route changes. Generally, **avoid hydrating atoms in a page** if the `Provider` is in a layout that persists.
+
+---
+
+## Re-Hydration
+
+By default, **hydration happens only once**: on the initial mount. Even if you pass new values to `useHydrateAtoms` or `HydrationBoundary` after that, the atom values remain as they were set the first time.
+
+However, if you need to **re-hydrate** (e.g., to sync with the latest server data after a route refresh), you can enable re-hydration:
+
+- With `useHydrateAtoms`:
+
+ ```tsx
+ import { useHydrateAtoms } from 'jotai-ssr';
+
+ const Component = ({ countFromServer }) => {
+ useHydrateAtoms([[countAtom, countFromServer]], { enableReHydrate: true });
+ const [count] = useAtom(countAtom);
+ return {count}
;
+ };
+ ```
+
+- With `HydrationBoundary`:
+
+ ```tsx
+ const ServerComponent = async () => {
+ const countFromServer = await fetchCount();
+ return (
+
+
+
+ );
+ };
+ ```
+
+When `enableReHydrate` is `true`, the component compares the new hydration values (via `Object.is`) and re-hydrates if they differ.
+
+### Route Refresh Considerations
+
+In Next.js App Router, calling `router.refresh()` or `revalidatePath()` triggers server code to re-fetch data, but the same client component instance persists. Normally, `useState` or Jotai hydration wouldn’t reset values. By turning on re-hydration, you can ensure your atoms get updated with the newest fetched data.
+
+The same principle applies in Remix or Waku if the layout is partially reused during a slug-based soft navigation.
+
+---
+
+## License
+
+MIT License. See [LICENSE](./LICENSE) for details.
+
+---
+
+## Feedback
+This is a new package and we would love to hear your feedback.
+Related discussion: https://github.com/pmndrs/jotai/discussions/2692
+
+---