前言

こんにちは、Watanabe Jin(@Sicut_study)です。
Reactを書いていると時々思うんです。

私はReactを活かしたコードをかけているのだろうか?

Reactにはデザインパターンとしていくつかの代表的なものが存在します。

  • HOC(高階コンポーネント) ※ 現在は不要
  • React Hooks
  • カスタム Hooks
  • Providerパターン
  • Container Presenterパターン
  • Render Props
  • Controlled Component
  • Extensible Styles
  • State Initializer
  • State Reducer
  • 条件レンダリング
  • Compound Component
    ...etc

どうでしょうか?
デザインパターンをみて実装のイメージがつくものは案外少ない人も多いかと思います。
React Hooksや条件レンダリングは割と利用します。

他のパターンは実装の機会も少ないので、そもそも触れる機会がないです。
もし他のパターンが最適な場合でも選択肢にならないでしょう。(知らないとできない)

ということで今回は絶対に知っておいてほしいReactデザインパターンを「タスク管理アプリ」を実装しながら学んでいきたいと思います。

Reactをさらに使いこなしたい!という人はぜひ最後までやってみてください!

已準備影片教材

こちらの教材にはより詳しく解説した動画もセットでご用意しています。
テキスト教材でわからない細かい箇所があれば動画も活用ください。

適合對象

  • Reactを書いたことがある人
  • Reactをもっと活かしたコードを書きたい人
  • 設計スキルを高めたい人

什麼是 React 設計模式?

そもそもReactデザインパターンとは...

Reactアプリケーション開発で繰り返し発生する課題を解決するための、検証済みの設計手法やベストプラクティスの集まりのこと

デザインパターンを利用することで以下のメリットがあります。

![image.png]()

代表的なデザインパターンを紹介します。
これらはReactを使う中で特に頻繁に用いられるプラクティスになります。

![image.png]()

Reactデザインパターンは、ただ単にコードを書くための「型」ではなく、 アプリケーションをより構造的、保守的、かつ効率的に開発するための、開発者コミュニティによって蓄積された「設計ノウハウ集」 です。
現代のWeb開発では、React Hooksのような新しい機能の登場により、パターンも進化し続けています。

今回はこの中でも普段はあまり利用しないけど知っておいてほしいものをアプリを作りながら学んでいきます。

本チュートリアルの構成について
各セクションでは、前のセクションのパターンを削除して新しいパターンに置き換えていきます。これは学習のためにパターンの違いを明確に理解するための構成です。実務ではこれらのパターンは排他的ではなく、Provider + カスタムフック + Presenter のように組み合わせて使うことが多いです。

1. 環境建置:建立基礎應用程式

安裝 Node.js(初學者指南)

まずはNode.jsがインストールされているか確認しましょう。ターミナル(コマンドプロンプト)で以下のコマンドを実行してください。

node <span>-v</span>
npm <span>-v</span>

建立專案

次に、Viteを使ってReact環境を作りましょう。

npm create vite@latest

Need to <span>install </span>the following packages:
[email protected]
Ok to proceed? <span>(</span>y<span>)</span> y

<span>></span> npx
<span>></span> <span>"create-vite"</span>

│
◇  Project name:
│  react-design
│
◇  Select a framework:
│  React
│
◇  Select a variant:
│  TypeScript
│
◇  Use rolldown-vite <span>(</span>Experimental<span>)</span>?:
│  No
│
◇  Install with npm and start now?
│  Yes
│
◇  Scaffolding project <span>in</span> /home/jinwatanabe/workspace/qiit/react-design...
│
◇  Installing dependencies with npm...

added 240 packages, and audited 241 packages <span>in </span>20s

48 packages are looking <span>for </span>funding
  run <span>`</span>npm fund<span>`</span> <span>for </span>details

found 0 vulnerabilities
│
◇  Starting dev server...

<span>></span> [email protected] dev
<span>></span> vite

  VITE v7.2.4  ready <span>in </span>446 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use <span>--host</span> to expose
  ➜  press h + enter to show <span>help</span>

http://localhost:5173を開くと以下の画面が表示されます。

![image.png]()

必要なアイコンライブラリをインストールします。作成されたプロジェクトディレクトリに移動してから別のターミナルを開いて以下のコマンドを実行してください。

<span>cd </span>react-design
npm <span>install </span>react-icons

インストールが完了したら、以下のコマンドで確認できます。

npm list react-icons

以下のような表示が出ればインストール成功です。

[email protected] C:\Users\ユーザー名\projects\react-design
└── [email protected]

VSCodeを開いてコードを書いていきましょう。
このチュートリアルではReactの基本的な部分に関しての説明は省略します。

src/index.css

<span>/* 全部刪除 */</span>

src/App.css

<span>/* src/App.css */</span>
<span>body</span> <span>{</span>
  <span>margin</span><span>:</span> <span>0</span><span>;</span>
  <span>font-family</span><span>:</span> <span>-</span><span>apple-system</span><span>,</span> <span>BlinkMacSystemFont</span><span>,</span> <span>'Segoe UI'</span><span>,</span> <span>Roboto</span><span>,</span> <span>Oxygen</span><span>,</span> <span>Ubuntu</span><span>,</span> <span>Cantarell</span><span>,</span> <span>'Open Sans'</span><span>,</span> <span>'Helvetica Neue'</span><span>,</span> <span>sans-serif</span><span>;</span>
  <span>-webkit-font-smoothing</span><span>:</span> <span>antialiased</span><span>;</span>
  <span>-moz-osx-font-smoothing</span><span>:</span> <span>grayscale</span><span>;</span>
  <span>background-color</span><span>:</span> <span>#f5f5f5</span><span>;</span>
<span>}</span>

<span>.app</span> <span>{</span>
  <span>max-width</span><span>:</span> <span>800px</span><span>;</span>
  <span>margin</span><span>:</span> <span>0</span> <span>auto</span><span>;</span>
  <span>padding</span><span>:</span> <span>2rem</span><span>;</span>
<span>}</span>

<span>.app-header</span> <span>{</span>
  <span>margin-bottom</span><span>:</span> <span>2rem</span><span>;</span>
  <span>text-align</span><span>:</span> <span>center</span><span>;</span>
<span>}</span>

<span>.app-header</span> <span>h1</span> <span>{</span>
  <span>margin</span><span>:</span> <span>0</span><span>;</span>
  <span>color</span><span>:</span> <span>#333</span><span>;</span>
<span>}</span>

<span>.tasks-container</span> <span>{</span>
  <span>background-color</span><span>:</span> <span>white</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>8px</span><span>;</span>
  <span>padding</span><span>:</span> <span>1.5rem</span><span>;</span>
  <span>box-shadow</span><span>:</span> <span>0</span> <span>2px</span> <span>4px</span> <span>rgba</span><span>(</span><span>0</span><span>,</span> <span>0</span><span>,</span> <span>0</span><span>,</span> <span>0.1</span><span>);</span>
<span>}</span>

<span>.task-form</span> <span>{</span>
  <span>display</span><span>:</span> <span>flex</span><span>;</span>
  <span>margin-bottom</span><span>:</span> <span>1.5rem</span><span>;</span>
  <span>gap</span><span>:</span> <span>0.5rem</span><span>;</span>
<span>}</span>

<span>.task-form</span> <span>input</span> <span>{</span>
  <span>flex</span><span>:</span> <span>1</span><span>;</span>
  <span>padding</span><span>:</span> <span>0.75rem</span><span>;</span>
  <span>border</span><span>:</span> <span>1px</span> <span>solid</span> <span>#ddd</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>4px</span><span>;</span>
  <span>font-size</span><span>:</span> <span>1rem</span><span>;</span>
<span>}</span>

