【React】「困難は分割せよ」―― 複雑な画面は小さな機能に分けて実装しよう。

**「Divide and Conquer / 分割統治法」** という解法（アルゴリズムの話でよく出てきますね）は、**「困難は分割せよ」** として知られる、デカルトが『方法序説』で提唱した思考法が元になっています[^1]。

[^1]: 「ローマ帝国を参考にダイクストラが分割統治法を提唱したんじゃないの？」と思った方へ -- [分割統治（Divide and Conquer）について | Ouobpo ](https://ameblo.jp/ouobpo/entry-10052917344.html)

>
> 第二は、わたしが検討する難問の一つ一つを、できるだけ多くの、しかも問題をよりよく解くために必要なだけの小部分に分割すること。
> ―― 岩波文庫 『方法序説』 デカルト著 谷川多佳子訳 
>

同様に、React で多機能で複雑な画面を作りたい時、それを**小さな機能の集まりと捉えて、それぞれをコンポーネントにする**ことで、開発がラクになることがあります。

![画面の状態とアクションの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/fe951b10-e32a-2ee5-009b-8dfd4a76da01.png)

フロントエンドの複雑さや、デザインのための Atomic Design という考え方の影響、または `/scripts` のように分ける習慣の名残なのか、フロントエンド開発者には、《再利用のためにコンポーネントを作る》という思い込みがあります。（もしくは、かつてありました。）

それが間違いというわけではありませんが、再利用*だけ*に留まるのは勿体ないです。

《**「再利用のため」だけでなく「パーツに分けて考える」ためにコンポーネントを分割する**》という考え方が、React 公式ドキュメントでも推奨されています。

> ほとんどの React アプリでは隅から隅までコンポーネントが使われます。つまり、ボタンのような再利用可能なところでのみ使うのではなく、サイドバーやリスト、最終的にはページ本体といった大きなパーツのためにも使うのです。コンポーネントは、1 回しか使わないような UI コードやマークアップであっても、それらを整理するための有用な手段です。
>
> https://ja.react.dev/learn/your-first-component#components-all-the-way-down  
> React 公式ドキュメント「初めてのコンポーネント > 隅から隅までコンポーネント」

---

このような方法を取ると嬉しい副産物もあります。

たとえば、 `newItemTitle` →  `title` のように名前を短縮できるようになって、コードを読解するときの認知負荷が低くなります。 (直接 export するモノではそうはいかない)

しかも、再レンダリングが頻繁に起こる（入力欄の onChange ）範囲を子コンポーネント内に限定できるので、**結果として再描画を大きく減らす**ことにも繋がります。

それでは、「複雑な画面」のいちばん簡単な例を題材にして、コード分割の際の考え方を垂れ流そうと思います。

# 画面の大まかな仕様

以下のような仕様のページを作るとします。

▼ 完成イメージ ▼
![完成イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/98550a14-7e48-05a3-65f3-032445411491.png)


- 「タスクの新規作成」
  - 「件名」入力欄
  - 「作成」ボタン
    - **押下時のアクション** 
      - タスクを新規作成。
      - **title** は「件名」の入力の内容とする。
      - 「件名」入力欄のリセットは行わない。
- 「全て完了」ボタン
  - 全てのタスクの **status** を "done" に変更
- タスク一覧の各アイテムについて
  - 「完了」ボタン / 「DONE」 ボタン
    - 「完了」ボタン
      - **status** が "todo" ならこちらを表示
      - **押下時のアクション**
        - **status** を "done" に変更
    - 「DONE」ボタン（非活性）
      - **status** が "done" ならこちらを表示
  - タスク名 
    - **title** を表示

# 実際に分けてみる

まず、この画面が保持するべき状態をまとめると以下のようになります。

- **タスクの一覧**
  - できるアクション : 
    - タスクを追加する
    - １件のタスクを指定して、**status** を "done" に変更する
    - タスク全件の **status** を "done" に変更する
  - 入力欄の状態と比べて永続的
- **「件名」入力欄の状態** (タスク新規作成)
  - できるアクション : 
    - 入力欄への入力をステートに反映する 
  - 一時的に利用される
  - 頻繁に更新される


 「タスク新規作成」の箇所は、新しいタスクを作るための**一時的な状態**を保持する場所です。しかも、(Controlled Componentの場合、)頻繁に再レンダリングを引き起こします。

それ以外のページ内容は、タスクを新規作成するための**入力欄の状態**をいっさい参照していません。

以上のことから、この「タスク新規作成」部分は、真っ先にコンポーネントとして分割されるべき部品だと考えます。

―― 私が脳内でフワッと考えていることを言葉に起こしたので、長ったらしくて理解しづらいですね。簡単な図式で下に示します。このような図を起こすと思考が整理されるので良いと思います。


![画面の状態とアクションの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/fe951b10-e32a-2ee5-009b-8dfd4a76da01.png)

- #から始まるのが各コンポーネントのステートです
  - 便宜上、カスタムフックで管理しているものを含んでいます。
- 実線の矢印は、 **アクション** がステート更新を引き起こす向きです
  - 「アクションの発生元 → ステートが更新されるコンポーネント」
  - コールバック Prop を渡す向きと逆です
  - **「子 → 親」 または　「コンポーネント内で完結」**
- 親が再レンダーされるたびに、子も再レンダーされます
  - React.memo() で防止できます
  - 親の再レンダー時ごとに 値が Prop 経由で **「親→子」** の向きに渡されるのを、ここでは **「値の伝播」** と呼んでいます

# 分割したコードの概要

ここではファイル・コンポーネントの概要のみを並べます。コード本文はこの記事の末尾に載せています。

コンポーネント名は **"Todo～"** と、feature の名前から始まるようにしています。共通のUIパーツではないからです。（TodoPage コンポーネントも同じ feature ディレクトリに入れて、 [eslint-plugin-import-access](https://zenn.dev/uhyo/articles/eslint-plugin-import-access) を活用してパッケージプライベート化する、という手もあります。）

ファイルを置くディレクトリも `components/` などではなく、目的駆動で分けられたディレクトリに入れています。




- src/pages/todo/
  - index.tsx (TodoPage コンポーネント)
    - 下記のコンポーネント・フックを利用して画面全体を組み立てる
- src/features/todo/
  - TodoCreateArea.tsx
    - 「タスク新規作成」の部分
    - コールバック Props : **onCreateTodoItem**
    - その他の Props : **なし**
    - ステート : **title**
  - TodoItem.tsx 
    - タスク一覧の各アイテム
    - コールバック Props : **onMarkAsDone**
    - その他の Props : **itemId, status, title**
    - ステート : **なし**
  - useTodoItems.ts
    - タスクの一覧とそれを更新する関数群を提供するフック

先程のコンポーネント依存関係図を実際のコンポーネント, Prop 名に直すとこのようになります。

![画面の状態とアクションの図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/cf3f2fd2-3478-3523-19fe-753e3c9c7be9.png)

## 余談 : ディレクトリ構造について

`features/`のような目的駆動のディレクトリが無いと、今回必要な「大きな画面を小さな機能に分ける」ためのコンポーネントを作るのが困難になってしまいます。

なので、目的駆動パッケージングを取り入れることをオススメします。

https://qiita.com/honey32/items/dbf3c5a5a71636374567

# 再レンダリングも抑制できた 👏

嬉しい副産物があります。

たびたび出てくる図ではアクションの起きる頻度を**矢印の太さ**で表していました。 そして、細い実線矢印は親 (**TodoPage**) に影響を与えますが、 **setTitle** の太い実線矢印のアクションは **TodoCreateArea** に閉じていて親には影響しません。

そのため、頻繁に起こるアクションが広範囲の再レンダリングを防ぐことが出来ているのです👏👏  ページ下部のソース全文を確認すれば分かりますが、 `React.memo()` は使用していません。


## 分割前の再レンダリングのようす

![todo-sample-unrefactored.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/62cbf8b9-fc7d-3c01-d9bf-d56d997c93f1.gif)

## 分割後の再レンダリングのようす

![todo-sample-refactored.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234029/39ebf39c-257e-4743-86f9-a69d078ed932.gif)

# おわりに

このように、 「状態やアクションに着目してコンポーネントに分ける」ことによって **「正しく動くだけでなく綺麗」なコード** が書けるようになります。

この記事が皆さんの解決すべき**難問を乗り越える力**となることを願っています。

# つづき

ふつう「作成」ボタンを押したら「件名」欄をリセットするでしょ？と思ったら続きも見てやってください。

https://qiita.com/honey32/items/64435d17bfaa2cb10b88

# 関連記事

▼ 元ネタになった記事

https://zenn.dev/azukiazusa/articles/react-rerender-patterns

▼ React の公式ドキュメントにも、同様の内容がまとめられています。

https://ja.reactjs.org/docs/thinking-in-react.html

▼ 小さく分けて書くメリットについては、こちらの記事も参考になります

https://scrapbox.io/mrsekut-p/1%E3%81%A4%E3%81%AEfile%E3%81%AB%E3%83%80%E3%83%A9%E3%83%80%E3%83%A9%E6%9B%B8%E3%81%8F%E3%81%93%E3%81%A8%E3%81%AE%E5%BC%8A%E5%AE%B3

▼ (私の記事)状態を整理する所で「えっ？〇〇はステートじゃないの？」って思った人へ

https://qiita.com/honey32/items/58e56e407d4d87e294a4

▼ (私の記事)コンポーネントを細かく分割しやすいディレクトリ構造について

https://qiita.com/honey32/items/dbf3c5a5a71636374567

▼ コンポーネントの分割については、こちらの記事も有益

https://qiita.com/waicode/items/0b144039bf4b4eddcfee

# 付録: 実際のコード

使用しているライブラリ: 

- `next` - 13.1.2 
  - /pages ディレクトリを使用
- `react`, `@react/dom` - 18.2.0
- `@mantine/core` - 5.10.1
- `@emotion/react` - 11.10.5
  - Mantine の依存  


## TodoPage コンポーネント

ページ本体のコンポーネントです。

コードを適度に短くしつつ過度な共通化・抽象化を避けているため、このページがどんな機能から成るのかコードだけでも分かりやすいのではと思います。

```tsx:src/pages/todo/index.tsx
import { NextPage } from "next";
import { Box, Button, Container, Divider } from "@mantine/core";

import TodoCreateArea from "@/features/todo/TodoCreateArea";
import TodoItem from "@/features/todo/TodoItem";
import useTodoItems from "@/features/todo/useTodoItems";

const TodoPage: NextPage = () => {
  const todoItems = useTodoItems();

  return (
    <Container>
      <Box my="lg">
        <TodoCreateArea onCreateTodoItem={todoItems.create} />
      </Box>

      <Button variant="outline" onClick={todoItems.markAllAsDone}>
        全て完了する
      </Button>

      <Box mt="md">
        {todoItems.items.map((item) => (
          <TodoItem
            key={item.id}
            itemId={item.id}
            title={item.title}
            status={item.status}
            onMarkAsDone={todoItems.markSingleItemAsDone}
          />
        ))}
        <Divider />
      </Box>
    </Container>
  );
};

export default TodoPage;
```

## useTodoItems フック

ここでは簡単のため、 useState + useCallback でタスクたちのデータを保持し、更新するための関数を提供していますが、 **実際のアプリケーションでは \@vercel/swr や TanStack Query を使って**バックエンドと通信することが多いと思います。

:::note warn
今回は「一つの画面」だから一つのカスタムフックにまとめた訳ではありません。そのような分割はカスタムフックをブラックボックス化するので**私は推奨しません**。

あくまで「TodoリストのCRUD操作」という**ひとまとまりの関心事**だからカスタムフックにまとめています。
:::

「export する関数には型定義が必要」といった規約を eslint で設定しているケースに配慮して、 `ReturnValue` で先に返り値の型を宣言してから、フック内でその型を引用して書いています。

<details><summary>src/features/todo/useTodoItems.ts のソースコード</summary><div>

```ts:src/features/todo/useTodoItems.ts
import { useCallback, useState } from "react";

type TodoItem = {
  title: string,
  id: number,
  status: "todo" | "done",
};

type ReturnValue = {
  items: TodoItem[],
  create: (newItem: Pick<TodoItem, "title">) => void,
  markSingleItemAsDone: (itemId: TodoItem["id"]) => void,
  markAllAsDone: () => void,
};

const useTodoItems = (): ReturnValue => {
  const [items, setItems] = useState<ReturnValue["items"]>([]);

  const create: ReturnValue["create"] = useCallback((newItem) => {
    setItems((prev) => [
      ...prev,
      {
        title: newItem.title,
        status: "todo",
        id: Date.now(),
      },
    ]);
  }, []);

  const markSingleItemAsDone: ReturnValue["markSingleItemAsDone"] = useCallback(
    (itemId) => {
      setItems((prev) =>
        prev.map((i) => (i.id === itemId ? { ...i, status: "done" } : i))
      );
    },
    []
  );

  const markAllAsDone: ReturnValue["markAllAsDone"] = useCallback(() => {
    setItems((prev) => prev.map((item) => ({ ...item, status: "done" })));
  }, []);

  return { items, create, markSingleItemAsDone, markAllAsDone };
};

export default useTodoItems;

```

</div></details>

## TodoCreateArea コンポーネント

文法的には `名詞+動詞` でおかしいですが、検索・補完のしやすさを考慮して **Todo** をプレフィックスにしています。

<details><summary>src/features/todo/TodoCreateArea.tsx のソースコード</summary><div>

```tsx:src/features/todo/TodoCreateArea.tsx
import { useState } from "react";
import { Button, Grid, Input, Paper, Text, Title } from "@mantine/core";

type Props = {
  onCreateTodoItem: (newItem: { title: string }) => void,
};

const TodoCreateArea: React.FC<Props> = ({ onCreateTodoItem }) => {
  const [title, setTitle] = useState<string>("");

  return (
    <Paper shadow="md" p="sm">
      <Title order={2}>タスクの新規作成</Title>
      <Grid>
        <Grid.Col span="content" style={{ alignSelf: "center" }}>
          <Text>件名: </Text>
        </Grid.Col>
        <Grid.Col span="auto">
          <Input
            name="newItemTitle"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
          />
        </Grid.Col>
        <Grid.Col span="content">
          <Button onClick={() => onCreateTodoItem({ title: title })}>
            作成
          </Button>
        </Grid.Col>
      </Grid>
    </Paper>
  );
};

export default TodoCreateArea;

```

</div></details>

## TodoItem コンポーネント

<details><summary>src/features/todo/TodoItems.tsx のソースコード</summary><div>

```tsx:src/features/todo/TodoItem.tsx
import { Button, Divider, Grid, Text } from "@mantine/core";

type Props = {
  itemId: number,
  status: "todo" | "done",
  title: string,
  onMarkAsDone: (id: number) => void,
};

const TodoItem: React.FC<Props> = ({ itemId, status, title, onMarkAsDone }) => {
  return (
    <>
      <Divider />
      <Grid p="sm">
        <Grid.Col span="content">
          {status === "todo" ? (
            <Button variant="outline" onClick={() => onMarkAsDone(itemId)}>
              完了
            </Button>
          ) : (
            <Button disabled>DONE</Button>
          )}
        </Grid.Col>

        <Grid.Col span="auto">
          <Text size={18} weight="bold">
            {title}
          </Text>
        </Grid.Col>
      </Grid>
    </>
  );
};

export default TodoItem;

```

</div><details>