React/Next.js/Remixにみるこの頃のアーキテクチャ変化
近年は以前ほどの大きな変化がなくなった印象のあるWebフロントエンド技術ですが、実はさまざまな変化が起きています。ただ、日々のお仕事が忙しく、なかなか最新の情報を取り入れることが難しいという方も多いのではないでしょうか。
そこで本連載では、Webフロントエンド技術のうち、ReactやNext.js、Remixなどのアーキテクチャまわりの変化について、全3回で横断的に紹介します。ひとつひとつの解説は簡単なものになりますが、キャッチアップの足がかりになるようなトピックを選びました。まずは知るところから、はじめてみませんか。第1回はReact19のアップデートにみる変化を取り上げます。
はじめに:概念の変化
新しい技術が生み出される中では大小さまざまな変化が起きていますが、それら変化の中で一番やっかいなのが、概念の変化ではないでしょうか。概念は自分の中で一旦消化しないと自由に活かすことが難しい類の情報だといえます。
近年のReactで起きた変化のうち、一番大きかった概念の変化は「React hooks」だったと思います。
React 16.8で導入されたこの機能は、単にコンポーネントの書き方を変えるだけではなく、従来クラスベースで記述していたコンポーネントを関数コンポーネントとして書き換えることにより、プログラミングスタイル自体も変える必要があったという点で、なかなか移行が難しいアップデートでした。
例えば、関数プログラミングに親しんでいた層の人には受け入れられやすかったのではないかと思いますが、推測しますがjQueryの延長線上でReactを使ってきた人は結構学習が大変だったはずです。
第1回:React19のアップデートにみる変化
Reactは現在19が最新のRC版として公開されています。以下のドキュメントに新機能の概要が記載されていますので、確認してみてください。
コンテンツ
Actionsで変わるフォーム周り
React 19の機能として大きく取り上げられているのは、「Actions」と「React Server Component(RSC)」です。RSCはレンダリングのアーキテクチャに大きな変化を与えるので次回解説します。
今回は、大きな機能変更といえるActions*からフォーム周りの変化について見ていきましょう。
Actionsは記事上では『非同期トランジションを使用する関数を規約として「アクション」と呼ぶ』とされています。しかしこれだけでは、よくわからないですよね。
form要素の指定の形式といくつかのhooksが追加されているのが今回の機能追加ではありますが、ぱっと見でわかりやすい点でいえば、form要素のaction属性に関数を指定することにより非同期な遷移をシンプルなコードで実現できるという部分があります。
通常、Reactを使わないWebアプリでも以下のような書き方をしていると思いますが、
<form action=”<送信先URL>”>
...
</form>
React 19からは同じような形式で
<form action={submitAction}>
...
</form>
という記述方式が使えます。ちなみにaction属性にURLを渡すと通常のフォームの挙動となります。
キャッチアップのコツ
Web標準に近い流れといえばフレームワークRemixも「Focused on web standards and modern web app UX, you’re simply going to build better websites」とトップページで謳っている通り、Web標準に焦点を充てた方向性を表しています。
何を勉強していいかわからなくなってきたら、Web標準の技術に立ち戻るというのは、勉強方法のスタンスとして有効です。
簡単ハンズオンで理解しよう
さて、form要素のaction属性を使えるとして、では一体どんなコードで書いていくのかはまだ想像がつきにくいと思います。そこで、コードサンプルを用意してみました。一緒に見ていきましょう。
今回のコードサンプルは Formから送信される先のバックエンドサーバーも簡易的に用意します。本当に簡易的ですが、次回以降のサンプルでも使いまわしますので、ぜひローカルで作成してみてください。
流れとしては、ローカルのバックエンドサーバーを作成した後、Next.js15のRC版のプロジェクトを作成します。なぜReact19の実験なのにNext.jsのプロジェクトを新規作成するかという点については、後半説明をします。■まずはPost先のバックエンドを作成する
ローカルのターミナルでバックエンドのNode.jsプロジェクトを作成するためのディレクトリを用意し、そのディレクトリに移動した後、npm init
コマンドにてプロジェクトを初期化してpackage.json
を生成します。この流れはよくやる、という方も多いかもしれません
$ mkdir my-backend
$ cd my-backend
$ npm init
続いて、必要なライブラリをインストールします。必要なライブラリは以下です。
cors
:CORS関係のサポートをするミドルウェアexpress
:Webサーバーのフレームワーク
$ npm install express cors
次に、src/app.js
に下記内容でファイルを生成します。ちなみに、4000番ポートで起動するのは、フロントエンド側のプロジェクトで3000番ポートを使うためです。
src/app.js
const express = require("express");
const cors = require("cors");
const app = express();
const port = process.env.PORT || 4000;
app.use(
express.urlencoded({
extended: true,
})
);
app.use(express.json());
app.use(cors());
app.post("/api/users", (req, res) => {
console.log(req.body);
res.json({ id: 1, name: req.body?.name, message: "success" });
});
app.listen(port, () => {
console.log(`Server is running on port ${port}.`);
});
大変シンプルなバックエンドですが、React 19の一部の機能を試すだけなので、この程度で十分です。もし既にお手製のAPIサーバーがあれば、そちらを使っても構いません。以下のコマンドで起動します。
$ node src/app.js
このAPIサーバーは/api/users
にPOSTをすると以下のJSONが返ってくるだけの機能しかありません。
{ id: 1, name:<POST 時に送信した name の値>, message: "success" }
■React19が動作するフロントエンドを用意する
続いてフロントエンド側のプロジェクトを作成します。今回は、Next.jsのRC版*で作成します。今回はnpx
にて以下のコマンドを実行し、以下のような初期構成でプロジェクトを作成します。
$ npx create-next-app@rc --turbo
✔ What is your project named? … <プロジェクト名>
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
✔ What import alias would you like configured? … @/*
まずは、バックエンドにPOSTする関数を用意します。data.tsx
という名前で以下のファイルを用意しました。このファイルにはMessage
というバックエンドからのレスポンスの型と、postUser
という関数が定義されています。この関数では先ほど作成したバックエンドにname
の値を送信する非同期な関数です
src/app/data.ts
export interface Message {
id: string;
name: string;
message: string;
}
export const postUser = async (name: string): Promise<Message> => {
const res = await fetch("http://localhost:4000/api/users", {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
return res.json();
};
続いて、作成したばかりのpostUser
関数をたたくためのフォームコンポーネントを用意しましょう。user.tsx
という名前で以下の内容のTSXファイルを用意します。これはクライアントコンポーネントとして作成します。
src/app/user.tsx
"use client";
import { useActionState } from "react";
import { Message, postUser } from "./data";
export const UserForm = () => {
const [state, submitAction, isPending] = useActionState(
async (_: Message | null, formData: FormData) => {
const name = formData.get("name") as string;
const result = await postUser(name);
return result;
},
null
);
return (
<>
<form action={submitAction} className="flex gap-4">
<input
name="name"
defaultValue={state?.name}
className="p-2 bg-slate-200"
/>
<button type="submit" className="p-2 bg-slate-300">
Submit
</button>
</form>
{isPending ? <p>Submitting ...</p> : null}
{state?.message ? <p>{state?.message}</p> : null}
{state ? <p>{JSON.stringify(state)}</p> : null}
</>
);
};
以上のコードを見てみると、React 19の新機能が多分に含まれていることがわかります。見たことのないものとしてはuseActionState
というhooks、そしてform
要素のaction
属性にsubmitAction
関数を渡しているところでしょうか。
まず、useActionState
というhooksから見てみます。このhooksはフォームと連携をして非同期処理の「フォームの状態」と「フォームアクション関数」を管理する機能があります。第1引数には「フォームアクション関数」を指定し、第2引数には初期状態のステートを指定します。
ここでは特に最初は何もないということでnull
を指定しています。フォームアクション関数は、現在の状態と現在のフォームデータを引数とする関数を指定します。この中でpostUser
関数を呼び出して実際にAPIに通信していることがわかります。
const [state, submitAction, isPending] = useActionState(
async (_: Message | null, formData: FormData) => {
const name = formData.get("name") as string;
const result = await postUser(name);
return result;
},
null
);
useActionState
の返り値は3つあり、それぞれ「現在の値」「form
要素のaction
属性に指定する関数」「保留中かどうかのboolean
値」となります。
ちなみに、細かい部分が気になる方は忘れずにリファレンスドキュメントにも目を通しておきましょう
以前であればonSubmit
やuseEffect
などを使って書いていたと思いますが、ずいぶんWeb標準に近い、すっきりとした感じになりました。非同期な処理も扱いやすくなっていると思いませんか?
続いてuseActionState
の返り値の一つ「form
要素のaction
属性に指定する関数」をform
要素のaction
属性にsubmitAction
関数を指定して完成です。
export const UserForm = () => {
...
return (
<>
<form action={submitAction} className="flex gap-4">
...
</form>
</>
);
};
あとは、このフォームをsrc/app/page.tsx
に埋め込むのみです。例えば以下のような感じです。
src/app/page.tsx
import { UserForm } from "./user";
export default function Home() {
return (
<div className="p-6">
<h1 className="font-bold mt-4 mb-2 text-2xl">Users</h1>
<h2 className="font-bold mt-4 mb-2">Add user</h2>
<UserForm></UserForm>
</div>
);
}
以上で、必要なファイルの作成は完了です。アプリケーションを起動してみましょう。
$ npm run dev
フォームに適当な名前を入れてSubmitボタンを押すと、サーバーに値が送信され、レスポンスも受け取れたことがわかると思います。
React 19以降はこのような風にフォームを作ることができます。いろいろ遊んでみましょう。
新しくReactアプリを作るときは
ところで、皆さんは最近新しくReactアプリを作ったことはありますか?
ひょっとしたら「最近新しくReactアプリを作成していないや」という人もいるかもしれません。お仕事でReactアプリを作る機会があっても、プロジェクト立ち上げの時期から関わっていなければ、すでにビルド環境が整った状態で開発を進めることになるからです。
大昔であれば(といっても2017年以前ですが)、WebpackやBabelを駆使してビルド環境を作っていたと思います。その後Create React App(通称CRA)が主流となっていき、CRAで新規Reactアプリを作成していた人も多いのではないでしょうか。
ところが、2023年よりCRAは新規Reactアプリ作成には推奨されなくなっています。現在のReactのドキュメントを見てみましょう。
現在ドキュメントではフレームワークを使うのがベストプラクティスであるというスタンスで記述されており、Next.jsやRemix、GatsbyなどがWebアプリケーションフレームワークとして並んでいます。
しかし、Reactの機能を単体で試したいとかコンパクトに導入したいというケースもあると思います。そんなときはViteのテンプレートを使うのが個人的にはおすすめです。
$ npm create vite@latest <プロジェクト名> -- --template react
シンプルな構成で、純粋にReactに取り組みたい人に扱いやすい構成です。また、次回以降で述べますが、ViteやTurboPackといった新世代のフロントエンドツールに慣れておくことも、技術になじんでおくという点でやっておきたいことです。
レガシーな環境での注意点と対応
レガシー環境向けで忘れてはならないアップデートに、関数コンポーネントのPropTypes
チェックの廃止があります。
PropTypes
はコンポーネントに渡すprops
の型チェックを行う仕組みとして、JavaScriptベースで作られているプロジェクトでは重宝していました。ただし、React 15.5にてReact.PropTypes
が外にnpm
パッケージとして切り出されました。
ただ、その後もこれらライブラリを用いればコンポーネントの型チェックを行うことができていました。ところが、React 19にてついにコンポーネントがPropTypes
チェックを行わなくなりました。
React 18まではブラウザの開発ツール上でこんな感じで型が合わないとエラーが出ていました。
これがReact 19ではエラーが出なくなっているので注意が必要です。例えばJavaScriptを使って書かれている古いフレームワークではクラスコンポーネントとPropTypes
でコンポーネントを構成しているケースが結構あるのではないでしょうか。実際筆者もPropTypes
を使ったプロジェクトに今も関わっています。
React 19では今後はTypeScriptを利用して型チェックをするようにというアナウンスがされており、PropTypes
を使っていたプロジェクトはTypeScriptへの書き換えも検討してみてはいかがでしょうか。
・・・
いかがでしたか。最新のReactでの変化が把握できたでしょうか。次回はレンダリング周りのアップデートについて見ていきます。お楽しみに。
▼React / Next.js / Remix にみるこの頃のアーキテクチャ変化
1:React 19 のアップデートにみる変化
2:レンダリングアーキテクチャの変化
3:フロントエンドルーティング定義の変化