6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsのRSC時代に振り落とされないために基本の理解に立ち返る

Last updated at Posted at 2024-06-26

こんなこと思ったりしていませんか

  • Next.js 13からRSCが導入されて、クライアントサイドとサーバーサイドの境界が曖昧になってやりづらくなった
  • 状態管理が必要だからと何となく"use client"を多用してしまい、とりあえず動くコードを書くに留まる
  • これまでの実装方法が様変わりして仕組みがイマイチしっくりきていない

Next.js 13で導入されて、早いもので14となりすっかり定着した感のあるRSCですが、
フロント=クライアント:Next.js
バックエンド=サーバー:GoやRails
などといった固定概念のままだと上記のような所感を覚えたり、「PHPみたいになってどういう変遷なんだろう」と思ったりするかもしれません。(はい、これ私です。)
このあたりはコードの書き方のような「お作法」ではなくNext.jsの根幹となる「仕組み」の部分なので、しっかりと理解をしないといつまで経っても何となく動くコードを書いている状態から抜けださえないのではと危機感を覚えました。
そう思い立ち、記事を書くに至りました。

はじめに

自分が理解を深めるために多くの方の技術発信を参考にさせていただきました。
自分が「RSCを理解し、そこから色々な概念を体系的に理解する」過程を伝えることがメインの記事ですので、
内容は都度貼ってあるリンク先の素晴らしい説明をご覧いただけますと幸いです。

〜総論〜RSCについて必要性を理解するために

uhyoさんの一言で理解するReact Server Componentsという記事を読むことを強くお勧めします。
なぜ、「PHPみたいになっていっているんだろう」というところがよく理解できます。
ざっくり言えばサーバーサイドでHTMLを生成するところは一緒※ではあるものの、「サーバーコンポーネント」と「クライアントコンポーネント」を併用することができる「多段階計算」型のもので、いわばPHP 進化型といった感じのようです。
これまでのようにクライアントサイドで処理を行うことがパフォーマンス的に良くないことを考慮し、フロントの実装はサーバーサイドで行うことを基本としつつ
パフォーマンスを可能な限り落とさずにクライアントサイドも実装ができる環境に向かっているんだ、と理解しました。

〜各論〜RSCを踏まえた仕組みの理解

総論にてRSCの必要性について理解が深まったかと思います。
おそらくこの時点でフロントエンドもサーバーサイドが基本となるようにマインドチェンジが行われたかと思います。
そうなるまでは以下に進んでも堂々巡りのような状態になると思うので、しっかりと腹落ちさせてから先に進んでもらえたらと思います。

RSCとServer Actionsとバリデーション

Server Actionsについては公式に言及はありますが、詳しい理解のためにServer Action と useFormState【Next.js】Server Actionsを現場で使うテクニックを参考にさせてもらいました。
Server Actionsとは、ブラウザ側からの関数を直接呼び出すことができるものです。
プログレッシブエンハンスメントやハイドレーション(後述)を考慮した実装のようですが、クライアントサイドとサーバサイドが意識された実装によって登場した概念だと理解しています。
現状ではform内のaction属性に直接サーバーサイドの関数を呼び出すといったユースケースがほぼほぼですが、今後どのように広がっていくのかは注視したいと思っています。
なお、formといえばReact Hook Formですが2024年6月現在はサーバーサイドでバリデーションが対応していないため、Conformというライブラリを使うことが多いようです。
Server Actions時代のformライブラリconformの記事にある通り、サーバーサイドのバリデーションをクライアント側でも使うことができます。
なお、Server Actions自体は既存の状態管理やAPIルート(エンドポイント)の代替になるような代物ではなさそうです。

Server ActionsとConformの実装例

Zodによるバリデーション用のスキーマ

schema.ts
import { z } from 'zod';

export const loginSchema = z.object({
  email: z.string().email(),
  password: z.string(),
});

呼び出されるサーバーサイドの関数

action.ts
'use server';

import { redirect } from 'next/navigation';
import { parseWithZod } from '@conform-to/zod';
import { loginSchema } from '@/app/schema';

export async function handleSubmit(prevState: unknown, formData: FormData) {
  const submission = parseWithZod(formData, {
    schema: loginSchema,
  });

  if (submission.status !== 'success') {
    return submission.reply();
  }
}
  // 以下、ログイン認証を行うエンドポイントへのデータフェッチやページのリダイレクトを行う

サーバーサイドの関数を呼び出すクライアントサイドのフォーム

page.tsx
'use client'

import { useForm } from '@conform-to/react'
import { useFormState } from 'react-dom'
import { handleSubmit } from './actions'
import { parseWithZod } from '@conform-to/zod'
import { loginSchema } from './schema' 

export default function Login() {
  const [lastResult, action] = useFormState(handleSubmit, undefined)
  const [form, fields] = useForm({
    lastResult,
    shouldValidate: 'onBlur',
    onValidate({ formData }) {
      return parseWithZod(formData, { schema: loginSchema })
    },
  })

  return (
    <form id={form.id} onSubmit={form.onSubmit} action={action} noValidate>
        <div>
          <label htmlFor={fields.email.id}>Email</label>
          <input
            type="email"
            id={fields.email.id}
            key={fields.email.key}
            name={fields.email.name}
            defaultValue={fields.email.initialValue}
            {...fields.email.props}
          />
          <div>{fields.email.errors}</div>
        </div>
        <div>
          <label htmlFor={fields.password.id}>Password</label>
          <input
            type="password"
            id={fields.password.id}
            key={fields.password.key}
            name={fields.password.name}
            defaultValue={fields.password.initialValue}
            {...fields.password.props}
          />
          <div>{fields.password.errors}</div>
        </div>
        <button type="submit">Log In</button>
        {lastResult?.error && <p>{lastResult.error}</p>}
      </form>
  )
}

