Next.jsを使った開発における勘所を鍛える

2024/08/04に公開

目的

Next.jsを使った開発における重要なポイントを理解し、勘所を鍛える

背景

Next.jsの開発時に何を使ってどう実装するのかがすぐに思い浮かばない。
どんなことが出来るのかを網羅的に学ぶことで何を使うべきなのかパッと出てくるようになりたいという思いのもと、当記事を書いています。

知識のキャッチアップ

まずは、以下に関して公式ドキュメントやGPTを用いて知識のキャッチアップを行います。

  • React
  • React Hook Form
  • Next.js
  • NextUI
  • next-safe-action

その上で、それぞれの違いを体感すべく、数パターンのTodoリストを実装します。

TODOリストの実装からそれぞれの違いを体感する

1. ReactとNext.jsの違い

2. React Hook Formを使った時と使わなかった時

まずは知識のインプットから。

Reactとは

Reactとは

Reactは、WEBアプリケーションのユーザーインターフェースを構築するためのJavaScriptライブラリです。2024年8月1日時点の最新バージョンは18.3.1です。

主な特徴

  • コンポーネントベース: UIを小さな再利用可能な部品(コンポーネント)に分割して開発します。これにより、コードの再利用性が高まり、保守が容易になります。
  • フック: React16.8で導入された機能で、関数コンポーネントでstateやライフサイクルなどのReactの機能を使えるようにします。
  • 宣言的: どのようにUIを構築するかではなく、どのようなUIにしたいかを記述することで、開発者の意図が明確に伝わります。
  • 仮想DOM: 効率的な更新のために、実際のDOMの軽量コピーを使用します。これにより、パフォーマンスが向上します。
  • Facebookが開発: ReactはFacebookによって開発されており、大規模なアプリケーションにも対応できる設計がされています。
  • 豊富なエコシステム: 多くのライブラリやツールが利用可能であり、開発者のニーズに合わせた選択が可能です。

Reactのエコシステム

Reactのエコシステムには、多くのライブラリやツールが存在し、それらを組み合わせることで、さまざまな機能を実現できます。

  • コンポーネントライブラリ: Material UIAnt DesignChakra UIなど、のライブラリを使用することで、事前に設計されたコンポーネントを利用し、迅速に美しいUIを構築できます。
  • ルーティング: React Routerのようなライブラリを使って、シングルページアプリケーション(SPA)のナビゲーションを簡単に実装できます。
  • 状態管理: ReduxMobXRecoilなどのライブラリを使って、アプリケーションの状態を効率的に管理できます。
  • フォーム処理: React Hook Formなどのライブラリを使って、複雑なフォームの処理を簡略化できます。
  • テスティング: JestReact Testing Libraryなどのツールを使って、Reactアプリケーションのテストを書くことができます。
  • 開発ツール: Create React Appのようなプロジェクトの設定を簡略化するツールがあり、迅速な開発を支援します。
  • アニメーション: React SpringFramer Motionなどのライブラリを使って、滑らかなアニメーションを実装できます。

Reactのフック

Reactのフックは、React16.8で導入された機能で、関数コンポーネントで状態やその他のReactの機能を使用するためのAPIです。クラスコンポーネントを書かずに、関数コンポーネントでReactの機能を使えるようにします。

主要なフックとその使用方法

1. useState

状態(state)を管理するためのフックです。

const [state, setState] = useState(initialValue);
  • state: 現在の状態値
  • setState: 状態を更新する関数
  • initialValue: 初期状態
使用例
カウントを増やすカウンター
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default Counter;

2. useEffect

副作用を扱うためのフックです。副作用とは、コンポーネントのレンダリング以外の操作や処理を指します。

役割
  • コンポーネントのレンダリング後に副作用を実行する
  • 依存関係に基づいて副作用の再実行を制御する
  • 必要に応じて、クリーンアップ(副作用の解除)を行う
