Skip to content

Commit

Permalink
feat: add doc
Browse files Browse the repository at this point in the history
  • Loading branch information
akkadaska committed Aug 12, 2024
1 parent 126d188 commit 7b82ea2
Showing 1 changed file with 205 additions and 0 deletions.
205 changes: 205 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,206 @@
# 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.

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.

## Feedback
This is a new package and we would love to hear your feedback.
Related discussion: https://github.com/pmndrs/jotai/discussions/2692

## 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.

## Installation
```bash
npm install jotai-ssr
# or
yarn add jotai-ssr
# or
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`.

layout.tsx:
```jsx
import { RenderingBoundary } from 'jotai-ssr'

export default function Layout({ children }) {
return (
<html>
<body>
<RenderingBoundary>
<LayoutComponent>
{children}
</LayoutComponent>
</RenderingBoundary>
</body>
</html>
)
}
```

page.tsx:
```jsx
import { RenderingBoundary } from 'jotai-ssr'

export default function Page() {
return (
<RenderingBoundary>
<Component />
</RenderingBoundary>
)
}
```

> Note: `RenderingBoundary` is used in both React Server Components and React Client Components. `RenderingBoundary` itself is a React Client Component.
`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`.

The store also independent for each request: the store will never be shared between requests.

#### 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.

#### 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

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.

#### 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.

### 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`.

```jsx
import { Suspense } from 'react'
import { SuspenseBoundary } from 'jotai-ssr'

function Component() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SuspenseBoundary>
<AsyncComponent />
</SuspenseBoundary>
</Suspense>
)
}
```
> 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.

With React Server Components:
```jsx
import { HydrationBoundary } from 'jotai-ssr'
import { dataAtom } from './atoms'

async function Component() {
const data = await fetch('https://api.example.com/data').then((res) => res.json())
return (
<HydrationBoundary hydrateAtoms={[[dataAtom, data]]}>
<ChildComponent />
</HydrationBoundary>
)
}
```

With React Client Components:
```jsx
'use client'

import { HydrationBoundary } from 'jotai-ssr'
import { idAtom } from './atoms'

function Component({ id }) {
return (
<HydrationBoundary hydrateAtoms={[[idAtom, id]]}>
<ChildComponent />
</HydrationBoundary>
)
}
```

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.

You can use multiple `HydrationBoundary` components.
```jsx
import { HydrationBoundary } from 'jotai-ssr'
import { dataA, dataB } from './atoms'

async function HydrationDataABoundary() {
const dataA = await fetch('https://api.example.com/dataA').then((res) => res.json())
return (
<HydrationBoundary hydrateAtoms={[[dataA, dataA]]}>
<ChildComponent />
</HydrationBoundary>
)
}

async function HydrationDataBBoundary() {
const dataB = await fetch('https://api.example.com/dataB').then((res) => res.json())
return (
<HydrationBoundary hydrateAtoms={[[dataB, dataB]]}>
<ChildComponent />
</HydrationBoundary>
)
}

function Component() {
return (
<div>
<HydrationDataABoundary>
<HydrationDataBBoundary>
<ChildComponent />
</HydrationDataBBoundary>
</HydrationDataABoundary>
</div>
)
}
```

#### 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
}
}
)
}
```

0 comments on commit 7b82ea2

Please sign in to comment.