初心者向け!3つのパターンのコード比較で理解するNext.jsのコンポーネント分割

初心者向け!3つのパターンのコード比較で理解するNext.jsのコンポーネント分割

投稿日: 2025年08月03日

学習振り返り
Tips
要約
  • コンポーネント化の重要性とその方法についての説明。
  • 初めに1つのファイルに全てのコードを書いた状態から、段階的にコンポーネントを分けていく手法を紹介。
  • 具体的なコード例を基に、再利用性や保守性を向上させるためのコンポーネント分割のポイントを解説。
音声で記事を再生
0:00

はじめに

コードレビューで「このコード、もう少しコンポーネントに分けた方が良さそうです!」というフィードバックを受けたものの、具体的にはどうすれば...と悩んでませんか?

自分も以前、コンポーネント化(コンポーネント分割)でめちゃくちゃ苦労しました😅 コンポーネントに分割しようと書き換えをはじめたら、警告やエラーの嵐で、苦労の末にやっと完成させたコードが再び機能不全になってしまう🤮(ツライ・・・。みんなが経験していく道です。

そんな茨の道の地図として、ここでは、全く同じフォーム画面について「コンポーネント化していない状態」から「適切にコンポーネント化された状態」まで3段階に進化させたコードを紹介していきます。

3つのコードを順番にコピペして動かし、見比べながらコンポーネント分割のポイントを掴んでみてください。「ここがこう変わったのか」や「ココとココがつながっているのか」という対応関係や規則性を読み取って、理解の助けにしてもらえればと思います。

コンポーネント化とは

Next.js(React)でのコンポーネント化とは、UIの一部を再利用可能な部品として切り出すことです。レゴブロックのように、小さな部品を組み合わせて大きなアプリケーションを作るイメージです。コンポーネントを適切に分割すると、再利用性や保守性、可読性が向上し、チーム開発では分担作業がしやすくなります😄

分からないことがでてきたら…

各コードを見比べるなかで「疑問に思うこと」や「解釈・理解に自信が持てないこと」がでてきたときは、次のようなプロンプトを使って生成AIに質問してみてください。

私はNext.jsの初心者です。【このブログ記事のURL】で解説されていることについて質問があります。記事のなかの「Step.2」の「PostForm.tsx」で【疑問点や不明点を記述】。丁寧に解説してください。

私はNext.jsの初心者です。【このブログ記事のURL】の「Step.1」で示されるコードから、「Step.2」に示されるコードに書き換える手順をステップバイステップで解説してください。また、コツやTipsがあれば教えてください。

Step.1 コンポーネント化していない状態

まずは、すべてのコードが1つのファイルに書かれた状態からスタートします。次のような「テキストボックス」と「ボタン」を持ったフォーム画面を構成するコードを扱っていきます。

初心者向け!3つのパターンのコード比較で理解するNext.jsのコンポーネント分割|ShiftBブログ

フォルダ構成

📂app/step1
 └─📑page.tsx

page.tsx

"use client";

import { useState } from "react";

const Page: React.FC = () => {
  const [name, setName] = useState<string>("");
  const [address, setAddress] = useState<string>("");
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const requestData = { name, address };
    setIsSubmitting(true);
    // (APIエンドポイントにPOSTリクエストを送信する処理がここにあると仮定)
    alert(`${JSON.stringify(requestData)} を送信しました。`);
    setIsSubmitting(false);
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <div className="mt-8 flex flex-col items-center gap-y-2">
          <div className="flex flex-col">
            <label htmlFor="name">名前</label>
            <input
              id="name"
              type="text"
              className="mb-2 rounded border-2"
              value={name}
              disabled={isSubmitting}
              onChange={(e) => setName(e.target.value)}
            />
          </div>
          <div className="flex flex-col">
            <label htmlFor="address">住所</label>
            <input
              id="address"
              type="text"
              className="mb-2 rounded border-2"
              value={address}
              disabled={isSubmitting}
              onChange={(e) => setAddress(e.target.value)}
            />
          </div>
          <button
            type="submit"
            className="rounded bg-blue-500 px-12 py-1 text-white"
            disabled={isSubmitting}
          >
            <span className="mr-1">🚀</span>送信
          </button>
        </div>
      </form>
    </div>
  );
};

export default Page;

このコードは問題なく動作しますが、すべてが1つのコンポーネント(page.tsx)に詰め込まれています。

この先、フォームの項目(例えば、年齢や電話番号など)が追加されると、似たようなコードの繰り返しで可読性が低下し、処理の見通しも悪くなります。また、他のページで同じようなフォームが必要になったときに、再利用や共通化が難しく、保守や改修の手間が大きくなるという技術的負債を抱えることになります。