<span>.task-form</span> <span>select</span> <span>{</span>
  <span>padding</span><span>:</span> <span>0.75rem</span><span>;</span>
  <span>border</span><span>:</span> <span>1px</span> <span>solid</span> <span>#ddd</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>4px</span><span>;</span>
  <span>font-size</span><span>:</span> <span>1rem</span><span>;</span>
<span>}</span>

<span>.task-form</span> <span>button</span> <span>{</span>
  <span>padding</span><span>:</span> <span>0.75rem</span> <span>1rem</span><span>;</span>
  <span>background-color</span><span>:</span> <span>#3498db</span><span>;</span>
  <span>color</span><span>:</span> <span>white</span><span>;</span>
  <span>border</span><span>:</span> <span>none</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>4px</span><span>;</span>
  <span>cursor</span><span>:</span> <span>pointer</span><span>;</span>
  <span>font-size</span><span>:</span> <span>1rem</span><span>;</span>
<span>}</span>

<span>.task-form</span> <span>button</span><span>:hover</span> <span>{</span>
  <span>background-color</span><span>:</span> <span>#2980b9</span><span>;</span>
<span>}</span>

<span>.task-list</span> <span>{</span>
  <span>list-style</span><span>:</span> <span>none</span><span>;</span>
  <span>padding</span><span>:</span> <span>0</span><span>;</span>
  <span>margin</span><span>:</span> <span>0</span><span>;</span>
<span>}</span>

<span>.task-item</span> <span>{</span>
  <span>display</span><span>:</span> <span>flex</span><span>;</span>
  <span>justify-content</span><span>:</span> <span>space-between</span><span>;</span>
  <span>align-items</span><span>:</span> <span>center</span><span>;</span>
  <span>padding</span><span>:</span> <span>0.75rem</span> <span>1rem</span><span>;</span>
  <span>margin-bottom</span><span>:</span> <span>0.5rem</span><span>;</span>
  <span>background-color</span><span>:</span> <span>#f9f9f9</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>4px</span><span>;</span>
  <span>box-shadow</span><span>:</span> <span>0</span> <span>1px</span> <span>2px</span> <span>rgba</span><span>(</span><span>0</span><span>,</span> <span>0</span><span>,</span> <span>0</span><span>,</span> <span>0.05</span><span>);</span>
<span>}</span>

<span>.task-item.completed</span> <span>.task-title</span> <span>{</span>
  <span>text-decoration</span><span>:</span> <span>line-through</span><span>;</span>
  <span>color</span><span>:</span> <span>#888</span><span>;</span>
<span>}</span>

<span>.task-priority</span> <span>{</span>
  <span>font-size</span><span>:</span> <span>0.8rem</span><span>;</span>
  <span>padding</span><span>:</span> <span>0.2rem</span> <span>0.5rem</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>3px</span><span>;</span>
  <span>color</span><span>:</span> <span>white</span><span>;</span>
  <span>margin-right</span><span>:</span> <span>0.5rem</span><span>;</span>
<span>}</span>

<span>.priority-low</span> <span>{</span>
  <span>background-color</span><span>:</span> <span>#27ae60</span><span>;</span>
<span>}</span>

<span>.priority-medium</span> <span>{</span>
  <span>background-color</span><span>:</span> <span>#f39c12</span><span>;</span>
<span>}</span>

<span>.priority-high</span> <span>{</span>
  <span>background-color</span><span>:</span> <span>#e74c3c</span><span>;</span>
<span>}</span>

<span>.task-actions</span> <span>{</span>
  <span>display</span><span>:</span> <span>flex</span><span>;</span>
  <span>gap</span><span>:</span> <span>0.5rem</span><span>;</span>
<span>}</span>

<span>.task-actions</span> <span>button</span> <span>{</span>
  <span>background</span><span>:</span> <span>none</span><span>;</span>
  <span>border</span><span>:</span> <span>none</span><span>;</span>
  <span>cursor</span><span>:</span> <span>pointer</span><span>;</span>
  <span>font-size</span><span>:</span> <span>1rem</span><span>;</span>
  <span>display</span><span>:</span> <span>flex</span><span>;</span>
  <span>align-items</span><span>:</span> <span>center</span><span>;</span>
  <span>padding</span><span>:</span> <span>0.25rem</span><span>;</span>
<span>}</span>

<span>.toggle-btn</span> <span>{</span>
  <span>color</span><span>:</span> <span>#27ae60</span><span>;</span>
<span>}</span>

<span>.delete-btn</span> <span>{</span>
  <span>color</span><span>:</span> <span>#e74c3c</span><span>;</span>
<span>}</span>

<span>.empty-message</span> <span>{</span>
  <span>text-align</span><span>:</span> <span>center</span><span>;</span>
  <span>color</span><span>:</span> <span>#888</span><span>;</span>
  <span>font-style</span><span>:</span> <span>italic</span><span>;</span>
<span>}</span>

次に、domainフォルダとTask.tsファイルを作成します。

<span>mkdir </span>src/domain
<span>touch </span>src/domain/Task.ts

src/domain/Task.ts

<span>export</span> <span>interface</span> <span>Task</span> <span>{</span>
  <span>id</span><span>:</span> <span>string</span><span>;</span>
  <span>title</span><span>:</span> <span>string</span><span>;</span>
  <span>completed</span><span>:</span> <span>boolean</span><span>;</span>
  <span>priority</span><span>:</span> <span>'</span><span>low</span><span>'</span> <span>|</span> <span>'</span><span>medium</span><span>'</span> <span>|</span> <span>'</span><span>high</span><span>'</span><span>;</span>
<span>}</span>

src/App.tsx

<span>import</span> <span>React</span><span>,</span> <span>{</span> <span>useState</span><span>,</span> <span>useEffect</span> <span>}</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>import</span> <span>{</span> <span>FaCheck</span><span>,</span> <span>FaTrash</span> <span>}</span> <span>from</span> <span>"</span><span>react-icons/fa</span><span>"</span><span>;</span>
<span>import</span> <span>"</span><span>./App.css</span><span>"</span><span>;</span>
<span>import</span> <span>type</span> <span>{</span> <span>Task</span> <span>}</span> <span>from</span> <span>"</span><span>./domain/Task</span><span>"</span><span>;</span>