RSCとプログレッシブエンハンスメント(Progressive Enhancement)

上にあったServer Actionsは既存の何かを置き換えるようなものではないですが、では何に寄与しているのかというと、プログレッシブエンハンスメント対応のようです。
プログレッシブエンハンスメントとはざっくり言うとブラウザやネットワーク環境などがいかなるものでも対応するようなことを指すようですが、実際は主にJavaScriptを用いない実装を指すようです。
ですのでformのaction内の属性のように、HTMLを用いた実装はプログレッシブエンハンスメントに対応しているといえますが、他方たくさんあるページを全てJavaScriptに置き換えるのは現実的には困難ではないかと思います。
a11y対応と同様に動作には影響しないが対応が望ましいものと認識しておきます。
現代の Progressive Enhancement について考えるを参考にさせていただきました。

RSCとSSRとハイドレーション

ハイドレーションはこれまでもあったものなので詳細は割愛しますが、サーバーサイドで作成したHTMLにクライアントサイドでJavaScriptを加えることでインタラクティブな動作を実現するものです。
クライアントサイドが想定しないHTMLをサーバーサイドが作るような実装をするとエラーが起きます。
こちらの記事は本当にわかりやすく書いてくれています。
7歳娘「パパ、ReactのHydration Errorってなんで起こるの?」
Next.jsとSSRやハイドレーションの関係ですが、まずこれまでのNext.jsではSSRによるプリレンダリングが行われ、その後クライアントサイドでJavaScriptを追加するハイドレーションが行われていました。
(SSGも同様。SSGはビルド時にDOMが生成されるという別挙動のため説明は割愛)
これにRSCが加わると、サーバーサイドでサーバーコンポーネントとクライアントコンポーネントがレンダリングをされてHTMLとクライアントコンポーネントがクライアント側に渡された後、クライアント側で再度レンダリングとハイドレーションが行われるという多段階となります。
なお、クライアントコンポーネントの定義にはコンポーネント冒頭に”use client"をつける必要があります。
参考:React Server ComponentsとApp Routerをそろそろちゃんと理解したい
ただ、これまでのようにフロントエンド=クライアントサイドという境界はなくなったため、”use client"を使ったクライアントサイドがある限りハイドレーションは発生するものの、徐々に減っていくのかなーなんて思いました。

これまでのSSRやSSGとPPR

Next.jsはSSR(+SSG)によってプリレンダリングが行われ、RSCによって多段階のレンダリング構成となったと書きましたが、現在は「パーシャルプリレンダリング(Partial Pre-Rendering)」とレンダリングモデルが登場しています。
現在に至る経緯は以下の記事を参考にしました。
PPR - pre-rendering新時代の到来とSSR/SSG論争の終焉
今はSSRとは呼ばず「Static Rendering」や「Dynamic Rendering」と読んでいるそうです。公式
これまでのSSGに相当するStatic Renderingを基本としながらユーザー操作などによって動的に内容が変わる部分をこれまでのSSRに相当するDynamic Renderingを使うというようです。
これまでトレードオフだったSSGとSSRを補完するような機能で、データフェッチを含めてより一層コンポーネント単位でカプセル化していく一環と感じましす。
まずRSCを押さえようというところから大分発展した内容でキャッチアップが大変な部分ではありますが
これからますます主流になっていく設計思想かと思いますので、今後もしっかりと意識していきたいところです。

SSRの捕捉
SSRの中でもSuspenseタグでレンダリングをチャンク的に扱うことを「Streaming SSR」と言います。
これによってローディングが終わった部分から順次画面表示をさせることができます。

まとめ

簡潔におさらいします。

  1. React Server Components (RSC)の重要性の理解

    • RSCはNext.js 13から採用され、フロントエンド開発がサーバーサイド中心にシフト(PHPに戻る)しています
    • クライアントサイドのJavaScript量を減らすことで、パフォーマンスの向上を図っています
    • その一方で、これまでの実装を排除せず開発の柔軟性を両立させるために「多段階計算」モデルを採用しています
  2. Server Actionsの役割

    • フォーム処理などのサーバーサイド機能をクライアントから直接呼び出すことができます
    • プログレッシブエンハンスメントを実現することができます
    • Conformのようなライブラリを使うことで、サーバーサイドとクライアントサイドで一貫したバリデーションが可能になります
  3. プログレッシブエンハンスメントとの兼ね合い

    • Server ActionsによってJavaScriptに依存しない基本機能の実装が可能となるが、全部が置き換わるわけではないため部分実現という感じ。a11yとともに対応するのが望ましいという認識としています
  4. SSRとハイドレーション

    • RSCにおけるハイドレーションは、他段階レンダリングのクラインアントコンポーネント側で発生する
    • フロント開発の環境がサーバーサイドによることでハイドレーションが行われる部分も徐々に減っていくのではないでしょうか
  5. パーシャルプリレンダリング(PPR)の登場

    • 静的レンダリングと動的レンダリングを組み合わせた新しいアプローチです
    • SSGとSSRのトレードオフを解消し、より柔軟なレンダリング戦略を可能にします
    • これからより取り入れられるであろう技術です
  6. 継続的な学習の重要性

    • フロントエンドはちょっとの間に驚くほど進化しすることを身をもって体験しました
    • キャッチアップは歯を磨くくらい習慣化しないと取り残されてしまいます

記事をまとめるより参考サイトの記事を読んで理解したり手を動かしたりすることに相当の時間がかかりました。
そのおかげでRSC時代の土俵くらいにはやっと入れた気がします。
もし内容に誤りがありましたら、ご指導いただけますと幸いです。
最後に、素晴らしい技術記事を惜しみなく提供してくださったエンジニアの皆様に改めて感謝申し上げます。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?