コンポーネントの分割

ToDoページをコンポーネントに分割します。

コンポーネントへの落とし込み

次に、ToDoページをどのようなコンポーネント構造にするかを考え、コンポーネントに落とし込んでいきます。

ToDoページのデザインから、コンポーネントの階層構造に落とし込んでいきます。(参考:React - Reactの流儀 ステップ 1

扱う情報の種類や用途から、ここでは以下のようにコンポーネントに分割します。

design

  • NavigationHeader(黄色):ナビゲーションメニューのヘッダ
  • TodoBoard(オレンジ色):ToDoを扱うエリア
  • TodoForm(青色):新しいToDoを入力する
  • TodoFilter(紫色):ToDoの表示対象を選択する
  • TodoList(緑色):ToDoを一覧形式で表示する
  • TodoItem(赤色):ToDoを1行で表示する

これらのコンポーネントを、以下のような階層構造で作成していきます。

  • NavigationHeader
  • TodoBoard
    • TodoForm
    • TodoFilter
    • TodoList
      • TodoItem

コンポーネントの作成

コンポーネントを作成するディレクトリとして、srcの下にcomponentsディレクトリを作成し、そこにコンポーネントを作成していきます。

ここでは、現在表示している静的なデータをそのまま使用して、それぞれのコンポーネントを作成していきます。(参考:React - Reactの流儀 ステップ 2

また、CSSファイルもそれぞれのコンポーネント単位に分割していきます。

importで読み込んだCSSファイルの適用範囲は、そのコンポーネント内だけでなく、全てのコンポーネント(グローバル)に適用されます。

そのため、この方法ではコンポーネント単位にCSSファイルを分けておく必要自体はありませんが、コンポーネント単位に分けることで、コンポーネント単位で取り回しがしやすかったり、適用範囲がコンポーネント単位になるような他の方法へ移行しやすくなる、といったメリットがあります。ただし、全体を把握しづらくなるため、予期せずスタイルが衝突してデザインが崩れてしまう、といったような事故が発生しやすいといったデメリットもあります。

ここでは、こういった事故を防ぐための配慮として、基本的にはクラス名を起点としたスタイル定義にして、クラス名の命名ルールを[コンポーネント名]_[任意文字列]とします。これにより、他のコンポーネントとクラス名が衝突することを防ぎ、事故が起きづらいようにしておきます。なお、デザインモックではすでにこのルールに則ったクラス名を使用しているため、デザインモックのCSSをそのまま使っていきます。

NavigationHeaderコンポーネントを作成するため、NavigationHeader.tsxを作成します。NavigationHeaderが使用するCSSも分割するため、NavigationHeader.cssも作成します。

NavigationHeaderが返すReact要素には、Appから該当部分を抽出します。

src/components/NavigationHeader.tsx

import React from 'react';
import './NavigationHeader.css';

export const NavigationHeader: React.FC = () => {
  return (
    <header className="PageHeader_header">
      <h1 className="PageHeader_title">Todoアプリ</h1>
      <nav>
        <ul className="PageHeader_nav">
          <li>テストユーザさん</li>
          <li>ログアウト</li>
        </ul>
      </nav>
    </header>
  );
};

src/components/NavigationHeader.css

.PageHeader_header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 5%;
  border-bottom: solid 1px black;
  background: black;
}
.PageHeader_title {
  color: white;
  font-size: 1.5rem;
}
.PageHeader_header a {
  text-decoration: none;
}
.PageHeader_nav {
  display: flex;
  list-style: none;
}
.PageHeader_nav li {
  margin-left: 30px;
  color: white;
}
.PageHeader_nav a {
  color: white;
}
.PageHeader_nav button,
.PageHeader_nav button:active,
.PageHeader_nav button:hover
{
  cursor: pointer;
  border: 0;
  background-color: transparent;
  color: white;
}

NavigationHeaderでは、TypeScriptの構文を使用してNavigationHeaderの型にReact.FCを指定しています。React.FCは、Reactが提供している関数コンポーネントを表す型になります。TypeScriptでは変数:型というような構文で、型を付けることができます。

export const NavigationHeader: React.FC = () => {
}

NavigationHeaderが作成できたら、AppNavigationHeaderを使用するように修正します。

src/App.tsx

import React from 'react';
import './App.css';
import { NavigationHeader } from './components/NavigationHeader';

function App() {
  return (
    <React.Fragment>
      <NavigationHeader />
      <div className="TodoBoard_content">
      ...
      </div>
    </React.Fragment>
  );
}

export default App;

また、App.cssからNavigationHeader.cssに抽出した定義を削除します。

この時点でページの表示内容を確認すると、何も変わらず表示されていることを確認します。これでAppからNavigationHeader部分の抽出は完了です。

TodoBoard

TodoBoardコンポーネントを作成するため、TodoBoard.tsxを作成します。TodoBoardが使用するCSSを分割するため、TodoBoard.cssも作成します。

TodoBoardが返すReact要素には、Appから該当部分を抽出します。このコンポーネントにはいくつかの子コンポーネントがありますが、少しずつ確認していくため、一度に作り込まずに一旦このコンポーネントで全て定義し、その後に分割していきます。

src/components/TodoBoard.tsx

import React from 'react';
import './TodoBoard.css';

export const TodoBoard: React.FC = () => {
  return (
    <div className="TodoBoard_content">
      <div className="TodoForm_content">
        <form className="TodoForm_form">
          <div className="TodoForm_input">
            <input type="text" placeholder="タスクを入力してください" />
          </div>
          <div className="TodoForm_button">
            <button type="button">追加</button>
          </div>
        </form>
      </div>
      <div className="TodoFilter_content">
        <button className="TodoFilter_buttonSelected">全て</button>
        <button className="TodoFilter_buttonUnselected">未完了のみ</button>
        <button className="TodoFilter_buttonUnselected">完了のみ</button>
      </div>
      <ul className="TodoList_list">
        <li className="TodoItem_item">
          <div className="TodoItem_todo">
            <label>
              <input type="checkbox" className="TodoItem_checkbox" checked={true} />
              <span>洗い物をする</span>
            </label>
          </div>
          <div className="TodoItem_delete">
            <button className="TodoItem_button">x</button>
          </div>
        </li>
        <li className="TodoItem_item">
          <div className="TodoItem_todo">
            <label>
              <input type="checkbox" className="TodoItem_checkbox" />
              <span>洗濯物を干す</span>
            </label>
          </div>
          <div className="TodoItem_delete">
            <button className="TodoItem_button">x</button>
          </div>
        </li>
        <li className="TodoItem_item">
          <div className="TodoItem_todo">
            <label>
              <input type="checkbox" className="TodoItem_checkbox" />
              <span>買い物へ行く</span>
            </label>
          </div>
          <div className="TodoItem_delete">
            <button className="TodoItem_button">x</button>
          </div>
        </li>
      </ul>
    </div>
  );
};

src/components/TodoBoard.css

.TodoBoard_content {
  margin-top: 10px;
  width: 40%;
  padding: 0 30%;
}

.TodoForm_content {
  margin-top: 20px;
  margin-bottom: 20px;
}
.TodoForm_form {
  width: 100%;
  display: flex;
  justify-content: space-between;
}
.TodoForm_input {
  width: 86%;
}
.TodoForm_input input{
  float: left;
  width: 95%;
  border-radius: 5px;
  padding: 8px;
  border: solid 1px lightgray;
  background-color: #fafbfc;
  font-size: 16px;
  outline: none;
}
.TodoForm_input input:focus {
  background-color: white;
}
.TodoForm_button {
  text-align: center;
  width: 14%;
}
.TodoForm_button button {
  height: 35px;
  cursor: pointer;
  line-height: 1;
  font-size: 1rem;
  color: white;
  background-color: darkgreen;
  border-radius: 5px;
  padding: 0 15px;
  border: none;
  vertical-align: middle;
}
.TodoForm_button button:hover {
  background-color: green;
}

.TodoFilter_content {
  text-align: right;
}
.TodoFilter_content button{
  margin-left: 5px;
}
.TodoFilter_buttonSelected {
  background-color: #31b3c7;
  border-width: 0;
  color: #fff;
  cursor: pointer;
  justify-content: center;
  padding: 7px 16px;
  text-align: center;
  white-space: nowrap;
  border-radius: 290486px;
  outline: none;
}
.TodoFilter_buttonUnselected {
  background-color: lightgray;
  border-width: 0;
  color: gray;
  cursor: pointer;
  justify-content: center;
  padding: 7px 16px;
  text-align: center;
  white-space: nowrap;
  border-radius: 290486px;
  outline: none;
}

.TodoList_list {
  list-style: none;
  padding: 0;
  margin: 20px 0;
}

.TodoItem_item {
  padding: 15px 10px;
  background: whitesmoke;
  margin-bottom: 10px;
}
.TodoItem_todo {
  float: left;
  text-align: left;
}
.TodoItem_checkbox {
  margin-right: 7px;
  outline: none;
}
.TodoItem_delete {
  text-align: right;
}
.TodoItem_button {
  font-size: 17px;
  font-weight: bold;
  border: none;
  color: grey;
  background: lightgrey;
  border-radius: 100%;
  width: 25px;
  height: 25px;
  line-height: 20px;
  cursor: pointer;
  outline: none;
}

TodoBoardが作成できたら、AppTodoBoardを使用するように修正します。

src/App.tsx

import React from 'react';
import './App.css';
import { NavigationHeader } from './components/NavigationHeader';
import { TodoBoard } from './components/TodoBoard';

function App() {
  return (
    <React.Fragment>
      <NavigationHeader />
      <TodoBoard />
    </React.Fragment>
  );
}

export default App;

また、App.cssからTodoBoard.cssに抽出した定義を削除します。

ページの表示内容を確認すると、何も変わらず表示されていることを確認します。これでAppからTodoBoard部分の抽出は完了です。

TodoForm

TodoBoardコンポーネントをさらに子コンポーネントに分割するため、TodoForm.tsxを作成します。TodoFormが使用するCSSを分割するため、TodoForm.cssも作成します。

TodoFormが返すReact要素には、TodoBoardから該当部分を抽出します。

src/components/TodoForm.tsx

import React from 'react';
import './TodoForm.css';

export const TodoForm: React.FC = () => {
  return (
    <div className="TodoForm_content">
      <form className="TodoForm_form">
        <div className="TodoForm_input">
          <input type="text" placeholder="タスクを入力してください" />
        </div>
        <div className="TodoForm_button">
          <button type="button">追加</button>
        </div>
      </form>
    </div>
  );
};

src/components/TodoForm.css

.TodoForm_content {
  margin-top: 20px;
  margin-bottom: 20px;
}
.TodoForm_form {
  width: 100%;
  display: flex;
  justify-content: space-between;
}
.TodoForm_input {
  width: 86%;
}
.TodoForm_input input{
  float: left;
  width: 95%;
  border-radius: 5px;
  padding: 8px;
  border: solid 1px lightgray;
  background-color: #fafbfc;
  font-size: 16px;
  outline: none;
}
.TodoForm_input input:focus {
  background-color: white;
}
.TodoForm_button {
  text-align: center;
  width: 14%;
}
.TodoForm_button button {
  height: 35px;
  cursor: pointer;
  line-height: 1;
  font-size: 1rem;
  color: white;
  background-color: darkgreen;
  border-radius: 5px;
  padding: 0 15px;
  border: none;
  vertical-align: middle;
}
.TodoForm_button button:hover {
  background-color: green;
}

TodoFormが作成できたら、TodoBoardTodoFormを使用するように修正し、TodoBoard.cssからTodoForm.cssに抽出した定義を削除します。

ページの表示内容を確認すると、何も変わらず表示されていることを確認します。これでTodoBoardからTodoForm部分の抽出は完了です。

TodoFilter

TodoBoardコンポーネントをさらに子コンポーネントに分割するため、TodoFilter.tsxを作成します。TodoFilterが使用するCSSを分割するため、TodoFilter.cssも作成します。

TodoFilterが返すReact要素には、TodoBoardから該当部分を抽出します。

src/components/TodoFilter.tsx

import React from 'react';
import './TodoFilter.css';

export const TodoFilter: React.FC = () => {
  return (
    <div className="TodoFilter_content">
      <button className="TodoFilter_buttonSelected">全て</button>
      <button className="TodoFilter_buttonUnselected">未完了のみ</button>
      <button className="TodoFilter_buttonUnselected">完了のみ</button>
    </div>
  );
};

src/components/TodoFilter.css

.TodoFilter_content {
  text-align: right;
}
.TodoFilter_content button{
  margin-left: 5px;
}
.TodoFilter_buttonSelected {
  background-color: #31b3c7;
  border-width: 0;
  color: #fff;
  cursor: pointer;
  justify-content: center;
  padding: 7px 16px;
  text-align: center;
  white-space: nowrap;
  border-radius: 290486px;
  outline: none;
}
.TodoFilter_buttonUnselected {
  background-color: lightgray;
  border-width: 0;
  color: gray;
  cursor: pointer;
  justify-content: center;
  padding: 7px 16px;
  text-align: center;
  white-space: nowrap;
  border-radius: 290486px;
  outline: none;
}

TodoFilterが作成できたら、TodoBoardTodoFilterを使用するように修正し、TodoBoard.cssからTodoFilter.cssに抽出した定義を削除します。

ページの表示内容を確認すると、何も変わらず表示されていることを確認します。これでTodoBoardからTodoFilter部分の抽出は完了です。

TodoList

TodoBoardコンポーネントをさらに子コンポーネントに分割するため、TodoList.tsxを作成します。TodoListが使用するCSSを分割するため、TodoList.cssも作成します。

TodoListが返すReact要素には、TodoBoardから該当部分を抽出します。このコンポーネントにはいくつかの子コンポーネントがありますが、TodoBoard作成時と同様、一旦このコンポーネントで全て定義し、その後に分割していきます。

src/components/TodoList.tsx

import React from 'react';
import './TodoList.css';

export const TodoList: React.FC = () => {
  return (
    <ul className="TodoList_list">
      <li className="TodoItem_item">
        <div className="TodoItem_todo">
          <label>
            <input type="checkbox" className="TodoItem_checkbox" checked={true} />
            <span>洗い物をする</span>
          </label>
        </div>
        <div className="TodoItem_delete">
          <button className="TodoItem_button">x</button>
        </div>
      </li>
      <li className="TodoItem_item">
        <div className="TodoItem_todo">
          <label>
            <input type="checkbox" className="TodoItem_checkbox" />
            <span>洗濯物を干す</span>
          </label>
        </div>
        <div className="TodoItem_delete">
          <button className="TodoItem_button">x</button>
        </div>
      </li>
      <li className="TodoItem_item">
        <div className="TodoItem_todo">
          <label>
            <input type="checkbox" className="TodoItem_checkbox" />
            <span>買い物へ行く</span>
          </label>
        </div>
        <div className="TodoItem_delete">
          <button className="TodoItem_button">x</button>
        </div>
      </li>
    </ul>
  );
};

src/components/TodoList.css

.TodoList_list {
  list-style: none;
  padding: 0;
  margin: 20px 0;
}

.TodoItem_item {
  padding: 15px 10px;
  background: whitesmoke;
  margin-bottom: 10px;
}
.TodoItem_todo {
  float: left;
  text-align: left;
}
.TodoItem_checkbox {
  margin-right: 7px;
}
.TodoItem_delete {
  text-align: right;
}
.TodoItem_button {
  font-size: 17px;
  font-weight: bold;
  border: none;
  color: grey;
  background: lightgrey;
  border-radius: 100%;
  width: 25px;
  line-height: 20px;
  cursor: pointer;
}

TodoListが作成できたら、TodoBoardTodoListを使用するように修正し、TodoBoard.cssからTodoList.cssに抽出した定義を削除します。

ページの表示内容を確認すると、何も変わらず表示されていることを確認します。これでTodoBoardからTodoList部分の抽出は完了です。

この時点で、TodoBoardのコンポーネント分割は完了したため、TodoBoard.tsxは次のようになっています。

src/components/TodoBoard.tsx

import React from 'react';
import './TodoBoard.css';
import { TodoForm } from './TodoForm';
import { TodoFilter } from './TodoFilter';
import { TodoList } from './TodoList';

export const TodoBoard: React.FC = () => {
  return (
    <div className="TodoBoard_content">
      <TodoForm />
      <TodoFilter />
      <TodoList />
    </div>
  );
};

src/components/TodoBoard.css

.TodoBoard_content {
  margin-top: 10px;
  width: 40%;
  padding: 0 30%;
}

TodoItem

TodoListコンポーネントをさらに子コンポーネントに分割するため、TodoItem.tsxを作成します。TodoItemが使用するCSSを分割するため、TodoItem.cssも作成します。

TodoItemが返すReact要素には、TodoListから該当部分を抽出しますが、TodoItemは複数配置し、それぞれの表示内容が異なります。このような場合には、コンポーネントにプロパティを定義し、親コンポーネントから引数で値を受け取るようにします。(参考:コンポーネントに props を渡す

ここでは、TypeScriptの構文であるtypeを使用し、プロパティの型を定義した型エイリアスを定義します。それをコンポーネントの型であるReact.FCの型引数として渡すことで、コンポーネントの引数をそれらの型でチェックすることができます。

type Props = {
  text: string
  completed: boolean
}

受け取った引数は、ページ作成時に実装したcheckedと同様、中括弧で囲うことによりJSXで使用することができますので、TodoItem.tsxの実装は次のとおりになります。

src/components/TodoItem.tsx

import React from 'react';
import './TodoItem.css';

type Props = {
  text: string
  completed: boolean
}

export const TodoItem: React.FC<Props> = ({text, completed}) => {
  return (
    <li className="TodoItem_item">
      <div className="TodoItem_todo">
        <label>
          <input type="checkbox" className="TodoItem_checkbox" checked={completed} />
          <span>{text}</span>
        </label>
      </div>
      <div className="TodoItem_delete">
        <button className="TodoItem_button">x</button>
      </div>
    </li>
  );
};

TodoListでは、次のようにしてTodoItemのプロパティに値を設定します。

src/components/TodoList.tsx

import React from 'react';
import './TodoList.css';
import { TodoItem } from './TodoItem';

export const TodoList: React.FC = () => {
  return (
    <ul className="TodoList_list">
      <TodoItem text="洗い物をする" completed={true} />
      <TodoItem text="洗濯物を干す" completed={false} />
      <TodoItem text="買い物へ行く" completed={false} />
    </ul>
  );
};

CSSファイルは他とのコンポーネントと同様に抽出します。

src/components/TodoItem.css

.TodoItem_item {
  padding: 15px 10px;
  background: whitesmoke;
  margin-bottom: 10px;
}
.TodoItem_todo {
  float: left;
  text-align: left;
}
.TodoItem_checkbox {
  margin-right: 7px;
  outline: none;
}
.TodoItem_delete {
  text-align: right;
}
.TodoItem_button {
  font-size: 17px;
  font-weight: bold;
  border: none;
  color: grey;
  background: lightgrey;
  border-radius: 100%;
  width: 25px;
  height: 25px;
  line-height: 20px;
  cursor: pointer;
  outline: none;
}

src/components/TodoList.css

.TodoList_list {
  list-style: none;
  padding: 0;
  margin: 20px 0;
}

TodoItemが作成できたら、TodoListTodoItemを使用するように修正し、TodoList.cssからTodoItem.cssに抽出した定義を削除します。

ページの表示内容を確認すると、何も変わらず表示されていることを確認します。これでTodoListからTodoItem部分の抽出は完了です。

これで、コンポーネントの分割は完了です。

results matching ""

    No results matching ""