<span>const</span> <span>App</span><span>:</span> <span>React</span><span>.</span><span>FC</span> <span>=</span> <span>()</span> <span>=></span> <span>{</span>
  <span>const</span> <span>[</span><span>tasks</span><span>,</span> <span>setTasks</span><span>]</span> <span>=</span> <span>useState</span><span><</span><span>Task</span><span>[]</span><span>></span><span>(()</span> <span>=></span> <span>{</span>
    <span>const</span> <span>savedTasks</span> <span>=</span> <span>localStorage</span><span>.</span><span>getItem</span><span>(</span><span>"</span><span>tasks</span><span>"</span><span>);</span>
    <span>return</span> <span>savedTasks</span> <span>?</span> <span>JSON</span><span>.</span><span>parse</span><span>(</span><span>savedTasks</span><span>)</span> <span>:</span> <span>[];</span>
  <span>});</span>

  <span>const</span> <span>[</span><span>title</span><span>,</span> <span>setTitle</span><span>]</span> <span>=</span> <span>useState</span><span>(</span><span>""</span><span>);</span>
  <span>const</span> <span>[</span><span>priority</span><span>,</span> <span>setPriority</span><span>]</span> <span>=</span> <span>useState</span><span><</span><span>Task</span><span>[</span><span>"</span><span>priority</span><span>"</span><span>]</span><span>></span><span>(</span><span>"</span><span>medium</span><span>"</span><span>);</span>

  <span>useEffect</span><span>(()</span> <span>=></span> <span>{</span>
    <span>localStorage</span><span>.</span><span>setItem</span><span>(</span><span>"</span><span>tasks</span><span>"</span><span>,</span> <span>JSON</span><span>.</span><span>stringify</span><span>(</span><span>tasks</span><span>));</span>
  <span>},</span> <span>[</span><span>tasks</span><span>]);</span>

  <span>const</span> <span>handleAddTask</span> <span>=</span> <span>(</span><span>e</span><span>:</span> <span>React</span><span>.</span><span>FormEvent</span><span>)</span> <span>=></span> <span>{</span>
    <span>e</span><span>.</span><span>preventDefault</span><span>();</span>
    <span>if </span><span>(</span><span>title</span><span>.</span><span>trim</span><span>())</span> <span>{</span>
      <span>const</span> <span>newTask</span><span>:</span> <span>Task</span> <span>=</span> <span>{</span>
        <span>// 在正式環境請使用 crypto.randomUUID() 或其他更安全的方法</span>
        <span>id</span><span>:</span> <span>Date</span><span>.</span><span>now</span><span>().</span><span>toString</span><span>(),</span>
        <span>title</span><span>:</span> <span>title</span><span>.</span><span>trim</span><span>(),</span>
        <span>completed</span><span>:</span> <span>false</span><span>,</span>
        <span>priority</span><span>,</span>
      <span>};</span>
      <span>setTasks</span><span>([...</span><span>tasks</span><span>,</span> <span>newTask</span><span>]);</span>
      <span>setTitle</span><span>(</span><span>""</span><span>);</span>
      <span>setPriority</span><span>(</span><span>"</span><span>medium</span><span>"</span><span>);</span>
    <span>}</span>
  <span>};</span>

  <span>const</span> <span>handleToggleTask</span> <span>=</span> <span>(</span><span>id</span><span>:</span> <span>string</span><span>)</span> <span>=></span> <span>{</span>
    <span>setTasks</span><span>(</span>
      <span>tasks</span><span>.</span><span>map</span><span>((</span><span>task</span><span>)</span> <span>=></span>
        <span>task</span><span>.</span><span>id</span> <span>===</span> <span>id</span> <span>?</span> <span>{</span> <span>...</span><span>task</span><span>,</span> <span>completed</span><span>:</span> <span>!</span><span>task</span><span>.</span><span>completed</span> <span>}</span> <span>:</span> <span>task</span>
      <span>)</span>
    <span>);</span>
  <span>};</span>

  <span>const</span> <span>handleDeleteTask</span> <span>=</span> <span>(</span><span>id</span><span>:</span> <span>string</span><span>)</span> <span>=></span> <span>{</span>
    <span>setTasks</span><span>(</span><span>tasks</span><span>.</span><span>filter</span><span>((</span><span>task</span><span>)</span> <span>=></span> <span>task</span><span>.</span><span>id</span> <span>!==</span> <span>id</span><span>));</span>
  <span>};</span>

  <span>return </span><span>(</span>
    <span><</span><span>div</span> <span>className</span><span>=</span><span>"app"</span><span>></span>
      <span><</span><span>header</span> <span>className</span><span>=</span><span>"app-header"</span><span>></span>
        <span><</span><span>h1</span><span>></span>任務管理應用程式<span></</span><span>h1</span><span>></span>
      <span></</span><span>header</span><span>></span>

      <span><</span><span>div</span> <span>className</span><span>=</span><span>"tasks-container"</span><span>></span>
        <span><</span><span>form</span> <span>className</span><span>=</span><span>"task-form"</span> <span>onSubmit</span><span>=</span><span>{</span><span>handleAddTask</span><span>}</span><span>></span>
          <span><</span><span>input</span>
            <span>type</span><span>=</span><span>"text"</span>
            <span>value</span><span>=</span><span>{</span><span>title</span><span>}</span>
            <span>onChange</span><span>=</span><span>{</span><span>(</span><span>e</span><span>)</span> <span>=></span> <span>setTitle</span><span>(</span><span>e</span><span>.</span><span>target</span><span>.</span><span>value</span><span>)</span><span>}</span>
            <span>placeholder</span><span>=</span><span>"新增任務"</span>
          <span>/></span>
          <span><</span><span>select</span>
            <span>value</span><span>=</span><span>{</span><span>priority</span><span>}</span>
            <span>onChange</span><span>=</span><span>{</span><span>(</span><span>e</span><span>)</span> <span>=></span> <span>setPriority</span><span>(</span><span>e</span><span>.</span><span>target</span><span>.</span><span>value</span> <span>as </span><span>Task</span><span>[</span><span>"</span><span>priority</span><span>"</span><span>])</span><span>}</span>
          <span>></span>
            <span><</span><span>option</span> <span>value</span><span>=</span><span>"low"</span><span>></span>低<span></</span><span>option</span><span>></span>
            <span><</span><span>option</span> <span>value</span><span>=</span><span>"medium"</span><span>></span>中<span></</span><span>option</span><span>></span>
            <span><</span><span>option</span> <span>value</span><span>=</span><span>"high"</span><span>></span>高<span></</span><span>option</span><span>></span>
          <span></</span><span>select</span><span>></span>
          <span><</span><span>button</span> <span>type</span><span>=</span><span>"submit"</span><span>></span>新增<span></</span><span>button</span><span>></span>
        <span></</span><span>form</span><span>></span>

        <span>{</span><span>tasks</span><span>.</span><span>length</span> <span>===</span> <span>0</span> <span>?</span> <span>(</span>
          <span><</span><span>p</span> <span>className</span><span>=</span><span>"empty-message"</span><span>></span>
            目前沒有任務。請新增一個任務。
          <span></</span><span>p</span><span>></span>
        <span>)</span> <span>:</span> <span>(</span>
          <span><</span><span>ul</span> <span>className</span><span>=</span><span>"task-list"</span><span>></span>
            <span>{</span><span>tasks</span><span>.</span><span>map</span><span>((</span><span>task</span><span>)</span> <span>=></span> <span>(</span>
              <span><</span><span>li</span>
                <span>key</span><span>=</span><span>{</span><span>task</span><span>.</span><span>id</span><span>}</span>
                <span>className</span><span>=</span><span>{</span><span>`task-item </span><span>${</span><span>task</span><span>.</span><span>completed</span> <span>?</span> <span>"</span><span>completed</span><span>"</span> <span>:</span> <span>""</span><span>}</span><span>`</span><span>}</span>
              <span>></span>
                <span><</span><span>div</span><span>></span>
                  <span><</span><span>span</span> <span>className</span><span>=</span><span>{</span><span>`task-priority priority-</span><span>${</span><span>task</span><span>.</span><span>priority</span><span>}</span><span>`</span><span>}</span><span>></span>
                    <span>{</span><span>task</span><span>.</span><span>priority</span><span>}</span>
                  <span></</span><span>span</span><span>></span>
                  <span><</span><span>span</span> <span>className</span><span>=</span><span>"task-title"</span><span>></span><span>{</span><span>task</span><span>.</span><span>title</span><span>}</span><span></</span><span>span</span><span>></span>
                <span></</span><span>div</span><span>></span>
                <span><</span><span>div</span> <span>className</span><span>=</span><span>"task-actions"</span><span>></span>
                  <span><</span><span>button</span>
                    <span>className</span><span>=</span><span>"toggle-btn"</span>
                    <span>onClick</span><span>=</span><span>{</span><span>()</span> <span>=></span> <span>handleToggleTask</span><span>(</span><span>task</span><span>.</span><span>id</span><span>)</span><span>}</span>
                    <span>aria-label</span><span>=</span><span>{</span>
                      <span>task</span><span>.</span><span>completed</span>
                        <span>?</span> <span>"</span><span>將任務標記為未完成</span><span>"</span>
                        <span>:</span> <span>"</span><span>將任務標記為完成</span><span>"</span>
                    <span>}</span>
                  <span>></span>
                    <span><</span><span>FaCheck</span> <span>/></span>
                  <span></</span><span>button</span><span>></span>
                  <span><</span><span>button</span>
                    <span>className</span><span>=</span><span>"delete-btn"</span>
                    <span>onClick</span><span>=</span><span>{</span><span>()</span> <span>=></span> <span>handleDeleteTask</span><span>(</span><span>task</span><span>.</span><span>id</span><span>)</span><span>}</span>
                    <span>aria-label</span><span>=</span><span>"刪除任務"</span>
                  <span>></span>
                    <span><</span><span>FaTrash</span> <span>/></span>
                  <span></</span><span>button</span><span>></span>
                <span></</span><span>div</span><span>></span>
              <span></</span><span>li</span><span>></span>
            <span>))</span><span>}</span>
          <span></</span><span>ul</span><span>></span>
        <span>)</span><span>}</span>
      <span></</span><span>div</span><span>></span>
    <span></</span><span>div</span><span>></span>
  <span>);</span>
<span>};</span>