useEffect(() => {
  // 副作用を行うコード
  return () => {
    // クリーンアップ関数(オプション)
  };
}, [dependencies]);
  • dependencies: 依存配列。この配列の値が変更された時に副作用が再実行されます。
使用例
カウントが変更された時にドキュメントのタイトルを更新する
import React, { useEffect, useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

export default Counter;

3. useContext

コンテキストを利用するためのフックです。

const value = useContext(MyContext);
使用例
コンテキストを使ったテーマ切り替え
import React, { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme === 'dark' ? '#333' : '#fff' }}>Button</button>;
}

export default App;

4. useReducer

複雑な状態ロジックを管理するためのフックです。

const [state, dispatch] = useReducer(reducer, initialState);
  • state:現在の状態
  • dispatch:アクションを送信する関数
    • アクションオブジェクトを引数に取り、reducerを呼び出して状態を更新します
    • アクションオブジェクトは通常、typeプロパティを持ちます
  • reducer: 状態を更新するロジックを含む関数。現在の状態と実行されたアクションを引数に取り、新しい状態を返します。
  • initialState: 初期状態
使用例
カウンターの状態を管理するリデューサー
import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
}

export default Counter;

5. useRef

DOM要素への参照や再レンダリングをトリガーせずに値を保持するためのフックです。

主な用途
  • DOM要素への参照
    • JSXの要素にref属性を設定することで、その要素への直接アクセスが可能になります
  • 可変な値の保持
    • コンポーネントの再レンダリングをトリガーせずに値を保持できます
特徴
  • .currentプロパティは可変
  • currentの値を変更しても再レンダリングは発生しません
  • refの値はコンポーネントのライフサイクルを通じて保持されます
useStateとの違い
  • useStateは値が変更されると再レンダリングをトリガーします
  • useRefは値が変更されても再レンダリングをトリガーしません
注意点
  • DOM操作を行う場合は、useEffectフック内で行うのが安心(レンダリング後に実行されるため)
  • refの値を直接レンダリングに使用することは避けます(変更が反映されない可能性があるため)
const refContainer = useRef(initialValue);
  • initialValue:refオブジェクトの.currentプロパティの初期値です
使用例
input要素にフォーカスを設定
import React, { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  useEffect(() => {
    // コンポーネントがマウントされたらinput要素にフォーカスを当てる
    inputEl.current.focus();
  }, []);

  return (
    <div>
      <input ref={inputEl} type="text" />
    </div>
  );
}

export default TextInputWithFocusButton;

6. useMemo

計算コストの高い値をメモ化するためのフックです。

主な用途
  • 計算コストの高い操作の結果をキャッシュします
  • 不要な再計算を避けてパフォーマンスを向上させます
特徴
  • 依存配列の値が変更されない限り、メモ化された値は再利用されます
  • コンポーネントが際レンダリングされても、依存値が変わらなければ計算は実行されません
注意点
  • メモ化自体もコストがかかるため、単純な計算には不要
useCallbackとの違い
  • useMemoは値をメモ化
  • useCallbackは関数をメモ化
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 第一引数:メモしたい値を計算する関数
  • 第二引数:依存配列(この配列の値が変更された時のみ、関数が再実行されます)
使用例
数値のリストをフィルタリングして偶数だけを表示する
import React, { useState, useMemo } from 'react';

function FilterList() {
  const [numbers, setNumbers] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

  const evenNumbers = useMemo(() => {
    return numbers.filter(number => number % 2 === 0);
  }, [numbers]);

  return (
    <div>
      <h1>Even Numbers</h1>
      <ul>
        {evenNumbers.map(number => (
          <li key={number}>{number}</li>
        ))}
      </ul>
    </div>
  );
}

export default FilterList;

7. useCallback

関数をメモ化するためのフックです。

主な用途
  • 子コンポーネントに渡すコールバック関数の参照に安定性を持ちます
  • 不要な際レンダリングを防ぎます(特にReact.memoと組み合わせて使用する場合)
  • 依存配列に使用する関数の安定性を保ちます(useEffectなどで使用する場合)
