banner
innei

innei

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

React Server Component と React Client Component の環境隔離と実践について探討する

前言#

私たちは、React Server Component 環境では、レンダリングの実行時が常にサーバー上であることを知っていますが、RCC では両方の環境が存在する可能性があります。ライブラリの参照元が特定の環境にのみ存在し、別の環境でエラーを報告するように制御するために、client-onlyまたはserver-onlyライブラリを使用できます。

Next.js は最初に React Server Component をサポートし、server-module-conventions rfcに従って、以下の内容は Next.js に基づいて展開されます。

この rfc では、package.jsonexportsフィールドに新たにreact-serverエクスポートが追加され、このフィールドのエクスポート位置は RSC 内の参照チェーンでのみ使用されることが示されています。

例えば、client-only/package.jsonは次のように定義されています。

{
  "exports": {
    ".": {
      "react-server": "./error.js",
      "default": "./index.js"
    }
 	}
}

react-serverは最初の行に書かれ、優先的に認識されます。したがって、RSC コンポーネント内でclient-onlyを参照すると、実際に参照されるのは./error.jsであり、この時点でエラーが発生します。

実際使用シーン#

新しい React アーキテクチャでは、RSC + RCC の 2 つの環境の組み合わせが常態化しています。したがって、ライブラリは 2 つの環境での使用方法を同時にサポートする必要があります。

ここでは次の国際化を例に挙げます。これは i18n ライブラリです。このライブラリの使用方法は RSC でも RCC でも同じです。例えば、

RCC では、次のようにuseTranslationsを使用します。

'use client'
import { useTranslations } from "next-intl";

export default function Page() {
  const t = useTranslations();
  return // ReactNode
}

RSC では、次のようにuseTranslationsを使用します。

import { useTranslations } from "next-intl";

export default async function Page({ params: { locale } }: PageParams) {
  unstable_setRequestLocale(locale);

  const t = useTranslations();
  return // ReactNode
}

あなたは気づいたかもしれませんが、これら 2 つの環境で使用する方法は同じで、メソッドのエクスポートも同じです。しかし、RSC ではフックを使用できないことに注意してくださいが、ここでは問題ありません。

ここで前述の server-module-conventions が関与しています。RSC でインポートされるuseTranslationsは実際にはフックではなく、単なる通常のメソッドです。ただし、メソッド呼び出しの一貫性を保証するために、名前も一貫性が保たれています。

next-intlpackage.jsonをさらに掘り下げると、exportsは次のように定義されています。

{
  "exports": {
    ".": {
      "types": "./dist/types/src/index.react-client.d.ts",
      "react-server": "./dist/esm/index.react-server.js",
      "default": "./dist/index.react-client.js"
    }
  }
}

そして、RSC で実際に指し示されるのはhttps://github.com/amannn/next-intl/blob/main/packages/next-intl/src/react-server/useTranslations.tsx であり、これらはすべて RSC の下での実装です。

ビジネスでの使用シーン#

ここでは、最近遭遇したシーンを挙げます。2 つのシーンでofetchインスタンスを区別する必要があります。私たちは、Next.js の RCC と RSC で Cookie を取得する方法が異なることを知っています。また、プリレンダリングページでリクエストされるインターフェースは常に RSC で発信されます。リクエストを発信する際に、認証関連のヘッダー、ユーザーエージェント、または実際のリクエスターの IP 情報など、いくつかのリクエスト情報を追加する必要があるかもしれません。

このような場合、2 つの異なるインスタンスを対象に作成できます。

まず、内部パッケージを作成します。例えばpackages/fetch

package.jsonを作成します。

{
  "name": "@shiro/fetch",
  "exports": {
    ".": {
      "react-server": "./src/fetch.server.ts",
      "default": "./src/fetch.client.ts"
    }
  },
  "devDependencies": {}
}

RSC 用のfetch.server.tsを作成します。

import 'server-only'

import { nanoid } from 'nanoid'
import { cookies, headers as nextHeaders } from 'next/headers'

export const $fetch = createFetch({
  defaults: {
    timeout: 8000,

    onRequest(context) {
      const cookie = cookies() // Next.js cookies()を使用してcookieを取得
      const token = cookie.get(TokenKey)?.value

      const headers: any = context.options.headers ?? {}
      if (token) {
        headers['Authorization'] = `bearer ${token}`
      }

      context.options.params ??= {}

      if (token) {
        context.options.params.r = nanoid()
      }

      if (context.options.params.token || token) {
        context.options.cache = 'no-store' // 認証にヒットした後はNext.jsにこのリクエストをキャッシュしないように伝えます
      }
      if (isDev) {
        console.info(`[Request/Server]: ${context.request}`)
      }

      const { get } = nextHeaders() // headers()メソッドを使用して元のリクエストヘッダーを取得

      const ua = get('user-agent')
      const ip =
        get('x-real-ip') ||
        get('x-forwarded-for') ||
        get('remote-addr') ||
        get('cf-connecting-ip')

      if (ip) {
        headers['x-real-ip'] = ip
        headers['x-forwarded-for'] = ip
      }

      headers['User-Agent'] =
        `${ua} NextJS/v${PKG.dependencies.next} ${PKG.name}/${PKG.version}`

      context.options.headers = headers
    },
    onResponse(context) {
      if (isDev) { // ここは必ずServerSide
        console.info(
          `[Response/Server]: ${context.request}`,
          context.response.status,
        )
      }
    },
  },
})

RCC 用のfetch.client.tsを作成します。

import 'client-only'
import Cookies from 'js-cookie'

function getToken(): string | null { // ここではjs-cookieを使用してブラウザ側のcookieを取得する必要があります
  const token = Cookies.get(TokenKey)

  return token || null
}


export const $fetch = createFetch({
  defaults: {
    timeout: 8000,
    onRequest(context) {
      const token = getToken()
      const headers: any = context.options.headers ?? {}
      if (token) {
        headers['Authorization'] = `bearer ${token}`
      }

      headers['x-session-uuid'] =
        globalThis?.sessionStorage?.getItem(uuidStorageKey) ?? uuid

      context.options.params ??= {}
      if (context.options.params.token) {
        context.options.cache = 'no-store'
      }
      if (isDev && isServerSide) {
        // eslint-disable-next-line no-console
        console.info(`[Request]: ${context.request}`)
      }

      context.options.headers = headers
    },
    onResponse(context) {
      if (isDev && isServerSide) { // ここでもServerSideを区別する必要があります
        console.info(`[Response]: ${context.request}`, context.response.status)
      }
    },
  },
})

これで、両者には同じメソッドがあり、両方ともエクスポートされています。したがって、ビジネスで使用できます。より良い TypeScript サポートのために、"moduleResolution": "Bundler"を変更する必要があります。そうそう、package.jsonでこの依存関係をリンクするのを忘れないでください。例えば、ここで"@shiro/fetch": "link:./packages/fetch"をプロジェクトのpackage.jsondependenciesに追加します。

使用方法は次の通りです:

import { $fetch } from '@shiro/fetch' // RCCとRSCでの使用方法は同じ

上記の実装は、Shiroiに実装されており、今後Shiroにオープンソース化する予定です。

最近解雇されて、非常に気分が落ち込んでいます。八股を見たりアルゴリズムを解いたりするよりも、頭が真っ白になるよりも、これらを研究する方が意味があると感じています。🥹😮‍💨、起業家を選ぶ際は慎重に行動すべきだと言えます。

この記事はMix Spaceによって xLog に同期更新されました。元のリンクはhttps://innei.in/posts/tech/exploring-environment-isolation-and-practice-of-react-server-component-and-react-client-component

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