<span>export</span> <span>default</span> <span>App</span><span>;</span>

以下の画面が表示されればベースのアプリは完成です。

![image.png]()

2. Container/Presenter 模式

まずはContainer/Presenterというデザインパターンを実装しましょう
Container/Presenterパターンは、コンポーネントを「見た目の表示を担当する部分」と「ロジックを担当する部分」に分離する設計パターンです。

![image.png]()

Reactには大事な原則として純粋性という考え方があります。

![image.png]()

Reactにおける純粋性とは、同じ入力(props・state・context)を受け取ったら常に同じ出力(UI)を返すという特性のことです。純粋なコンポーネントは、これらの入力が変わらない限り同じ結果をレンダリングします。

Container/Presenterパターンが良いのは関心の分離をすることで理解しやすいコードになると同時に純粋性を守ることができるようになることです。

それでは先程のアプリにContainer/Presenterを適応するようにリファクタリングを行います。

src/index.css

<span>/* src/index.css */</span>
<span>body</span> <span>{</span>
  <span>margin</span><span>:</span> <span>0</span><span>;</span>
  <span>font-family</span><span>:</span> <span>-</span><span>apple-system</span><span>,</span> <span>BlinkMacSystemFont</span><span>,</span> <span>'Segoe UI'</span><span>,</span> <span>'Roboto'</span><span>,</span> <span>'Oxygen'</span><span>,</span> <span>'Ubuntu'</span><span>,</span> <span>'Cantarell'</span><span>,</span> <span>'Fira Sans'</span><span>,</span> <span>'Droid Sans'</span><span>,</span> <span>'Helvetica Neue'</span><span>,</span> <span>sans-serif</span><span>;</span>
  <span>-webkit-font-smoothing</span><span>:</span> <span>antialiased</span><span>;</span>
  <span>-moz-osx-font-smoothing</span><span>:</span> <span>grayscale</span><span>;</span>
<span>}</span>

<span>*</span> <span>{</span>
  <span>box-sizing</span><span>:</span> <span>border-box</span><span>;</span>
<span>}</span>

<span>.app</span> <span>{</span>
  <span>max-width</span><span>:</span> <span>800px</span><span>;</span>
  <span>margin</span><span>:</span> <span>0</span> <span>auto</span><span>;</span>
  <span>padding</span><span>:</span> <span>2rem</span><span>;</span>
<span>}</span>

<span>.app-header</span> <span>{</span>
  <span>margin-bottom</span><span>:</span> <span>2rem</span><span>;</span>
  <span>text-align</span><span>:</span> <span>center</span><span>;</span>
<span>}</span>

<span>.tasks-container</span> <span>{</span>
  <span>background-color</span><span>:</span> <span>#f5f5f5</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>8px</span><span>;</span>
  <span>padding</span><span>:</span> <span>1.5rem</span><span>;</span>
  <span>box-shadow</span><span>:</span> <span>0</span> <span>2px</span> <span>4px</span> <span>rgba</span><span>(</span><span>0</span><span>,</span> <span>0</span><span>,</span> <span>0</span><span>,</span> <span>0.1</span><span>);</span>
<span>}</span>

<span>.task-form</span> <span>{</span>
  <span>display</span><span>:</span> <span>flex</span><span>;</span>
  <span>margin-bottom</span><span>:</span> <span>1.5rem</span><span>;</span>
  <span>gap</span><span>:</span> <span>0.5rem</span><span>;</span>
<span>}</span>

<span>.task-form</span> <span>input</span> <span>{</span>
  <span>flex</span><span>:</span> <span>1</span><span>;</span>
  <span>padding</span><span>:</span> <span>0.75rem</span><span>;</span>
  <span>border</span><span>:</span> <span>1px</span> <span>solid</span> <span>#ddd</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>4px</span><span>;</span>
  <span>font-size</span><span>:</span> <span>1rem</span><span>;</span>
<span>}</span>

<span>.task-form</span> <span>select</span> <span>{</span>
  <span>padding</span><span>:</span> <span>0.75rem</span><span>;</span>
  <span>border</span><span>:</span> <span>1px</span> <span>solid</span> <span>#ddd</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>4px</span><span>;</span>
  <span>font-size</span><span>:</span> <span>1rem</span><span>;</span>
<span>}</span>

<span>.task-form</span> <span>button</span> <span>{</span>
  <span>padding</span><span>:</span> <span>0.75rem</span> <span>1rem</span><span>;</span>
  <span>background-color</span><span>:</span> <span>#3498db</span><span>;</span>
  <span>color</span><span>:</span> <span>white</span><span>;</span>
  <span>border</span><span>:</span> <span>none</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>4px</span><span>;</span>
  <span>cursor</span><span>:</span> <span>pointer</span><span>;</span>
  <span>font-size</span><span>:</span> <span>1rem</span><span>;</span>
<span>}</span>

<span>.task-list</span> <span>{</span>
  <span>list-style</span><span>:</span> <span>none</span><span>;</span>
  <span>padding</span><span>:</span> <span>0</span><span>;</span>
  <span>margin</span><span>:</span> <span>0</span><span>;</span>
<span>}</span>

<span>.task-item</span> <span>{</span>
  <span>display</span><span>:</span> <span>flex</span><span>;</span>
  <span>justify-content</span><span>:</span> <span>space-between</span><span>;</span>
  <span>align-items</span><span>:</span> <span>center</span><span>;</span>
  <span>padding</span><span>:</span> <span>0.75rem</span> <span>1rem</span><span>;</span>
  <span>margin-bottom</span><span>:</span> <span>0.5rem</span><span>;</span>
  <span>background-color</span><span>:</span> <span>white</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>4px</span><span>;</span>
  <span>box-shadow</span><span>:</span> <span>0</span> <span>1px</span> <span>2px</span> <span>rgba</span><span>(</span><span>0</span><span>,</span> <span>0</span><span>,</span> <span>0</span><span>,</span> <span>0.05</span><span>);</span>
<span>}</span>