特徴
  • 依存配列の値が変更されない限り、メモ化された関数は再利用されます
  • コンポーネントが再レンダリングされても、依存値が変わらなければ新しい関数は生成されません
使うべき理由
  • 子コンポーネントがReact.memoでラップされており、プロップスとして関数を渡す場合
  • useEffectの依存配列に関数を含める必要がある場合
  • 関数が複雑で再生成のコストが高い場合
※React.memoとは?

React のパフォーマンス最適化のためのより高度な機能。これは、関数コンポーネントをメモ化(記憶)するために使用されます。

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
  • 第一引数:メモ化したい関数
  • 第二引数:依存配列(この配列の値が変更された時のみ、関数が再生成される)
使用例
子コンポーネントに渡すイベントハンドラをメモ化する
import React, { useState, useCallback } from 'react';

function Child({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
}

export default Parent;

Reactのルール(一部抜粋)

1. コンポーネントのルール

1.1 単一責任の原則

  • 各コンポーネントは1つの責任のみを持つべき
  • 大きなコンポーネントは小さな、再利用可能なコンポーネントに分割する

1.2 純粋関数としてのコンポーネント

  • 同じ入力(props)に対して常に同じ出力(JSX)を返す
  • 副作用はuseEffect内で管理する

2. フックのルール

2.1 フックは最上位でのみ呼び出す

  • 条件分岐やループ内でフックを使用しない
  • コンポーネントの最上位レベルでのみフックを呼び出す

2.2 React関数内でのみフックを呼び出す

  • 通常のJavaScript関数ではなく、Reactの関数コンポーネント内でのみフックを使用する
  • カスタムフック内でも使用可能(useで始まる関数名)

3. 状態管理

3.1 状態の最小化

  • 必要な状態のみを保持し、派生データは計算して取得する

3.2 状態の適切な配置

  • 状態を必要とするコンポーネントの最も近い共通の親に配置する
React Hook Formとは

React Hook Formとは

React Hook Formは、Reactアプリケーションでフォーム処理を簡単かつ効率的に行うためのライブラリです。

主な特徴

  • 軽量: 他のフォームライブラリと比較して小さなバンドルサイズです。
  • パフォーマンス: 不要な再レンダリングを最小限に抑えます。
  • 柔軟性: 制御されたコンポーネントと非制御コンポーネントの両方をサポートします。
  • バリデーション: 組み込みのバリデーション機能を提供します。

主なAPI

  • useForm:フォームの状態と機能を管理するメインのフック
  • register:入力フィールドをReact Hook Formに登録
  • handleSubmit:フォーム送信時の処理を定義
  • errors:バリデーションエラーを含むオブジェクト
  • watch:フォームの値の変更を監視
  • setValue:フォームの値を手動で設定

使用方法

  • useFormフックを使用してフォームの状態を初期化
  • register関数を使用して各入力フィールドを登録
  • handleSubmitを使用してフォーム送信処理を定義
  • エラー処理とバリデーションメッセージの表示
例:簡単なログインフォーム
import React from 'react';
import { useForm } from 'react-hook-form';

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = data => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          {...register("email", { 
            required: "Email is required",
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
              message: "Invalid email address"
            }
          })}
        />
        {errors.email && <span>{errors.email.message}</span>}
      </div>
      
      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          {...register("password", { 
            required: "Password is required",
            minLength: {
              value: 8,
              message: "Password must be at least 8 characters"
            }
          })}
        />
        {errors.password && <span>{errors.password.message}</span>}
      </div>
      
      <button type="submit">Login</button>
    </form>
  );
}

export default LoginForm;

バリデーション

  • 組み込みのバリデーションルール(required, minLength, pattern など)
  • カスタムバリデーション関数のサポート
  • エラーメッセージのカスタマイズ