例えば「すべてのフォームで入力欄のデザインをそろえたい」と思ったとき、各ページにバラバラにUI定義が書かれていると、何度も同じ修正をする必要があり、手間もミス(修正漏れなど)も増えてしまいます。

Step.2 フォーム全体をコンポーネント化

まずは、フォーム部分を1つのコンポーネント(PostForm.tsx)として切り出してみます。

  • Step.1 のコードと見比べながら、どの処理が親(page.tsx)に残り、どの処理が子(PostForm.tsx)へ分離されたのかを意識して読み進めてみてください。

  • page.tsx から PostForm.tsx に、どのように情報(props)が渡されているかに注目してみましょう。

    • 特に、(=名前や住所などのデータ)と、関数(=データを変更したり送信したりの操作をする関数)が、interface Props {…}で、それぞれ、どのように型定義されているかに注意してください。

フォルダ構成

📂app/step2
 ├─📑page.tsx
 └─📂_components
    └─📑PostForm.tsx

page.tsx

"use client";

import { useState } from "react";
import PostForm from "./_components/PostForm";

const Page: React.FC = () => {
  const [name, setName] = useState<string>("");
  const [address, setAddress] = useState<string>("");
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const requestData = { name, address };
    setIsSubmitting(true);
    // (APIエンドポイントにPOSTリクエストを送信する処理がここにあると仮定)
    alert(`${JSON.stringify(requestData)} を送信しました。`);
    setIsSubmitting(false);
  };

  return (
    <div>
      <PostForm
        propNameLabel="名前"
        propName={name}
        propSetName={setName}
        propAddressLabel="住所"
        propAddress={address}
        propSetAddress={setAddress}
        propIsSubmitting={isSubmitting}
        propHandleSubmit={handleSubmit}
      />
      {/**
       * ⚠️ 通常、上記のような "propName" といった名前はつけません!普通は "name" とします。
       *
       * でも初心者だと「これ、親コンポーネントと子コンポーネントのどっちの name?」ってなりがちなので、
       * あえて props 側(=子コンポーネント側)の変数名の頭に "prop" をつけて区別しやすくしています。
       */}
    </div>
  );
};

export default Page;

PostForm.tsx

"use client";

import { Dispatch, SetStateAction } from "react";

interface Props {
  propNameLabel: string;
  propName: string;
  propSetName: Dispatch<SetStateAction<string>>;
  // propsSetName: (value: string) => void; // これでもOK

  propAddressLabel: string;
  propAddress: string;
  propSetAddress: Dispatch<SetStateAction<string>>;
  // propsSetAddress: (value: string) => void; // これでもOK

  propIsSubmitting: boolean;
  propHandleSubmit: (e: React.FormEvent) => void;
}

/**
 * ⚠️ 通常の開発では、"propName" のような名前は使わず、単に "name" などとします。
 *
 * ただし初心者にとっては「この name は親?子?」と混乱しやすいため、
 * props 側(=子コンポーネント側)に "prop" をつけて、区別を明示しています。
 */

const PostForm: React.FC<Props> = ({
  propName,
  propSetName,
  propNameLabel,
  propAddress,
  propSetAddress,
  propAddressLabel,
  propIsSubmitting,
  propHandleSubmit,
}) => {
  return (
    <div>
      <form onSubmit={propHandleSubmit}>
        <div className="mt-8 flex flex-col items-center gap-y-2">
          <div className="flex flex-col">
            <label htmlFor="name">{propNameLabel}</label>
            <input
              id="name"
              type="text"
              className="mb-2 rounded border-2"
              value={propName}
              disabled={propIsSubmitting}
              onChange={(e) => propSetName(e.target.value)}
            />
          </div>
          <div className="flex flex-col">
            <label htmlFor="address">{propAddressLabel}</label>
            <input
              id="address"
              type="text"
              className="mb-2 rounded border-2"
              value={propAddress}
              disabled={propIsSubmitting}
              onChange={(e) => propSetAddress(e.target.value)}
            />
          </div>
          <button
            type="submit"
            className="rounded bg-blue-500 px-8 py-1 text-white"
            disabled={propIsSubmitting}
          >
            <span className="mr-1">🚀</span>送信
          </button>
        </div>
      </form>
    </div>
  );
};

export default PostForm;

ポイント解説

PostForm.tsxPropsの型定義には Dispatch<SetStateAction<string>>という型が登場します。これは、以下のようにVSCodeでpage.tsxsetNameにカーソルを合わせて表示される型情報とあわせていけばOKです。

初心者向け!3つのパターンのコード比較で理解するNext.jsのコンポーネント分割|ShiftBブログ

エラーを体験して慣れておこう🐛