<span>.task-item.completed</span> <span>.task-title</span> <span>{</span>
  <span>text-decoration</span><span>:</span> <span>line-through</span><span>;</span>
  <span>color</span><span>:</span> <span>#888</span><span>;</span>
<span>}</span>

<span>.task-priority</span> <span>{</span>
  <span>font-size</span><span>:</span> <span>0.8rem</span><span>;</span>
  <span>padding</span><span>:</span> <span>0.2rem</span> <span>0.5rem</span><span>;</span>
  <span>border-radius</span><span>:</span> <span>3px</span><span>;</span>
  <span>color</span><span>:</span> <span>white</span><span>;</span>
  <span>margin-right</span><span>:</span> <span>0.5rem</span><span>;</span>
<span>}</span>

<span>.priority-low</span> <span>{</span>
  <span>background-color</span><span>:</span> <span>#27ae60</span><span>;</span>
<span>}</span>

<span>.priority-medium</span> <span>{</span>
  <span>background-color</span><span>:</span> <span>#f39c12</span><span>;</span>
<span>}</span>

<span>.priority-high</span> <span>{</span>
  <span>background-color</span><span>:</span> <span>#e74c3c</span><span>;</span>
<span>}</span>

<span>.task-actions</span> <span>{</span>
  <span>display</span><span>:</span> <span>flex</span><span>;</span>
  <span>gap</span><span>:</span> <span>0.5rem</span><span>;</span>
<span>}</span>

<span>.task-actions</span> <span>button</span> <span>{</span>
  <span>background</span><span>:</span> <span>none</span><span>;</span>
  <span>border</span><span>:</span> <span>none</span><span>;</span>
  <span>cursor</span><span>:</span> <span>pointer</span><span>;</span>
  <span>font-size</span><span>:</span> <span>1rem</span><span>;</span>
  <span>display</span><span>:</span> <span>flex</span><span>;</span>
  <span>align-items</span><span>:</span> <span>center</span><span>;</span>
  <span>padding</span><span>:</span> <span>0.25rem</span><span>;</span>
<span>}</span>

<span>.toggle-btn</span> <span>{</span>
  <span>color</span><span>:</span> <span>#27ae60</span><span>;</span>
<span>}</span>

<span>.delete-btn</span> <span>{</span>
  <span>color</span><span>:</span> <span>#e74c3c</span><span>;</span>
<span>}</span>

<span>.empty-message</span> <span>{</span>
  <span>text-align</span><span>:</span> <span>center</span><span>;</span>
  <span>color</span><span>:</span> <span>#888</span><span>;</span>
  <span>font-style</span><span>:</span> <span>italic</span><span>;</span>
<span>}</span>
<span>mkdir </span>src/components
<span>mkdir </span>src/components/TaskList
<span>touch </span>src/components/TaskList/TaskListPresenter.tsx
<span>touch </span>src/components/TaskList/TaskListContainer.tsx

<span>mkdir </span>src/components/TaskForm
<span>touch </span>src/components/TaskForm/TaskFormPresenter.tsx

src/components/TaskList/TaskListPresenter.tsx

<span>import</span> <span>React</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>import</span> <span>{</span> <span>FaCheck</span><span>,</span> <span>FaTrash</span> <span>}</span> <span>from</span> <span>"</span><span>react-icons/fa</span><span>"</span><span>;</span>
<span>import</span> <span>type</span> <span>{</span> <span>Task</span> <span>}</span> <span>from</span> <span>"</span><span>../../domain/Task</span><span>"</span><span>;</span>

<span>interface</span> <span>TaskListPresenterProps</span> <span>{</span>
  <span>tasks</span><span>:</span> <span>Task</span><span>[];</span>
  <span>onToggleTask</span><span>:</span> <span>(</span><span>id</span><span>:</span> <span>string</span><span>)</span> <span>=></span> <span>void</span><span>;</span>
  <span>onDeleteTask</span><span>:</span> <span>(</span><span>id</span><span>:</span> <span>string</span><span>)</span> <span>=></span> <span>void</span><span>;</span>
<span>}</span>

<span>const</span> <span>TaskListPresenter</span><span>:</span> <span>React</span><span>.</span><span>FC</span><span><</span><span>TaskListPresenterProps</span><span>></span> <span>=</span> <span>({</span>
  <span>tasks</span><span>,</span>
  <span>onToggleTask</span><span>,</span>
  <span>onDeleteTask</span><span>,</span>
<span>})</span> <span>=></span> <span>{</span>
  <span>if </span><span>(</span><span>tasks</span><span>.</span><span>length</span> <span>===</span> <span>0</span><span>)</span> <span>{</span>
    <span>return </span><span>(</span>
      <span><</span><span>p</span> <span>className</span><span>=</span><span>"empty-message"</span><span>></span>
        目前沒有任務。請新增一個任務。
      <span></</span><span>p</span><span>></span>
    <span>);</span>
  <span>}</span>

  <span>return </span><span>(</span>
    <span><</span><span>ul</span> <span>className</span><span>=</span><span>"task-list"</span><span>></span>
      <span>{</span><span>tasks</span><span>.</span><span>map</span><span>((</span><span>task</span><span>)</span> <span>=></span> <span>(</span>
        <span><</span><span>li</span>
          <span>key</span><span>=</span><span>{</span><span>task</span><span>.</span><span>id</span><span>}</span>
          <span>className</span><span>=</span><span>{</span><span>`task-item </span><span>${</span><span>task</span><span>.</span><span>completed</span> <span>?</span> <span>"</span><span>completed</span><span>"</span> <span>:</span> <span>""</span><span>}</span><span>`</span><span>}</span>
        <span>></span>
          <span><</span><span>div</span><span>></span>
            <span><</span><span>span</span> <span>className</span><span>=</span><span>{</span><span>`task-priority priority-</span><span>${</span><span>task</span><span>.</span><span>priority</span><span>}</span><span>`</span><span>}</span><span>></span>
              <span>{</span><span>task</span><span>.</span><span>priority</span><span>}</span>
            <span></</span><span>span</span><span>></span>
            <span><</span><span>span</span> <span>className</span><span>=</span><span>"task-title"</span><span>></span><span>{</span><span>task</span><span>.</span><span>title</span><span>}</span><span></</span><span>span</span><span>></span>
          <span></</span><span>div</span><span>></span>
          <span><</span><span>div</span> <span>className</span><span>=</span><span>"task-actions"</span><span>></span>
            <span><</span><span>button</span>
              <span>className</span><span>=</span><span>"toggle-btn"</span>
              <span>onClick</span><span>=</span><span>{</span><span>()</span> <span>=></span> <span>onToggleTask</span><span>(</span><span>task</span><span>.</span><span>id</span><span>)</span><span>}</span>
              <span>aria-label</span><span>=</span><span>{</span>
                <span>task</span><span>.</span><span>completed</span> <span>?</span> <span>"</span><span>將任務標記為未完成</span><span>"</span> <span>:</span> <span>"</span><span>將任務標記為完成</span><span>"</span>
              <span>}</span>
            <span>></span>
              <span><</span><span>FaCheck</span> <span>/></span>
            <span></</span><span>button</span><span>></span>
            <span><</span><span>button</span>
              <span>className</span><span>=</span><span>"delete-btn"</span>
              <span>onClick</span><span>=</span><span>{</span><span>()</span> <span>=></span> <span>onDeleteTask</span><span>(</span><span>task</span><span>.</span><span>id</span><span>)</span><span>}</span>
              <span>aria-label</span><span>=</span><span>"刪除任務"</span>
            <span>></span>
              <span><</span><span>FaTrash</span> <span>/></span>
            <span></</span><span>button</span><span>></span>
          <span></</span><span>div</span><span>></span>
        <span></</span><span>li</span><span>></span>
      <span>))</span><span>}</span>
    <span></</span><span>ul</span><span>></span>
  <span>);</span>
<span>};</span>