高度な機能

  • フォームの値の監視と条件付きレンダリング
  • 動的フォームフィールドの追加と削除
  • フォームの状態のリセット
  • 非同期バリデーション

パフォーマンス最適化

  • 不要な再レンダリングを避けるために、コンポーネントの分割を推奨
  • useCallbackとuseMemoを使用して関数と値をメモ化
import React from 'react';
import { useForm } from 'react-hook-form';

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = data => {
    console.log(data);
    // ここでログイン処理を行う
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          {...register("email", { 
            required: "Email is required",
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
              message: "Invalid email address"
            }
          })}
        />
        {errors.email && <span>{errors.email.message}</span>}
      </div>
      
      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          {...register("password", { 
            required: "Password is required",
            minLength: {
              value: 8,
              message: "Password must be at least 8 characters"
            }
          })}
        />
        {errors.password && <span>{errors.password.message}</span>}
      </div>
      
      <button type="submit">Login</button>
    </form>
  );
}

export default LoginForm;

register("example")は何をしている?

このフィールドを "example" という名前で react-hook-form のシステムに登録しています。

Inputコンポーネントにregisterを直接使用せずcontrolプロパティを使用するケースを目にしたことがあるので、それぞれの違いを列挙します。

Inputコンポーネントのregister、controlプロパティの違い

統合方法の違い

  • registerは主に基本的なHTML入力要素(<input>, <select> など)に直接使用されます
  • controlはカスタムコンポーネントや複雑な入力要素をreact-hook-form と統合するために使用します

実装の複雑さ

  • register を使用する方が簡単で直接的
  • controlを使用する場合、通常はControllerコンポーネントやuseControllerフックを併用する必要があり、やや複雑になります

柔軟性

  • controlを使用する方が、カスタムロジックや複雑な入力処理を実装する際に柔軟性が高くなります

パフォーマンス

  • controlを使用すると、不要な再レンダリングを避けるなど、パフォーマンスの最適化がしやすくなる場合があります
Next.jsとは

Next.jsとは

Next.jsはReactベースのウェブアプリケーションフレームワークで、サーバーサイドレンダリング(SSR)、静的サイト生成(SSG)、ルーティングなどの機能を提供します。Next.jsは2016年にVercel社からリリースされ、2024年8月1日時点の最新バージョンは14.2です。

ReactとNext.jsの違い

特徴 React Next.js
種類 ライブラリ フレームワーク
レンダリング クライアントサイド(デフォルト) サーバーサイド、静的、クライアントサイド
ルーティング 要追加(React Router等) 組み込み(ファイルベース)
パフォーマンス最適化 手動 自動(コード分割等)
サーバーサイド機能 なし APIルート機能あり
ビルドプロセス 要設定 最小限の設定で可能

主な特徴

  • サーバーサイドレンダリング: サーバー側でページを生成することで、初期読み込みが速くなり、SEO対策にも有効です。
  • 静的サイト生成: ビルド時にHTMLを生成し、配信することで高速なパフォーマンスを実現します。
  • ファイルベースのルーティング: プロジェクトのディレクトリ構造に基づいて自動的にルーティングを設定します。

サーバーアクション

Next.jsでは、サーバーアクションを使用してクライアントサイドのコンポーネントから直接サーバーサイドの処理を呼び出すことができます。これにより、フロントエンドとバックエンドの境界が曖昧になり、開発がシームレスになります。

APIとの主な違い:

1. サーバーの立ち上げが不要:

  • 従来のAPI: 別途サーバーを立ち上げ、エンドポイントを定義する必要がある。
  • サーバーアクション: Next.jsアプリケーション内で直接定義・使用できる。

2. OpenAPI定義が不要:

  • 従来のAPI: エンドポイントとフロントエンドの型を合わせるためにOpenAPI定義が必要。
  • サーバーアクション: TypeScriptの型推論により自動的に型が合う。

