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';