banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

サーバーアクション & ストリーミング可能なUI

Note

この小節は https://sdk.vercel.ai/docs/concepts/ai-rsc に触発されています。

以下のコードは Next.js 14.1.2-canary.3 で動作します。他のバージョンでは変更があるかもしれません。

この記事は、私が執筆中の ちょっと違った Next.js 小冊子に初めて掲載されました。ご支援いただければ幸いです。

LLM プロジェクトでは、常にストリーミングレンダリングの情報が見られます。

image

リクエストを見てみましょう。

image

実際には、これはストリーミングの 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 (


レイアウトがレンダリングされた時間: {Date.now ()}

{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>
  )
}
```tsx filename="app/server-action/action.tsx" 'use server'

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>
  )
}
```tsx filename="app/server-action/action.tsx" 'use server'

export const actionReturnReactNode = async () => {
return

React ノード

}

</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>
  )
}

image

ボタンをクリックすると、ページが 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 の条件でループを抜けます。

効果は以下の通りです:

image

このストリーミング 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


読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。