<span>export</span> <span>default</span> <span>TaskListPresenter</span><span>;</span>

src/components/TaskForm/TaskFormPresenter.tsx

<span>import</span> <span>React</span><span>,</span> <span>{</span> <span>useState</span> <span>}</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>import</span> <span>type</span> <span>{</span> <span>Task</span> <span>}</span> <span>from</span> <span>"</span><span>../../domain/Task</span><span>"</span><span>;</span>

<span>interface</span> <span>TaskFormPresenterProps</span> <span>{</span>
  <span>onAddTask</span><span>:</span> <span>(</span><span>title</span><span>:</span> <span>string</span><span>,</span> <span>priority</span><span>:</span> <span>Task</span><span>[</span><span>"</span><span>priority</span><span>"</span><span>])</span> <span>=></span> <span>void</span><span>;</span>
<span>}</span>

<span>// 表單的輸入值(title・priority)是直接由使用者操作所產生的 UI 本地狀態。</span>
<span>// 這與 Container 所管理的應用程式商業邏輯不同,</span>
<span>// 因此由 Presenter 在內部管理。</span>
<span>// 「Presenter 不持有商業邏輯」的原則在此被維持。</span>
<span>const</span> <span>TaskFormPresenter</span><span>:</span> <span>React</span><span>.</span><span>FC</span><span><</span><span>TaskFormPresenterProps</span><span>></span> <span>=</span> <span>({</span> <span>onAddTask</span> <span>})</span> <span>=></span> <span>{</span>
  <span>const</span> <span>[</span><span>title</span><span>,</span> <span>setTitle</span><span>]</span> <span>=</span> <span>useState</span><span>(</span><span>""</span><span>);</span>
  <span>const</span> <span>[</span><span>priority</span><span>,</span> <span>setPriority</span><span>]</span> <span>=</span> <span>useState</span><span><</span><span>Task</span><span>[</span><span>"</span><span>priority</span><span>"</span><span>]</span><span>></span><span>(</span><span>"</span><span>medium</span><span>"</span><span>);</span>

  <span>const</span> <span>handleSubmit</span> <span>=</span> <span>(</span><span>e</span><span>:</span> <span>React</span><span>.</span><span>FormEvent</span><span>)</span> <span>=></span> <span>{</span>
    <span>e</span><span>.</span><span>preventDefault</span><span>();</span>
    <span>if </span><span>(</span><span>title</span><span>.</span><span>trim</span><span>())</span> <span>{</span>
      <span>onAddTask</span><span>(</span><span>title</span><span>,</span> <span>priority</span><span>);</span>
      <span>setTitle</span><span>(</span><span>""</span><span>);</span>
      <span>setPriority</span><span>(</span><span>"</span><span>medium</span><span>"</span><span>);</span>
    <span>}</span>
  <span>};</span>

  <span>return </span><span>(</span>
    <span><</span><span>form</span> <span>className</span><span>=</span><span>"task-form"</span> <span>onSubmit</span><span>=</span><span>{</span><span>handleSubmit</span><span>}</span><span>></span>
      <span><</span><span>input</span>
        <span>type</span><span>=</span><span>"text"</span>
        <span>value</span><span>=</span><span>{</span><span>title</span><span>}</span>
        <span>onChange</span><span>=</span><span>{</span><span>(</span><span>e</span><span>)</span> <span>=></span> <span>setTitle</span><span>(</span><span>e</span><span>.</span><span>target</span><span>.</span><span>value</span><span>)</span><span>}</span>
        <span>placeholder</span><span>=</span><span>"新增任務"</span>
      <span>/></span>
      <span><</span><span>select</span>
        <span>value</span><span>=</span><span>{</span><span>priority</span><span>}</span>
        <span>onChange</span><span>=</span><span>{</span><span>(</span><span>e</span><span>)</span> <span>=></span> <span>setPriority</span><span>(</span><span>e</span><span>.</span><span>target</span><span>.</span><span>value</span> <span>as </span><span>Task</span><span>[</span><span>"</span><span>priority</span><span>"</span><span>])</span><span>}</span>
      <span>></span>
        <span><</span><span>option</span> <span>value</span><span>=</span><span>"low"</span><span>></span>低<span></</span><span>option</span><span>></span>
        <span><</span><span>option</span> <span>value</span><span>=</span><span>"medium"</span><span>></span>中<span></</span><span>option</span><span>></span>
        <span><</span><span>option</span> <span>value</span><span>=</span><span>"high"</span><span>></span>高<span></</span><span>option</span><span>></span>
      <span></</span><span>select</span><span>></span>
      <span><</span><span>button</span> <span>type</span><span>=</span><span>"submit"</span><span>></span>新增<span></</span><span>button</span><span>></span>
    <span></</span><span>form</span><span>></span>
  <span>);</span>
<span>};</span>

<span>export</span> <span>default</span> <span>TaskFormPresenter</span><span>;</span>

src/components/TaskList/TaskListContainer.tsx

<span>import</span> <span>React</span><span>,</span> <span>{</span> <span>useState</span><span>,</span> <span>useEffect</span> <span>}</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>import</span> <span>TaskListPresenter</span> <span>from</span> <span>"</span><span>./TaskListPresenter</span><span>"</span><span>;</span>
<span>import</span> <span>type</span> <span>{</span> <span>Task</span> <span>}</span> <span>from</span> <span>"</span><span>../../domain/Task</span><span>"</span><span>;</span>
<span>import</span> <span>TaskFormPresenter</span> <span>from</span> <span>"</span><span>../TaskForm/TaskFormPresenter</span><span>"</span><span>;</span>

