Note
この小節は https://sdk.vercel.ai/docs/concepts/ai-rsc に触発されています。
以下のコードは Next.js 14.1.2-canary.3 で動作します。他のバージョンでは変更があるかもしれません。
この記事は、私が執筆中の ちょっと違った Next.js 小冊子に初めて掲載されました。ご支援いただければ幸いです。
LLM プロジェクトでは、常にストリーミングレンダリングの情報が見られます。
リクエストを見てみましょう。
実際には、これはストリーミングの RSC ペイロードです。つまり、UI の更新はサーバーのストリーミング RSC ペイロードによって駆動されています。ストリーミングの RSC ペイロードが次の行を読み取ると、UI が更新されます。
このセクションでは、RSC を利用してストリーミングレンダリングメッセージフローを簡単に実装します。
サーバーアクション#
始める前に、サーバーアクション は実際には POST リクエストであり、サーバーはサーバーアクション関数の参照を呼び出し、HTTP リクエストの方法でストリーミング結果を返すことを知っておく必要があります。
サーバーアクションでは、非同期メソッドを定義する必要があります。リクエストは非同期であるためです。次に、シリアライズ可能なデータを返す必要があります。たとえば、関数のようなものは返せません。
私たちは、サーバーアクションを使用してページのデータを更新することがよくあります。たとえば、revalidatePath
を使用します。
試してみましょう。
```tsx filename="app/server-action/layout.tsx" import type { PropsWithChildren } from 'react'export default async ({ children }: PropsWithChildren) => {
return (
{children}
)
}
</Tab>
<Tab label="page.tsx">
```tsx filename="app/server-action/page.tsx"
'use client'
import { useState } from 'react'
import type { ReactNode } from 'react'
import { actionRevalidate } from './action'
export default () => {
return (
<div className="flex flex-col gap-4">
<ServerActionRevalidate />
</div>
)
}
const ServerActionRevalidate = () => {
return (
<form
action={async (e) => {
await actionRevalidate()
}}
>
<button type="submit">このページのレイアウトを再検証</button>
</form>
)
}
import { revalidatePath } from 'next/cache'
export const actionRevalidate = async () => {
revalidatePath('/server-action')
}
</Tab>
</Tabs>
![](https://nextjs-book.innei.in/images/4.gif)
ボタンをクリックすると、ページが再レンダリングされ、ページがリロードされることなく最新のサーバー時間が更新されました。
## サーバーアクションを使用してストリーミング UI を取得
少し考えてみましょう。サーバーアクションが ReactNode 型を返したらどうなるでしょう。
<Tabs>
<Tab label="page.tsx">
```tsx filename="app/server-action/page.tsx" {21}
'use client'
import { useState } from 'react'
import type { ReactNode } from 'react'
import { actionReturnReactNode } from './action'
export default () => {
return (
<div className="flex flex-col gap-4">
<ServerActionRenderReactNode />
</div>
)
}
const ServerActionRenderReactNode = () => {
const [node, setNode] = useState<ReactNode | null>(null)
return (
<form
action={async (e) => {
const node = await actionReturnReactNode()
setNode(node)
}}
>
<button type="submit">サーバーアクションから ReactNode をレンダリング</button>
</form>
)
}
export const actionReturnReactNode = async () => {
return
}
</Tab>
</Tabs>
![](https://nextjs-book.innei.in/images/5.gif)
ボタンをクリックすると、ページが React ノードをレンダリングしました。この React ノードはサーバーアクションから返されたものです。
私たちは、App Router でサーバーコンポーネントを使用できることを知っています。サーバーコンポーネントは、非同期をサポートする無状態コンポーネントです。非同期コンポーネントの戻り値は実際には `Promise<ReactNode>` であり、`ReactNode` はシリアライズ可能なオブジェクトです。
では、Suspense + 非同期コンポーネントを利用すると、どのような結果になるでしょうか。
<Tabs>
<Tab label="action.tsx">
```tsx filename="app/server-action/action.tsx" {3,7}
export const actionReturnReactNodeSuspense = async () => {
const Row = async () => {
await sleep(300)
return <div>React ノード</div>
}
return (
<Suspense fallback={<div>読み込み中</div>}>
<Row />
</Suspense>
)
}
```
</Tab>
<Tab label="page.tsx">
```tsx filename="app/server-action/page.tsx"
'use client'
import { useState } from 'react'
import type { ReactNode } from 'react'
import { actionReturnReactNodeSuspense } from './action'
export default () => {
return (
<div className="flex flex-col gap-4">
<ServerActionRenderReactNode />
</div>
)
}
const ServerActionRenderReactNode = () => {
const [node, setNode] = useState<ReactNode | null>(null)
return (
<form
action={async (e) => {
const node = await actionReturnReactNodeSuspense()
setNode(node)
}}
>
<button type="submit">サーバーアクションから ReactNode をレンダリング</button>
</form>
)
}
ボタンをクリックすると、ページが Suspense コンポーネントをレンダリングし、読み込み中を表示しました。その後、非同期コンポーネントが読み込まれるのを待ち、React ノードを表示しました。
この特性を利用して、このメソッドを簡単に改造することができます。たとえば、タイピングエフェクトを実現できます。
export const actionReturnReactNodeSuspenseStream = async () => {
const createStreamableRow = () => {
const { promise, reject, resolve } = createResolvablePromise()
const Row = (async ({ next }: { next: Promise<any> }) => {
const promise = await next
if (promise.done) {
return promise.value
}
return (
<Suspense fallback={promise.value}>
<Row next={promise.next} />
</Suspense>
)
}) /* 私たちの React タイピングは非同期コンポーネントをサポートしていません */ as unknown as React.FC<{
next: Promise<any>
}>
return {
row: <Row next={promise} />,
reject,
resolve,
}
}
let { reject, resolve, row } = createStreamableRow()
const update = (nextReactNode: ReactNode) => {
const resolvable = createResolvablePromise()
resolve({ value: nextReactNode, done: false, next: resolvable.promise })
resolve = resolvable.resolve
reject = resolvable.reject
}
const done = (finalNode: ReactNode) => {
resolve({ value: finalNode, done: true, next: Promise.resolve() })
}
;(async () => {
for (let i = 0; i < typewriterText.length; i++) {
await sleep(10)
update(<div>{typewriterText.slice(0, i)}</div>)
}
done(
<div>
{typewriterText}
<p>タイピング完了。</p>
</div>,
)
})()
return <Suspense fallback={<div>読み込み中</div>}>{row}</Suspense>
}
上記のコードでは、createStreamableRow
が Suspense によってラップされた Row コンポーネントを作成し、ネストされた Promise を利用しています。現在の promise の value が done でない限り、内部の Suspense は常に resolve されません。これにより、新しい React ノードを常に置き換えることができます。
update
では、元の resolve された promise を置き換え、新しい promise が resolve されていない場合、Suspense は前の promise の値をフォールバックします。これを繰り返し、done === true
の条件でループを抜けます。
効果は以下の通りです:
このストリーミング UI を利用して、AI 関数呼び出しと組み合わせることで、サーバーサイドでさまざまな異なる UI コンポーネントを必要に応じて描画できます。
Warning
このストリーミングによってコンポーネントが更新されるため、サーバーは常に長接続を維持する必要があります。また、各更新を駆動する RSC ペイロードは前回の基盤の全量更新であるため、長文の場合、転送されるデータ量は非常に大きくなり、帯域幅の圧力が増加する可能性があります。
さらに、Vercel などのサーバーレスプラットフォームでは、長接続を維持することが大量の計算リソースを消費し、最終的に請求書が非常に制御不能になる可能性があります。
上記のすべてのコード例は、demo/steamable-ui にあります。
参考:https://sdk.vercel.ai/docs/concepts/ai-rsc#create-an-airsc-instance-on-the-server
この記事は Mix Space によって xLog に同期更新されました。
元のリンクは https://innei.in/posts/programming/nextjs-rsc-server-action-and-streamable-ui