page.tsxのなかの <PostForm …/> で、propNameLabel="名前"をコメントアウトすると(VSCode上で)どのようなエラーが表示されるかを確認・把握しておいてください。
このエラーは、コンポーネントを分割していくときに、頻繁に遭遇する典型的なエラーになります。エラーは「どうして起きるのか?」をセットで理解しておくと、今後、開発作業がグッと楽になります。

定着確認(練習問題1)

page.tsxに、以下のように「電話番号」のステート(文字列)を追加して、それを入力してもらうフィールドを追加してみてください。

const [tel, setTel] = useState<string>("");
  • 同じ文字列型のステートであるnameaddress の処理を参考にしましょう。

  • page.tsxPostForm.tsx も書き換えが必要です。

  • 書き換えたら、実際に「送信」ボタンを押して動作を確認してみましょう。

定着確認(練習問題2)

page.tsxに、以下のように「年齢」のステート(数値型)を追加して、それを入力してもらうフィールドを追加してみてください。

const[age, setAge] = useState<number>(20);
  • Propsの型定義などで、数値型(number)であることに注意しましょう。

Step.3 より細かくコンポーネント化

さらに細かく分割して、再利用性を高めてみます。

フォルダ構成

📂app/step3
 ├─📑page.tsx
 └─📂_components
    ├─📑SubmitButton.tsx
    └─📑TextField.tsx

page.tsx

"use client";

import { useState } from "react";
import TextField from "./_components/TextField";
import SubmitButton from "./_components/SubmitButton";

const Page: React.FC = () => {
  const [name, setName] = useState<string>("");
  const [address, setAddress] = useState<string>("");
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const requestData = { name, address };
    setIsSubmitting(true);
    // (APIエンドポイントにPOSTリクエストを送信する処理がここにあると仮定)
    alert(`${JSON.stringify(requestData)} を送信しました。`);
    setIsSubmitting(false);
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <div className="mt-8 flex flex-col items-center gap-y-2">
          <TextField
            label="名前"
            value={name}
            setValue={setName}
            isSubmitting={isSubmitting}
          />
          <TextField
            label="住所"
            value={address}
            setValue={setAddress}
            isSubmitting={isSubmitting}
          />
          <SubmitButton isSubmitting={isSubmitting}>
            <span className="mr-1">🚀</span>送信
          </SubmitButton>
        </div>
      </form>
    </div>
  );
};

export default Page;

TextField.tsx

"use client";

import { Dispatch, SetStateAction } from "react";

interface Props {
  label: string;
  value: string;
  setValue: Dispatch<SetStateAction<string>>;
  isSubmitting: boolean;
}

const TextField: React.FC<Props> = ({
  label,
  value,
  setValue,
  isSubmitting,
}) => {
  return (
    <div className="flex flex-col">
      <label htmlFor={label}>{label}</label>
      <input
        id={label}
        type="text"
        className="mb-2 rounded border-2"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        disabled={isSubmitting}
      />
    </div>
  );
};

export default TextField;

SubmitButton.tsx

"use client";

import { ReactNode } from "react";

interface Props {
  isSubmitting: boolean;
  children: ReactNode;
}

const SubmitButton: React.FC<Props> = ({ children, isSubmitting }) => {
  return (
    <button
      type="submit"
      className="rounded bg-blue-500 px-8 py-1 text-white"
      disabled={isSubmitting}
    >
      {children}
    </button>
  );
};

export default SubmitButton;

SubmitButton.tsx コンポーネントでは、「🚀送信」という情報をchildren という特別な props を使って受け取っています。詳しい解説は生成AIに譲ります(手抜き💦。

Next.js(React)でコンポーネントを分割する際、props のなかに children というものを使っている例を見かけます。この children という仕組みについて、具体的にどんな場面で使われていて、どういう役割を持っているのかを、初心者にも分かるように丁寧に説明してください。

特に、通常の props として「値」を渡すケースとどんな違いがあるのか、実際のコード例とともに教えてください。

定着確認(練習問題3)

改行付きのテキストが扱えるようなTextAreaField.tsx を作成してみましょう。そして、次のような自己紹介(about)が入力できるフィールドをフォームにしてみましょう。

const [about, setAbout] = useState<string>("");
  • textareaタグを使って実装しましょう。

  • 既存のTextField.tsx を参考に実装してみましょう。

おまけ:オフ会参加の報告

2025年8月2日、ShiftB 大阪オフ会(オフラインもくもく会)に参加してきました!

企画してくださった tomoe さん、参加者の皆さん、ありがとうございました✨。本記事は、参加記念の投稿となります。

シェア!

Threads
Loading...
記事一覧に戻る
Threads
0