<span>const</span> <span>TaskListContainer</span><span>:</span> <span>React</span><span>.</span><span>FC</span> <span>=</span> <span>()</span> <span>=></span> <span>{</span>
  <span>const</span> <span>[</span><span>tasks</span><span>,</span> <span>setTasks</span><span>]</span> <span>=</span> <span>useState</span><span><</span><span>Task</span><span>[]</span><span>></span><span>(()</span> <span>=></span> <span>{</span>
    <span>const</span> <span>savedTasks</span> <span>=</span> <span>localStorage</span><span>.</span><span>getItem</span><span>(</span><span>"</span><span>tasks</span><span>"</span><span>);</span>
    <span>return</span> <span>savedTasks</span> <span>?</span> <span>JSON</span><span>.</span><span>parse</span><span>(</span><span>savedTasks</span><span>)</span> <span>:</span> <span>[];</span>
  <span>});</span>

  <span>useEffect</span><span>(()</span> <span>=></span> <span>{</span>
    <span>localStorage</span><span>.</span><span>setItem</span><span>(</span><span>"</span><span>tasks</span><span>"</span><span>,</span> <span>JSON</span><span>.</span><span>stringify</span><span>(</span><span>tasks</span><span>));</span>
  <span>},</span> <span>[</span><span>tasks</span><span>]);</span>

  <span>const</span> <span>handleToggleTask</span> <span>=</span> <span>(</span><span>id</span><span>:</span> <span>string</span><span>)</span> <span>=></span> <span>{</span>
    <span>setTasks</span><span>((</span><span>prevTasks</span><span>)</span> <span>=></span>
      <span>prevTasks</span><span>.</span><span>map</span><span>((</span><span>task</span><span>)</span> <span>=></span>
        <span>task</span><span>.</span><span>id</span> <span>===</span> <span>id</span> <span>?</span> <span>{</span> <span>...</span><span>task</span><span>,</span> <span>completed</span><span>:</span> <span>!</span><span>task</span><span>.</span><span>completed</span> <span>}</span> <span>:</span> <span>task</span>
      <span>)</span>
    <span>);</span>
  <span>};</span>

  <span>const</span> <span>handleDeleteTask</span> <span>=</span> <span>(</span><span>id</span><span>:</span> <span>string</span><span>)</span> <span>=></span> <span>{</span>
    <span>setTasks</span><span>((</span><span>prevTasks</span><span>)</span> <span>=></span> <span>prevTasks</span><span>.</span><span>filter</span><span>((</span><span>task</span><span>)</span> <span>=></span> <span>task</span><span>.</span><span>id</span> <span>!==</span> <span>id</span><span>));</span>
  <span>};</span>

  <span>const</span> <span>handleAddTask</span> <span>=</span> <span>(</span><span>title</span><span>:</span> <span>string</span><span>,</span> <span>priority</span><span>:</span> <span>Task</span><span>[</span><span>"</span><span>priority</span><span>"</span><span>])</span> <span>=></span> <span>{</span>
    <span>const</span> <span>newTask</span><span>:</span> <span>Task</span> <span>=</span> <span>{</span>
      <span>// 在正式環境請使用 crypto.randomUUID() 或其他更安全的方法</span>
      <span>id</span><span>:</span> <span>Date</span><span>.</span><span>now</span><span>().</span><span>toString</span><span>(),</span>
      <span>title</span><span>,</span>
      <span>completed</span><span>:</span> <span>false</span><span>,</span>
      <span>priority</span><span>,</span>
    <span>};</span>

    <span>setTasks</span><span>((</span><span>prevTasks</span><span>)</span> <span>=></span> <span>[...</span><span>prevTasks</span><span>,</span> <span>newTask</span><span>]);</span>
  <span>};</span>

  <span>return </span><span>(</span>
    <span><</span><span>div</span> <span>className</span><span>=</span><span>"tasks-container"</span><span>></span>
      <span><</span><span>TaskFormPresenter</span> <span>onAddTask</span><span>=</span><span>{</span><span>handleAddTask</span><span>}</span> <span>/></span>
      <span><</span><span>TaskListPresenter</span>
        <span>tasks</span><span>=</span><span>{</span><span>tasks</span><span>}</span>
        <span>onToggleTask</span><span>=</span><span>{</span><span>handleToggleTask</span><span>}</span>
        <span>onDeleteTask</span><span>=</span><span>{</span><span>handleDeleteTask</span><span>}</span>
      <span>/></span>
    <span></</span><span>div</span><span>></span>
  <span>);</span>
<span>};</span>

<span>export</span> <span>default</span> <span>TaskListContainer</span><span>;</span>

PresenterではContainerから状態変更の関数とステートを受け取って表示だけを行っています。

      <span><</span><span>TaskListPresenter</span>
        <span>tasks</span><span>=</span><span>{</span><span>tasks</span><span>}</span>
        <span>onToggleTask</span><span>=</span><span>{</span><span>handleToggleTask</span><span>}</span>
        <span>onDeleteTask</span><span>=</span><span>{</span><span>handleDeleteTask</span><span>}</span>
      <span>/></span>

アプリが正しく使えればリファクタリングができています。
こうすることで画面の表示に関する修正はPresenter、データに関して修正する場合はContainerを修正すればいいので関心の分離によりコードの認知負荷も軽減します。

Container/Presenter 模式要點

メリット

  • 関心の分離により、コードの認知負荷が下がる
  • Presenterはpropsだけに依存するため、単体テストが書きやすい(モックが少なくて済む)
  • UIの修正とロジックの修正が独立して行えるため、影響範囲が明確になる
  • デザイナーとエンジニアで作業を分担しやすくなる

デメリット

  • ファイル数が増え、小規模なコンポーネントではオーバーエンジニアリングになりがち
  • ContainerとPresenterの境界が曖昧になることがある(フォームのローカルstateなど)

向いているケース

  • ロジックが複雑でテストを書きたいとき
  • UIのバリエーションが複数必要なとき(同じロジックで見た目だけ変える)
  • チームで役割分担して開発するとき

3. 自訂 Hook

Container/PresenterパターンでロジックをContainerに分離しましたが、カスタムフックを利用するとさらに独立した関数として抽出でき、より柔軟な設計が可能になります。

![image.png]()

カスタムフックはuseから始まる名前をつける必要があります。
それではContainer/Presenterパターンをやめてカスタムフックを利用しましょう

<span>rm </span>src/components/TaskList/TaskListContainer.tsx
<span>rm </span>src/components/TaskList/TaskListPresenter.tsx
<span>rm </span>src/components/TaskForm/TaskFormPresenter.tsx
<span>touch </span>src/components/TaskList.tsx
<span>touch </span>src/components/TaskForm.tsx
<span>mkdir </span>src/hooks
<span>touch </span>src/hooks/useTaskList.ts

src/components/TaskList.tsx

<span>import</span> <span>React</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>import</span> <span>{</span> <span>FaCheck</span><span>,</span> <span>FaTrash</span> <span>}</span> <span>from</span> <span>"</span><span>react-icons/fa</span><span>"</span><span>;</span>
<span>import</span> <span>type</span> <span>{</span> <span>Task</span> <span>}</span> <span>from</span> <span>"</span><span>../domain/Task</span><span>"</span><span>;</span>

<span>interface</span> <span>TaskListProps</span> <span>{</span>
  <span>tasks</span><span>:</span> <span>Task</span><span>[];</span>
  <span>onToggleTask</span><span>:</span> <span>(</span><span>id</span><span>:</span> <span>string</span><span>)</span> <span>=></span> <span>void</span><span>;</span>
  <span>onDeleteTask</span><span>:</span> <span>(</span><span>id</span><span>:</span> <span>string</span><span>)</span> <span>=></span> <span>void</span><span>;</span>
<span>}</span>