3. コード共有:

  • 従来のAPI: サーバーとクライアントで別々にコードを管理する必要がある。
  • サーバーアクション: 同じプロジェクト内でコードを共有できる。
従来のAPIアプローチ
// server.ts (別のサーバープロジェクト)
import express from 'express';
import { z } from 'zod';

const app = express();
app.use(express.json());

const todoSchema = z.object({
  title: z.string().min(1).max(100),
});

app.post('/api/todos', (req, res) => {
  try {
    const { title } = todoSchema.parse(req.body);
    // データベース操作
    const newTodo = { id: Date.now(), title, completed: false };
    res.status(201).json(newTodo);
  } catch (error) {
    res.status(400).json({ error: 'Invalid input' });
  }
});

app.listen(3001, () => console.log('API server running on port 3001'));

// client/src/api.ts (フロントエンドプロジェクト)
export async function createTodo(title: string) {
  const response = await fetch('http://localhost:3001/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title }),
  });
  if (!response.ok) throw new Error('Failed to create todo');
  return response.json();
}
従来のAPIアプローチ
Next.jsのサーバーアクション
app/actions.ts
'use server'

import { z } from 'zod';

const todoSchema = z.object({
  title: z.string().min(1).max(100),
});

export async function createTodo(data: z.infer<typeof todoSchema>) {
  const { title } = todoSchema.parse(data);
  // データベース操作
  const newTodo = { id: Date.now(), title, completed: false };
  return newTodo;
}
app/components/TodoForm.tsx
'use client'

import { useTransition } from 'react';
import { createTodo } from '../actions';

export function TodoForm() {
  const [isPending, startTransition] = useTransition();

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    startTransition(() => createTodo({ title: formData.get('title') as string }));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="title" required />
      <button type="submit" disabled={isPending}>Add Todo</button>
    </form>
  );
}
NextUIとは

NextUIとは

NextUIは、React向けのUIコンポーネントライブラリで、美しいデザインと高いカスタマイズ性が特徴です。NextUIはVercelとは無関係の独立したコミュニティプロジェクトです。

主な特徴

  • 美しいモダンなデザイン: 最新のデザイントレンドに沿ったコンポーネントを提供します。
  • 高度なカスタマイズ性: テーマやスタイルのカスタマイズが容易です。
  • TypeScriptサポート: 型安全性の高い開発が可能です。
  • ダークモードサポート: ビルトインのダークモード機能を提供します。
  • アクセシビリティ: WAI-ARIAガイドラインに準拠しており、アクセシビリティに配慮しています。
  • レスポンシブデザイン: モバイルフレンドリーなコンポーネントを提供します。
    NextUIの使用は非常に簡単で、以下のようにインストールして使用できます。

主要コンポーネント

  • レイアウト:Grid, Container, Spacer など
  • フォーム:Input, Checkbox, Radio, Select など
  • ナビゲーション:Navbar, Link, Pagination など
  • フィードバック:Modal, Toast, Popover など
  • データ表示:Table, Card, Avatar など

使用方法

  • インストール:npm install @nextui-org/react
  • NextUIProviderでアプリケーションをラップ
  • 必要なコンポーネントをインポートして使用
npm install @nextui-org/react
import { NextUIProvider, Button } from '@nextui-org/react';

function App() {
  return (
    <NextUIProvider>
      <Button>Click me</Button>
    </NextUIProvider>
  );
}

この例では、NextUIProviderでアプリケーション全体をラップし、Buttonコンポーネントを使用しています。NextUIは、Reactアプリケーションのスタイリングを迅速かつ簡単に行いたい場合に最適な選択です。特に、統一されたデザインを保ちながら迅速にプロトタイプを作成する際に役立ちます。

NextUIとTailwindCSSの違い

比較表

