diff --git a/README.md b/README.md
index 89a91e0..1b3fb7c 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,31 @@
# 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.
+- **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:
+## Table of Contents
-- `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.
+1. [Installation](#installation)
+2. [Creating a Safe Store for Each Request](#creating-a-safe-store-for-each-request)
+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. [Streaming Hydration with Async Atoms](#streaming-hydration-with-async-atoms)
+5. [Important Notes on Hydration Logic](#important-notes-on-hydration-logic)
+6. [Soft Navigation in SSR Frameworks](#soft-navigation-in-ssr-frameworks)
+7. [Re-Hydration](#re-hydration)
+
+---
## Installation
+
```bash
npm install jotai-ssr
# or
@@ -27,189 +34,338 @@ 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}
-
-
-
-
- )
-}
-```
+To create an isolated store per request, you should use `Provider` for each layout or page that uses Jotai like so:
-page.tsx:
-```jsx
-import { RenderingBoundary } from 'jotai-ssr'
+```tsx
+import { Provider } from 'jotai';
-export default function Page() {
+const Page = () => {
return (
-
-
-
- )
-}
+ {/* Your content */}
+ );
+};
```
-> Note: `RenderingBoundary` is used in both React Server Components and React Client Components. `RenderingBoundary` itself is a React Client Component.
+If you need to pass a custom store to the `Provider`, you can do so in one of the following ways:
-`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`.
+1. **Using useState**
-The store also independent for each request: the store will never be shared between requests.
+ ```tsx
+ 'use client';
-#### 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.
+ import { createStore, Provider } from 'jotai';
+ import { useState } from 'react';
-#### 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
+ const Page = () => {
+ const [store] = useState(() => createStore());
+ return {/* your content */};
+ };
+ ```
-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.
+2. **Using useRef**
-#### 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.
+ ```tsx
+ 'use client';
-### 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`.
+ import { createStore, Provider } from 'jotai';
+ import { useRef } from 'react';
-```jsx
-import { Suspense } from 'react'
-import { SuspenseBoundary } from 'jotai-ssr'
+ const Page = () => {
+ const storeRef = useRef(undefined);
+ if (!storeRef.current) {
+ storeRef.current = createStore();
+ }
+ return {/* your content */};
+ };
+ ```
-function Component() {
- return (
- Loading...}>
-
-
-
-
- )
+Both approaches ensure a new store instance is created for each request, preventing data from leaking between different users.
+
+---
+
+## Hydration
+
+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**.
+
+### 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;
+}
+
+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}
;
}
```
-> 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.
+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).
+
+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';
+
+ 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.
-### 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.
+### `HydrationBoundary`
-With React Server Components:
-```jsx
-import { HydrationBoundary } from 'jotai-ssr'
-import { dataAtom } from './atoms'
+For React Server Components (RSC) and for a clearer boundary-based approach, **jotai-ssr** provides a `HydrationBoundary` component:
-async function Component() {
- const data = await fetch('https://api.example.com/data').then((res) => res.json())
+```tsx
+import { HydrationBoundary } from 'jotai-ssr';
+import { countAtom } from './atoms'; // "use client" inside this file
+
+const ServerComponent = async () => {
+ const countFromServer = await fetchCount();
return (
-
-
+
+ {/* Components that consume countAtom */}
- )
-}
+ );
+};
```
-> 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 (
-
-
-
- )
-}
+
+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.
+
+---
+
+## Streaming Hydration with Async Atoms
+
+Some SSR frameworks (like Next.js) support **streaming responses** so that part of your UI can render before all data is loaded. With Jotai, you can stream data from the server into **async atoms**. This allows your components to start rendering in a suspended state, then reveal when the data arrives. Here’s how you can do it with **jotai-ssr**:
+
+### Defining an Async Atom
+
+First, define an atom that holds a promise:
+
+```tsx
+'use client';
+
+import { atom, useAtomValue } from 'jotai';
+
+// Define an async atom that resolves to a number.
+export const countAtom = atom>();
+
+export const CountComponent = () => {
+ // Jotai will automatically suspend here until the promise resolves
+ const count = useAtomValue(countAtom);
+ return {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.
+> **Note**: Because this is an async atom, **any** component reading it will suspend by default, so you must wrap it with ``.
+
+### Streaming Hydration in the Server/Edge Environment
+
+Next, in your server-side or edge code, you can fetch the data and hydrate the async atom with a **promise**:
+
+```tsx
+import { HydrationBoundary } from 'jotai-ssr';
+import { Suspense } from 'react';
+import { countAtom, CountComponent } from './client';
-You can use multiple `HydrationBoundary` components.
-```jsx
-import { HydrationBoundary } from 'jotai-ssr'
-import { dataAAtom, dataBAtom } from './atoms'
+export const StreamingHydration = () => {
+ // Suppose this fetchCount function returns a Promise
+ const countPromise = fetchCount();
-async function HydrationDataABoundary({ children }) {
- const dataA = await fetch('https://api.example.com/dataA').then((res) => res.json())
return (
-
- {children}
+
+ loading...}>
+
+
- )
-}
+ );
+};
+```
+
+1. **Fetch or stream data** (e.g., `fetchCount()`) on the server.
+2. **Hydrate** the async atom by passing `[countAtom, countPromise]` to `HydrationBoundary`.
+3. **Wrap** your async-consuming components (``) with a `` boundary to handle the loading state.
+
+This setup ensures that:
+
+- The async atom suspends until the promise resolves.
+- The hydrated value is set **immediately** in the store, allowing the client to continue with the same promise.
+- React’s Suspense boundary displays a fallback (`loading...`) until the atom’s promise settles.
+
+This pattern allows you to combine the power of **async atoms** with server-side streaming, letting Jotai manage data states in a way that feels natural within React’s **Suspense** model.
+
+---
-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
+
+---
diff --git a/src/HydrationBoundary.tsx b/src/HydrationBoundary.tsx
deleted file mode 100644
index 50027a4..0000000
--- a/src/HydrationBoundary.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-'use client';
-
-import { atom, useStore, type WritableAtom } from 'jotai';
-import { type ReactNode, useRef } from 'react';
-
-type AnyWritableAtom = WritableAtom;
-
-type InferAtomTuples = {
- [K in keyof T]: T[K] extends readonly [infer A, ...unknown[]]
- ? A extends WritableAtom
- ? readonly [A, ...Args]
- : T[K]
- : never;
-};
-
-export function HydrationBoundary<
- T extends (readonly [AnyWritableAtom, ...unknown[]])[],
->({
- children,
- hydrateAtoms,
-}: {
- children?: ReactNode;
- hydrateAtoms: InferAtomTuples;
-}) {
- const isHydratedRef = useRef(false);
- const store = useStore();
-
- if (!isHydratedRef.current) {
- store.set(isHydratingPrimitiveAtom, true);
- for (const [atom, ...args] of hydrateAtoms) {
- store.set(atom, ...args);
- }
- store.set(isHydratingPrimitiveAtom, false);
- isHydratedRef.current = true;
- }
-
- return <>{children}>;
-}
-
-const isHydratingPrimitiveAtom = atom(false);
-
-export const isHydratingAtom = atom((get) => get(isHydratingPrimitiveAtom));
diff --git a/src/RenderingBoundary.tsx b/src/RenderingBoundary.tsx
deleted file mode 100644
index d644a54..0000000
--- a/src/RenderingBoundary.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-'use client';
-
-import { createStore, Provider } from 'jotai';
-import { type ReactNode, useEffect, useState } from 'react';
-
-export function RenderingBoundary({
- children,
- performanceImpactingUseUpperStore,
-}: {
- children?: ReactNode;
- performanceImpactingUseUpperStore?: boolean;
-}) {
- const [isRefUpperStore, setIsRefUpperStore] = useState(false);
- const [firstRenderStore] = useState(() => createStore());
-
- useEffect(() => {
- if (performanceImpactingUseUpperStore && !isRefUpperStore) {
- setIsRefUpperStore(true);
- }
- }, [isRefUpperStore, performanceImpactingUseUpperStore]);
-
- if (isRefUpperStore) {
- return <>{children}>;
- }
-
- return {children};
-}
diff --git a/src/SuspenseBoundary.tsx b/src/SuspenseBoundary.tsx
deleted file mode 100644
index 8e91f35..0000000
--- a/src/SuspenseBoundary.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { type ReactNode } from 'react';
-import { RenderingBoundary } from './RenderingBoundary.js';
-
-export function SuspenseBoundary({ children }: { children?: ReactNode }) {
- return (
-
- {children}
-
- );
-}
diff --git a/src/hydrate-atoms/HydrationBoundary.tsx b/src/hydrate-atoms/HydrationBoundary.tsx
new file mode 100644
index 0000000..7c4ae70
--- /dev/null
+++ b/src/hydrate-atoms/HydrationBoundary.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import type { PropsWithChildren } from 'react';
+import type {
+ AnyWritableAtom,
+ HydrateAtomOptions,
+ InferAtomTuples,
+} from './types.js';
+import { useHydrateAtoms } from './use-hydrate-atoms.js';
+
+export function HydrationBoundary<
+ T extends (readonly [AnyWritableAtom, ...unknown[]])[],
+>({
+ children,
+ hydrateAtoms,
+ options,
+}: PropsWithChildren<{
+ hydrateAtoms: InferAtomTuples;
+ options?: HydrateAtomOptions | undefined;
+}>) {
+ useHydrateAtoms(hydrateAtoms, options);
+
+ return <>{children}>;
+}
diff --git a/src/hydrate-atoms/index.ts b/src/hydrate-atoms/index.ts
new file mode 100644
index 0000000..bde88b9
--- /dev/null
+++ b/src/hydrate-atoms/index.ts
@@ -0,0 +1,2 @@
+export * from './use-hydrate-atoms.js';
+export * from './HydrationBoundary.js';
diff --git a/src/hydrate-atoms/types.ts b/src/hydrate-atoms/types.ts
new file mode 100644
index 0000000..e2ee2e6
--- /dev/null
+++ b/src/hydrate-atoms/types.ts
@@ -0,0 +1,18 @@
+import type { createStore, WritableAtom } from 'jotai';
+
+export type Store = ReturnType;
+
+export type AnyWritableAtom = WritableAtom;
+
+export type InferAtomTuples = {
+ [K in keyof T]: T[K] extends readonly [infer A, ...unknown[]]
+ ? A extends WritableAtom
+ ? readonly [A, ...Args]
+ : T[K]
+ : never;
+};
+
+export type HydrateAtomOptions = {
+ store?: Store | undefined;
+ enableReHydrate?: boolean | undefined;
+};
diff --git a/src/hydrate-atoms/use-hydrate-atoms.ts b/src/hydrate-atoms/use-hydrate-atoms.ts
new file mode 100644
index 0000000..c6cfde0
--- /dev/null
+++ b/src/hydrate-atoms/use-hydrate-atoms.ts
@@ -0,0 +1,44 @@
+import { atom, useStore } from 'jotai';
+import type {
+ AnyWritableAtom,
+ HydrateAtomOptions,
+ InferAtomTuples,
+} from './types.js';
+import { useCallback, useEffect, useRef } from 'react';
+
+export function useHydrateAtoms<
+ T extends (readonly [AnyWritableAtom, ...unknown[]])[],
+>(hydrateAtoms: InferAtomTuples, options?: HydrateAtomOptions) {
+ const isHydratedRef = useRef(false);
+ const lastHydrateAtoms = useRef(hydrateAtoms);
+ const store = useStore(
+ options?.store != null ? { store: options?.store } : undefined,
+ );
+
+ const hydrate = useCallback(() => {
+ store.set(isHydratingPrimitiveAtom, true);
+ for (const [atom, ...args] of hydrateAtoms) {
+ store.set(atom, ...args);
+ }
+ store.set(isHydratingPrimitiveAtom, false);
+ isHydratedRef.current = true;
+ }, [store, hydrateAtoms]);
+
+ if (!isHydratedRef.current) {
+ hydrate();
+ }
+
+ useEffect(() => {
+ if (!options?.enableReHydrate) {
+ return;
+ }
+ if (hydrateAtoms !== lastHydrateAtoms.current) {
+ lastHydrateAtoms.current = hydrateAtoms;
+ hydrate();
+ }
+ }, [hydrate, options?.enableReHydrate, hydrateAtoms]);
+}
+
+const isHydratingPrimitiveAtom = atom(false);
+
+export const isHydratingAtom = atom((get) => get(isHydratingPrimitiveAtom));
diff --git a/src/index.ts b/src/index.ts
index 7261e5f..40e4f9f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1 @@
-export * from './HydrationBoundary.js';
-export * from './RenderingBoundary.js';
-export * from './SuspenseBoundary.js';
+export * from './hydrate-atoms/index.js';