<span>const</span> <span>TaskList</span><span>:</span> <span>React</span><span>.</span><span>FC</span><span><</span><span>TaskListProps</span><span>></span> <span>=</span> <span>({</span> <span>tasks</span><span>,</span> <span>onToggleTask</span><span>,</span> <span>onDeleteTask</span> <span>})</span> <span>=></span> <span>{</span>
  <span>if </span><span>(</span><span>tasks</span><span>.</span><span>length</span> <span>===</span> <span>0</span><span>)</span> <span>{</span>
    <span>return </span><span>(</span>
      <span><</span><span>p</span> <span>className</span><span>=</span><span>"empty-message"</span><span>></span>
        目前沒有任務。請新增一個任務。
      <span></</span><span>p</span><span>></span>
    <span>);</span>
  <span>}</span>

  <span>return </span><span>(</span>
    <span><</span><span>ul</span> <span>className</span><span>=</span><span>"task-list"</span><span>></span>
      <span>{</span><span>tasks</span><span>.</span><span>map</span><span>((</span><span>task</span><span>)</span> <span>=></span> <span>(</span>
        <span><</span><span>li</span>
          <span>key</span><span>=</span><span>{</span><span>task</span><span>.</span><span>id</span><span>}</span>
          <span>className</span><span>=</span><span>{</span><span>`task-item </span><span>${</span><span>task</span><span>.</span><span>completed</span> <span>?</span> <span>"</span><span>completed</span><span>"</span> <span>:</span> <span>""</span><span>}</span><span>`</span><span>}</span>
        <span>></span>
          <span><</span><span>div</span><span>></span>
            <span><</span><span>span</span> <span>className</span><span>=</span><span>{</span><span>`task-priority priority-</span><span>${</span><span>task</span><span>.</span><span>priority</span><span>}</span><span>`</span><span>}</span><span>></span>
              <span>{</span><span>task</span><span>.</span><span>priority</span><span>}</span>
            <span></</span><span>span</span><span>></span>
            <span><</span><span>span</span> <span>className</span><span>=</span><span>"task-title"</span><span>></span><span>{</span><span>task</span><span>.</span><span>title</span><span>}</span><span></</span><span>span</span><span>></span>
          <span></</span><span>div</span><span>></span>
          <span><</span><span>div</span> <span>className</span><span>=</span><span>"task-actions"</span><span>></span>
            <span><</span><span>button</span>
              <span>className</span><span>=</span><span>"toggle-btn"</span>
              <span>onClick</span><span>=</span><span>{</span><span>()</span> <span>=></span> <span>onToggleTask</span><span>(</span><span>task</span><span>.</span><span>id</span><span>)</span><span>}</span>
              <span>aria-label</span><span>=</span><span>{</span><span>task</span><span>.</span><span>completed</span> <span>?</span> <span>"</span><span>將任務標記為未完成</span><span>"</span> <span>:</span> <span>"</span><span>將任務標記為完成</span><span>"</span><span>}</span>
            <span>></span>
              <span><</span><span>FaCheck</span> <span>/></span>
            <span></</span><span>button</span><span>></span>
            <span><</span><span>button</span>
              <span>className</span><span>=</span><span>"delete-btn"</span>
              <span>onClick</span><span>=</span><span>{</span><span>()</span> <span>=></span> <span>onDeleteTask</span><span>(</span><span>task</span><span>.</span><span>id</span><span>)</span><span>}</span>
              <span>aria-label</span><span>=</span><span>"刪除任務"</span>
            <span>></span>
              <span><</span><span>FaTrash</span> <span>/></span>
            <span></</span><span>button</span><span>></span>
          <span></</span><span>div</span><span>></span>
        <span></</span><span>li</span><span>></span>
      <span>))</span><span>}</span>
    <span></</span><span>ul</span><span>></span>
  <span>);</span>
<span>};</span>

<span>export</span> <span>default</span> <span>TaskList</span><span>;</span>

src/components/TaskForm.tsx


<span>import</span> <span>React</span><span>,</span> <span>{</span> <span>useState</span> <span>}</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>import</span> <span>type</span> <span>{</span> <span>Task</span> <span>}</span> <span>from</span> <span>"</span><span>../domain/Task</span><span>"</span><span>;</span>

<span>interface</span> <span>TaskFormProps</span> <span>{</span>
  <span>onAddTask</span><span>:</span> <span>(</span><span>title</span><span>:</span> <span>string</span><span>,</span> <span>priority</span><span>:</span> <span>Task</span><span>[</span><span>"</span><span>priority</span><span>"</span><span>])</span> <span>=></span> <span>void</span><span>;</span>
<span>}</span>

<span>const</span> <span>TaskForm</span><span>:</span> <span>React</span><span>.</span><span>FC</span><span><</span><span>TaskFormProps</span><span>></span> <span>=</span> <span>({</span> <span>onAddTask</span> <span>})</span> <span>=></span> <span>{</span>
  <span>const</span> <span>[</span><span>title</span><span>,</span> <span>setTitle</span><span>]</span> <span>=</span> <span>useState</span><span>(</span><span>""</span><span>);</span>
  <span>const</span> <span>[</span><span>priority</span><span>,</span> <span>setPriority</span><span>]</span> <span>=</span> <span>useState</span><span><</span><span>Task</span><span>[</span><span>"</span><span>priority</span><span>"</span><span>]</span><span>></span><span>(</span><span>"</span><span>medium</span><span>"</span><span>);</span>

  <span>const</span> <span>handleSubmit</span> <span>=</span> <span>(</span><span>e</span><span>:</span> <span>React</span><span>.</span><span>FormEvent</span><span>)</span> <span>=></span> <span>{</span>
    <span>e</span><span>.</span><span>preventDefault</span><span>();</span>
    <span>if </span><span>(</span><span>title</span><span>.</span><span>trim</span><span>())</span> <span>{</span>
      <span>onAddTask</span><span>(</span><span>title</span><span>,</span> <span>priority</span><span>);</span>
      <span>setTitle</span><span>(</span><span>""</span><span>);</span>
      <span>setPriority</span><span>(</span><span>"</span><span>medium</span><span>"</span><span>);</span>
    <span>}</span>
  <span>};</span>

  <span>return </span><span>(</span>
    <span><</span><span>form</span> <span>className</span><span>=</span><span>"task-form"</span> <span>onSubmit</span><span>=</span><span>{</span><span>handleSubmit</span><span>}</span><span>></span>
      <span><</span><span>input</span>
        <span>type</span><span>=</span><span>"text"</span>
        <span>value</span><span>=</span><span>{</span><span>title</span><span>}</span>
        <span>onChange</span><span>=</span><span>{</span><span>(</span><span>e</span><span>)</span> <span>=></span> <span>setTitle</span><span>(</span><span>e</span><span>.</span><span>target</span><span>.</span><span>value</span><span>)</span><span>}</span>
        <span>placeholder</span><span>=</span><span>"新增任務"</span>
      <span>/></span>
      <span><</span><span>select</span>
        <span>value</span><span>=</span><span>{</span><span>priority</span><span>}</span>
        <span>onChange</span><span>=</span><span>{</span><span>(</span><span>e</span><span>)</span> <span>=></span> <span>setPriority</span><span>(</span><span>e</span><span>.</span><span>target</span><span>.</span><span>value</span> <span>as </span><span>Task</span><span>[</span><span>"</span><span>priority</span><span>"</span><span>])</span><span>}</span>
      <span>></span>
        <span><</span><span>option</span> <span>value</span><span>=</span><span>"low"</span><span>></span>低<span></</span><span>option</span><span>></span>
        <span><</span><span>option</span> <span>value</span><span>=</span><span>"medium"</span><span>></span>中<span></</span><span>option</span><span>></span>
        <span><</span><span>option</span> <span>value</span><span>=</span><span>"high"</span><span>></span>高<span></</span><span>option</span><span>></span>
      <span></</span><span>select</span><span>></span>
      <span><</span><span>button</span> <span>type</span><span>=</span><span>"submit"</span><span>></span>新增<span></</span><span>button</span><span>></span>
    <span></</span><span>form</span><span>></span>
  <span>);</span>
<span>};</span>

<span>export</span> <span>default</span> <span>TaskForm</span><span>;</span>

---

原文出處:https://qiita.com/Sicut_study/items/299a221088f5905332a4

精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬10   ❤️2
364
🥈
我愛JS
📝2   💬9   ❤️2
93
🥉
💬1  
4
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登