特徴 NextUI Tailwind CSS
タイプ コンポーネントライブラリ ユーティリティファーストCSSフレームワーク
主な目的 事前に設計されたReactコンポーネントを提供 カスタムデザインを迅速に構築するためのユーティリティクラスを提供
カスタマイズ性 コンポーネントレベルでのカスタマイズ 非常に高い(クラスの組み合わせで細かい調整が可能)
デザインの一貫性 高い(プリセットされたデザイン) 開発者次第(一貫性を保つには努力が必要)
パフォーマンス 最適化されたコンポーネント 使用したクラスのみをビルドに含める(PurgeCSS)
React統合 ネイティブサポート 追加のセットアップが必要
アクセシビリティ コンポーネントレベルで対応 開発者が実装する必要がある
テーマ設定 組み込みのテーマシステム カスタマイズ可能な設定ファイル

基本的な性質

  • NextUI: Reactアプリケーション向けの完全なUIコンポーネントライブラリ
  • Tailwind: HTMLのクラス名を使用してスタイリングを行うユーティリティベースのCSSフレームワーク

使用目的

  • NextUI: 迅速にモダンなUIを構築したい開発者向け。特に、一貫したデザインが必要な場合に適している。
  • Tailwind: カスタムデザインを細かく制御したい開発者向け。柔軟性が高く、独自のデザインシステムを構築する際に適している。

コンポーネントとスタイリング

  • NextUI: 事前に設計されたコンポーネント(ボタン、カード、フォームなど)を提供。各コンポーネントは独自のAPIを持つ。
  • Tailwind: コンポーネントは提供せず、HTMLに直接適用できるユーティリティクラスを提供。

カスタマイズ

  • NextUI: コンポーネントのプロパティを通じてカスタマイズ。テーマシステムを使用してグローバルな変更も可能。
  • Tailwind: 設定ファイルを通じてカラー、サイズ、ブレークポイントなどをカスタマイズ。クラスの組み合わせで細かい調整が可能。

パフォーマンス

  • NextUI: 最適化されたコンポーネントを提供。必要なコンポーネントのみをインポート可能。
  • Tailwind: 使用していないクラスを除外するPurgeCSSにより、最終的なCSSファイルサイズを最小化。

アクセシビリティ

  • NextUI: コンポーネントレベルでアクセシビリティに対応。
  • Tailwind: アクセシビリティは開発者が自身で実装する必要がある。
next-safe-actionとは

next-safe-actionとは

next-safe-actionは、Next.jsアプリケーションでサーバーアクションを型安全に扱うためのライブラリです。最新バージョンは4.0.4です。このライブラリは、サーバーアクションの定義と使用を簡素化し、型安全性とセキュリティを強化します。

主な特徴

  • 型安全性: TypeScriptを完全にサポートし、クライアントとサーバー間のデータの型を保証します。
  • 入力バリデーション: Zodなどのスキーマバリデーションライブラリと統合し、入力データを検証します。
  • エラーハンドリング: サーバーサイドのエラーを適切に捕捉し、クライアントに伝播させます。
  • セキュリティ: CSRFトークンの自動処理など、セキュリティ機能を組み込んでいます。

使用方法

next-safe-actionを使うことで、以下のように簡単にサーバーアクションを定義し、クライアントから呼び出すことができます。

// next-safe-actionを使用してTodoを作成するサーバーアクションを定義し、それをクライアントコンポーネントから使用している
// actions/createTodo.ts
import { z } from 'zod';
import { action } from '@/lib/safe-action'

const schema = z.object({
  title: z.string().min(1).max(100),
});

export const createTodo = action(schema, async ({ title }) => {
  // サーバーサイドのロジック(例:データベースへの保存)
  const newTodo = await saveTodoToDatabase(title);
  return { success: true, todo: newTodo };
});

// components/CreateTodoForm.tsx
'use client'

import { useAction } from '@/lib/safe-action';
import { createTodo } from '@/actions/createTodo';

