Next.jsで学ぶReact講座(完全初心者向け・30日)演習問題・課題 | 第4章:実用的React – 状態設計

React Next.js
スポンサーリンク

この課題のねらい

ここは「React を“なんとなく書ける”から、“設計して書ける”に変わる」ポイントです。
テーマは 状態設計(state をどこに持つか)
コンポーネントを増やしていくと必ずぶつかるのが、

  • どのコンポーネントがどの state を持つべきか
  • 親から子に何を渡すべきか
  • そもそもこの state、本当に必要か

という問題です。ここを整理できるようになると、一気にコードがスッキリします。


状態設計の基本発想

「誰がその情報を知っているべきか?」で考える

state の持ち場所を考えるときの軸はシンプルで、

「この情報を知っている必要があるコンポーネントはどれか?」

です。

  • その情報を使うコンポーネントが 1 つだけなら、そのコンポーネントが持てばいい。
  • 複数のコンポーネントで共有したいなら、「共通の親」に state を持たせて、props で渡す。

この「共通の親に state を引き上げる」ことを、よく state のリフトアップ と呼びます。


例題:Todoリストの状態設計

コンポーネント構成をざっくり決める

シンプルな Todo アプリを例にします。
構成イメージはこんな感じです。

  • TodoApp(全体をまとめる親)
    • TodoInput(新しい Todo を入力するフォーム)
    • TodoList(Todo の一覧表示)
      • TodoItem(1件分の Todo 表示)

ここで考えたいのは、

  • Todo の配列(todos)をどこに持つか
  • 入力中のテキスト(inputValue)をどこに持つか

です。

stateの持ち場所を整理する

結論から言うと、こうなります。

  • todos(Todo の配列)
    TodoApp が持つ(一覧も追加も削除も、全部アプリ全体の話だから)
  • inputValue(入力中のテキスト)
    TodoInput が持つ(フォームの中だけで完結しているから)

図にすると、こんなイメージです(テキストで図を書くと):

TodoApp
  ├─ state: todos
  ├─ TodoInput
  │     └─ state: inputValue
  └─ TodoList
        └─ TodoItem(複数)

TodoApp は「アプリ全体の状態(todos)」を管理する親。
TodoInput は「フォームの入力状態」を自分で持つ子。
TodoListTodoItem は、todos を props として受け取って表示するだけの“表示専用コンポーネント”にできます。


親子関係を図に書く(もう少し具体的に)

コードと図を対応させてみる

ざっくりしたコード例も見てみましょう。

// TodoApp.tsx
"use client";

import { useState } from "react";
import { TodoInput } from "./TodoInput";
import { TodoList } from "./TodoList";

type Todo = {
  id: number;
  title: string;
};

export default function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);

  function handleAddTodo(title: string) {
    setTodos((prev) => [
      ...prev,
      { id: Date.now(), title },
    ]);
  }

  return (
    <main style={{ padding: "24px" }}>
      <h1>Todo アプリ</h1>
      <TodoInput onAdd={handleAddTodo} />
      <TodoList todos={todos} />
    </main>
  );
}
TSX
// TodoInput.tsx
"use client";

import { useState } from "react";

type TodoInputProps = {
  onAdd: (title: string) => void;
};

export function TodoInput({ onAdd }: TodoInputProps) {
  const [value, setValue] = useState("");

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!value.trim()) return;
    onAdd(value.trim());
    setValue("");
  }

  return (
    <form onSubmit={handleSubmit} style={{ marginTop: "12px" }}>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Todo を入力"
      />
      <button type="submit" style={{ marginLeft: "8px" }}>
        追加
      </button>
    </form>
  );
}
TSX
// TodoList.tsx
type Todo = {
  id: number;
  title: string;
};

type TodoListProps = {
  todos: Todo[];
};

export function TodoList({ todos }: TodoListProps) {
  return (
    <ul style={{ marginTop: "16px" }}>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}
TSX

この構成を図にすると、こうなります。

TodoApp
  ├─ state: todos
  ├─ handleAddTodo(title)
  ├─ TodoInput
  │     ├─ state: value
  │     └─ props: onAdd(= handleAddTodo)
  └─ TodoList
        └─ props: todos

ここでのポイントは、

  • 「Todo の配列」はアプリ全体で共有したいので、親の TodoApp が持つ。
  • 子の TodoInput は、「入力中のテキスト」だけを自分で持つ。
  • TodoList は state を持たず、「渡された todos を表示するだけ」のコンポーネントにする。

この「どのコンポーネントが何を知っているか」を図に書いて整理するのが、状態設計の第一歩です。


挑戦課題:不要なstateを削除する

「それ、本当にstateである必要ある?」を疑う

React を書き慣れてくると、つい何でもかんでも state に入れがちです。
でも、次のようなものは state にしなくていい(=不要な state) です。

  • props から計算で導ける値
  • 他の state から常に計算できる値
  • 一時的な表示用で、レンダリングのたびに計算しても問題ないもの

例を見てみましょう。

const [todos, setTodos] = useState<Todo[]>([]);
const [todoCount, setTodoCount] = useState(0);

useEffect(() => {
  setTodoCount(todos.length);
}, [todos]);
TSX

これは「Todo の数」を別 state として持っていますが、
実は todos.length を毎回計算すれば済むので、todoCount は不要です。

不要なstateを削除してスッキリさせる

上の例は、こう書き換えられます。

const [todos, setTodos] = useState<Todo[]>([]);

// どこかの JSX 内
<p>Todo の数: {todos.length}</p>
TSX

これで、

  • state が 1 つ減る
  • useEffect も不要になる
  • 「状態の源泉」が todos だけになり、バグの余地が減る

というメリットがあります。

状態設計で大事なのは、

「この情報の“本当の元”はどこか?」を一つに決めること

です。

  • todos が「真の状態」
  • todos.length は「そこから計算される派生情報」

なので、state として持つのは todos だけで十分、という判断になります。


状態設計のチェックリスト的な考え方

状態設計をするとき、こんな問いを自分に投げてみてください。

  • この state を知る必要があるコンポーネントはどれか?
    → その中で一番上の親に持たせる。
  • この値は、他の state や props から計算で出せないか?
    → 出せるなら state にしない。
  • 子コンポーネントが「ただ表示するだけ」で済むようにできないか?
    → できるなら、親に state を寄せて、子には props だけ渡す。

このあたりを意識し始めると、

  • state の数が減る
  • useEffect の数も減る
  • 「どこを見れば何が分かるか」が明確になる

ので、コードの見通しが一気によくなります。


まとめ:状態設計でつかんでほしいこと

この課題で本当に持ち帰ってほしいのは、次の感覚です。

  • state の持ち場所は「その情報を誰が使うか」で決める。
  • 複数のコンポーネントで共有したい state は、共通の親に持たせて props で渡す。
  • 図(テキストでもOK)に「親子関係」と「誰がどの state を持つか」を書き出すと、頭が整理される。
  • 他の state や props から計算できるものは、state にしない(不要な state を削る)。

タイトルとURLをコピーしました