チュートリアルこのページを編集

サーバ関数を使う

このチュートリアルの目標は、クライアント側のJavaScriptとReactハイドレーションを一切使用しないことです。React Server Componentsとサーバ関数のみを使用します。これは可能でしょうか?もちろん可能です!

新しいフォルダを作成してpnpmを初期化し、必要な依存関係をすべてインストールして、新しいプロジェクトを作成しましょう。

mkdir todo
cd todo
pnpm init
pnpm add @lazarv/react-server better-sqlite3 zod
pnpm add -D @types/better-sqlite3 @types/react @types/react-dom autoprefixer postcss tailwindcss@3 typescript
pnpx tailwindcss@3 init -p

Todo項目を保存するには、ローカルのSqliteデータベースを使用します。検証にはZodを使用し、スタイル設定にはTailwind CSSを使用します。すべてのソースコードをTailwindコンテンツとして含めるには、tailwind.config.jsを次のように変更します:

./tailwind.config.js
/** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.tsx"], theme: { extend: {}, }, plugins: [], };

今回は特別凝ったTailwindでのスタイリングは行わないので、通常の3行のTailwind設定をsrc/index.cssに配置します:

./src/index.css
@tailwind base; @tailwind components; @tailwind utilities;

さて、何よりも良いのは古典的な"Hello World!"アプリです。Todoアプリのエントリーポイントを作成しましょう!以下のコードをsrc/index.tsxに入れてください:

./src/index.tsx
export default function Index() { return ( <h1>Hello World!</h1> ); }

このマイクロアプリを実行するには、pnpm react-server ./src/index.tsxを使用するだけです。作業を簡単にするために、package.jsonにnpmスクリプトをいくつか追加しましょう:

./package.json
"scripts": { "dev": "react-server ./src/index.tsx", "build": "react-server build ./src/index.tsx", "start": "react-server start" },

pnpm devを使用して開発サーバを起動します。開発サーバが起動したら、http://localhost:3000を開いて、Hello World!アプリを起動してください。pnpm dev --openを使用しても実行できます。

Todoアプリにはレイアウトが必要なので、src/Layout.tsxを作成しましょう:

./src/Layout.tsx
export default function Layout({ children }: React.PropsWithChildren) { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Todo</title> </head> <body> <div className="p-4"> <h1 className="text-4xl font-bold mb-4"> <a href="/">Todo</a> </h1> {children} </div> </body> </html> ); }

特別なことは何もなく、通常のHTMLドキュメントテンプレートです。このレイアウトコンポーネントをアプリのラッパーとして使用します。

メインページはTodoアプリです。ここで、すべての構成要素を使用してアプリを作成します。Hello World!に別れを告げて、コンポーネントを次のように変更します:

./src/index.tsx
import "./index.css"; import { allTodos } from "./actions"; import AddTodo from "./AddTodo"; import Item from "./Item"; import Layout from "./Layout"; export default function Index() { const todos = allTodos(); return ( <Layout> <AddTodo /> {todos.length === 0 && <p className="text-gray-500">No todos yet!</p>} {todos.map((todo) => ( <Item key={todo.id} title={todo.title} id={todo.id} /> ))} </Layout> ); }

このReact Server Componentでは、allTodos()を呼び出して保存されているすべてのTodo項目を取得し、その結果を使用してJSXをレンダリングします。レイアウトコンポーネントを使用して、コンテンツをHTMLドキュメントにラップします。

アイテムをレンダリングするには、src/Item.tsx にItemコンポーネントを作成しましょう:

./src/Item.tsx
import { deleteTodo } from "./actions"; type Props = { id: number; title: string; }; export default function Item({ id, title }: Props) { return ( <div className="flex row items-center justify-between py-1 px-4 my-1 rounded-lg text-lg border bg-gray-100 text-gray-600 mb-2"> <p className="flex-1">{title}</p> <form action={deleteTodo}> <input type="hidden" name="id" value={id} /> <button className="font-medium">Delete</button> </form> </div> ); }

Itemコンポーネントは、idtitleプロパティを使用してTodoアイテムをレンダリングします。しかし、<form action={deleteTodo}>はどうでしょうか?これはサーバ関数です。ユーザーが"削除"ボタンをクリックしてフォームを送信すると、ブラウザはサーバ関数を呼び出します。Reactはサーバ関数のプログレッシブエンハンスメントをサポートしており、最初のフォームアクションはフォームに非表示の入力フィールドを含めることでサーバ関数を呼び出すため、フロントエンドにJavaScriptがなくてもこれが可能です。

./src/Item.tsx
<input type="hidden" name="$ACTION_ID_/Users/lazarv/Projects/tutorials/todo/src/actions.ts#deleteTodo">

フレームワークは、この$ACTION_ID_プレフィックス付きパスをサーバ関数に解決し、サーバ関数を呼び出します!

Todoアプリのすべての機能を実装します。これはアプリの中で最も複雑な部分ですが、怖がることはありません。とてもシンプルなので安心してください。それでは、src/actions.tsを作成しましょう。

./src/actions.ts
"use server"; import { redirect } from "@lazarv/react-server"; import Database from "better-sqlite3"; import * as zod from "zod"; const db = new Database("db.sqlite"); db.exec( "CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT)" ); type Todo = { id: number; title: string; }; const addTodoSchema = zod.object({ title: zod .string() .min(3, "Title must be at least 3 characters") .max(100, "Title must be at most 100 characters") .refine((value) => value.length > 0, "Title is required") .transform((value) => value.trim()), }); const deleteTodoSchema = zod.object({ id: zod.string().transform((value) => parseInt(value.trim(), 10)), }); export async function addTodo(formData: FormData) { const result = addTodoSchema.safeParse(Object.fromEntries(formData)); if (!result.success) { throw result.error.issues; } const { title } = result.data; db.prepare("INSERT INTO todos(title) VALUES (?)").run(title); redirect("/"); } export function allTodos() { return db.prepare("SELECT * FROM todos").all() as Todo[]; } export async function deleteTodo(formData: FormData) { const result = deleteTodoSchema.safeParse(Object.fromEntries(formData)); if (!result.success) { throw result.error.issues; } const { id } = result.data; db.prepare("DELETE FROM todos WHERE id = ?").run(id); redirect("/"); }

このファイルの最初の行では、“use server”;ディレクティブを使用して、このファイルをサーバ関数モジュールとして扱うようにフレームワークをインストールします。エクスポートされたすべての非同期関数は、サーバ関数として使用できるようになります。

モジュールのインポート時にSqliteデータベースを初期化し、アイテムの追加および削除操作用のZodスキーマを作成します。

すべてのサーバ関数では、フォームで定義したすべてのフィールドを含むFormDataインスタンスを受け取ります。Object.fromEntriesを使用してJavaScriptオブジェクトに変換した後、これらをsafeParseします。

Zodの検証が失敗した場合、検証の問題をエラーとしてスローします。

成功した場合は、データベースコマンドを実行して、Todo項目をINSERTまたはDELETEします。

最後に、redirectを使用して、ユーザーをサーバ関数呼び出しから戻します。これは、ユーザーがブラウザページを更新してTodo項目を再度作成または削除し、フォームの送信を再利用することを望まないために必要です。

また、ここではallTodos関数も実装し、ストレージ関連のコードすべてを1つのファイルにまとめました。

AddTodoコンポーネントを実装するには、次の内容のsrc/AddTodo.tsxファイルを作成します:

./src/AddTodo.tsx
import { useActionState } from "@lazarv/react-server/router"; import type { ZodIssue } from "zod"; import { addTodo } from "./actions"; export default function AddTodo() { const { formData, error } = useActionState< typeof addTodo, string & Error & ZodIssue[] >(addTodo); return ( <form action={addTodo} className="mb-4"> <div className="mb-2"> <input name="title" type="text" className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg p-2.5" defaultValue={formData?.get?.("title") as string} autoFocus /> </div> <button className="text-white bg-blue-700 hover:bg-blue-800 rounded-lg px-5 py-2 mb-2 text-center" type="submit" > Submit </button> {error?.map?.(({ message }, i) => ( <p key={i} className="bg-red-50 border rounded-lg border-red-500 text-red-500 p-2.5 mb-2" > {message} </p> )) ?? (error && ( <p className="bg-red-50 border rounded-lg border-red-500 text-red-500 p-2.5"> {error} </p> ))} </form> ); }

<form>からサーバ関数を使用する方法は既にわかっています。しかし、サーバ関数呼び出しの結果はどうでしょうか? useActionStateが役に立ちます!

addTodoサーバ関数の関数参照をuseActionStateに渡すことで、この特定のサーバー関数が呼び出されたときのサーバ関数呼び出しの結果を取得できるため、error結果を収集できます。これは、addサーバ関数でスローしたZodエラーの問題になります。したがって、ここですべてのZod検証の問題を反復処理し、サーバ側で検証エラーメッセージをレンダリングできます。

開発サーバの使用中、ページがブラウザにいくつかのJavaScriptモジュールをロードしたことに気付くでしょう。これはホットモジュール置換にのみ使用されます。実稼働ビルドでは、ドキュメントとCSSアセットのみがブラウザにロードされます。

本番環境用にビルドするには、pnpm buildを実行し、その後pnpm startを使用して本番サーバーを起動できます。

これで小さなTodoアプリが完成しました。例はGitHubでも見つかります。上記のアプローチを使用すると、Todo項目の更新機能を簡単に追加し、項目を完了としてマークできます。ここではクライアントコンポーネントは使用していません。これは別のチュートリアルで説明します。