export function CreateTodoForm() {
  const { execute, result, status } = useAction(createTodo);

  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    execute({ title: formData.get('title') as string });
  };

  return (
    <form onSubmit={onSubmit}>
      <input type="text" name="title" required />
      <button type="submit" disabled={status === 'executing'}>
        Add Todo
      </button>
      {result && result.data && (
        <p>Successfully created todo: {result.data.todo.title}</p>
      )}
    </form>
  );
}

// lib/safe-action.ts
import { createSafeActionClient } from 'next-safe-action';

export const action = createSafeActionClient();
export { useAction } from 'next-safe-action/client';

next-safe-actionがあると何が嬉しいの?

型安全性

  • next-safe-actionでは、スキーマ定義から入力と出力の型が自動的に推論される。
  • 標準の実装では、型の定義と検証を手動で行う必要がある。

バリデーション

  • next-safe-actionでは、スキーマ定義がアクション定義に直接統合されています。
  • 標準の実装では、バリデーションロジックを明示的に記述する必要があります。

エラーハンドリング

  • next-safe-actionでは、エラーが自動的に捕捉され、型安全な方法で返されます。
  • 標準の実装では、エラーハンドリングを手動で実装する必要があります。

クライアント側の使用

  • next-safe-actionのuseActionフックは、実行、状態管理、結果の取得を1つのAPIで提供します。
  • 標準の実装では、useFormStateを使用し、状態管理を手動で行う必要があります。

コードの簡潔さ

  • next-safe-actionを使用した実装は、より簡潔で読みやすくなっています。
  • 標準の実装では、より多くのボイラープレートコードが必要です。

次に、数種類のTODOリストを作ってそれぞれの違いを体感します。

TODOリストを作ってそれぞれの違いを体感する

ReactとNext.jsの違い

所感

ReactとNext.jsそれぞれでTodoリストを開発し、両者の違いに深い発見がありました。

1. バックエンドの統合が不要

Reactでアプリを作る際、バックエンドは別途用意する必要がありました。
Express.jsなどでサーバーを立て、APIを実装し、それをReactアプリから呼び出す...という具合です。

Next.jsでは、API RoutesやServer Componentsを使うことで、同じプロジェクト内にフロントエンドとバックエンドのコードを共存させることができます。

バックエンドのお膳立てが不要だから便利だー!!

2. ファイルベースのルーティング

Reactでは、通常 react-router などのライブラリを使ってルーティングを設定します。

Next.jsでは、ファイルシステムに基づいたルーティングが自動的に行われます。
pages/about.js というファイルを作れば、自動的に/aboutというルートが生成されるのです。

これにより、アプリの構造が直感的でわかりやすい!

React Hook Formを使った時と使わなかった時

所感

私が体感したReact Hook Formを使った時と使わなかった時の違いは、以下の2点に集約されます。

1. リアルタイムバリデーション

React Hook Formは入力中や送信前にZodのバリデーションを自動的に行ってくれます。これにより、ユーザーは即座にフィードバックを受け取ることができ、フォームの使いやすさが大幅に向上します。

4. 自動的なエラー管理

React Hook Formはエラーの状態管理を自動的に行います。従来の方法では、エラーの状態を手動で管理する必要があり、それはしばしば複雑で間違いやすいプロセスでした。React Hook Formを使用することで、このプロセスが大幅に簡素化され、開発者はより本質的なロジックに集中できるようになります。

まとめ

今回の記事の目的は、「Next.jsを使った開発における勘所を鍛える」でした。
Next.jsに関連する事項を学び、それぞれの違いを実装しながら体感したことでより理解が深まりました。

普段Tiltを使って準備された環境の上で開発をしているので気づかなかった、環境構築の大変さにも気づけて良い機会になったと思います。

関連記事

Next.jsを使った開発における勘所を鍛える
React + Zod + Bun + PrismaでTODOリストを作りました
Next.js(App Router) + Zod + PrismaでTODOリストを作りました
Next.js(App Router) + Zod + Prisma+React Hook FormでTODOリストを作りました
GitHub

Discussion