React アプリのコンポーネント化

この記事は翻訳作業中です。

この時点では、アプリは一枚岩です。アプリに何かをさせる前に、管理しやすく、記述しやすいコンポーネントに分解する必要があります。React には、何がコンポーネントで何がコンポーネントでないかという難しいルールはありません。それはあなた次第なのです!この記事では、アプリをコンポーネントに分解するための賢明な方法を紹介します。

前提条件:

HTMLCSSJavaScript のコア言語に精通していること、ターミナル/コマンドラインの知識があること。

目的: Todo リストアプリをコンポーネントに分割するための賢明な方法を示すこと。

最初のコンポーネントの定義

コンポーネントの定義は、ある程度練習をするまでは難しいと思われるかもしれませんが、要点は以下の通りです。

  • アプリの明らかな "カタマリ" を表している場合、それはおそらくコンポーネントです。
  • よく再利用されるのであれば、それはおそらくコンポーネントです。

2 番目の箇条書きは特に価値があります: 一般的な UI 要素からコンポーネントを作成することで、コードを一箇所で変更することができ、そのコンポーネントが使用される場所のどこでも変更内容を確認することができます。また、すぐにすべてをコンポーネントに分割する必要もありません。2 つ目の箇条書きをヒントに、UI の中で最も再利用され、最も重要な部分である TODO リスト項目からコンポーネントを作成してみましょう。

<Todo />の作成

コンポーネントを作る前に、そのための新しいファイルを作らなければなりません。実は、コンポーネント用の新しいディレクトリの作成が必要です。次のコマンドは、components ディレクトリを作成し、その中に Todo.js というファイルを作成します。これらを実行する前に、アプリのルートにいることを確認してください!

mkdir src/components
touch src/components/Todo.js

新しい Todo.js ファイルは今は空です。ファイルを開いて最初の行に次を入力してください。

import React from "react";

今回は Todo というコンポーネントを作る予定なので、以下のように Todo.js にもそのためのコードを追加していきます。このコードでは、関数の定義とエクスポートを一行で定義しています。

export default function Todo() {
  return (

  );
}

ここまでは問題ありませんが、このコンポーネントは何かを返さなければなりません!src/App.js に戻って、最初の <li> をコピーし、Todo.js に貼り付けて、以下のように読み込みます。

export default function Todo() {
  return (
    <li className="todo stack-small">
      <div className="c-cb">
        <input id="todo-0" type="checkbox" defaultChecked={true} />
        <label className="todo-label" htmlFor="todo-0">
          Eat
        </label>
      </div>
      <div className="btn-group">
        <button type="button" className="btn">
          Edit <span className="visually-hidden">Eat</span>
        </button>
        <button type="button" className="btn btn__danger">
          Delete <span className="visually-hidden">Eat</span>
        </button>
      </div>
    </li>
  );
}

注意: コンポーネントは常に何かを返さなければなりません。もし今後あなたが何も返さないコンポーネントをレンダリングしようとしたら、React はブラウザにエラーを表示するでしょう。

これで Todo コンポーネントは完成しました。App.js で、ファイルの先頭付近に次の行を追加して Todo をインポートします。

import Todo from "./components/Todo";

このコンポーネントをインポートすると、App.js<li> 要素をすべて <Todo /> コンポーネント呼び出しに置き換えることができます。<ul> は以下のようになるはずです。

<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading"
>
  <Todo />
  <Todo />
  <Todo />
</ul>

ブラウザに戻ってみると、何か不幸なことに気づくでしょう: あなたのリストは、最初のタスクを3回繰り返すようになりました!

Our todo list app, with todo components repeating because the label is hardcoded into the component

私たちは食べたいだけではなく、他にもやるべきこと — そう、TODO があります。次に、異なるコンポーネント呼び出しで別々のコンテンツをレンダリングする方法を見てみましょう。

「一意の」 <Todo /> を作成

コンポーネントが強力なのは、UI の一部を再利用し、その UI のソースを 1 か所で参照できるからです。問題は、通常、各コンポーネントのすべてを再利用するのではなく、ほとんどの部分を再利用しつつ小さな部分を変更したいということです。そこでプロップ( props )の出番です。

name に何が入るでしょう?

完了させたいタスクの名前を追跡するために、それぞれの <Todo /> コンポーネントが一意の名前を表示するようにしなければなりません。

App.jsでは、それぞれの <Todo /> に名前のプロップを与えます。先ほどのタスクの名前を使ってみましょう。

<Todo name="Eat" />
<Todo name="Sleep" />
<Todo name="Repeat" />

ブラウザを更新すると...以前と全く同じものが表示されます。<Todo /> にプロップを与えましたが、まだ使っていません。Todo.js に戻って解決しましょう。

最初に Todo() 関数の定義を変更して、props をパラメータとして受け取るようにします。props がコンポーネントによって正しく受信されているかどうかを確認したい場合は、先ほどと同様に console.log()props を取得することができます。

コンポーネントが props を取得していることを確認したら、Eatname のプロップで置き換えることができます。覚えておいてください: JSX の式の途中では、中括弧を使って変数の値を注入します。

これらをまとめると、Todo() 関数は次のようになるはずです。

export default function Todo(props) {
  return (
    <li className="todo stack-small">
      <div className="c-cb">
        <input id="todo-0" type="checkbox" defaultChecked={true} />
        <label className="todo-label" htmlFor="todo-0">
          {props.name}
        </label>
      </div>
      <div className="btn-group">
        <button type="button" className="btn">
          Edit <span className="visually-hidden">{props.name}</span>
        </button>
        <button type="button" className="btn btn__danger">
          Delete <span className="visually-hidden">{props.name}</span>
        </button>
      </div>
    </li>
  );
}

これで、ブラウザには3つの一意のタスクが表示されるようになりました。しかし、もう一つの問題が残っています: これらはすべてデフォルトでチェックされています。

Our todo list, with different todo labels now they are passed into the components as props

それは completed ですか?

元の静的リストでは、Eat だけがチェックされていました。もう一度言いますが、<Todo />コンポーネントを構成するUIのほとんどを再利用しつつ、一つだけ変更したいのです。これは別のプロップが良い仕事をしてくれます!App.js での各 <Todo /> の呼び出しには、完了したことを示す新しいプロップを与えます。最初の (Eat) は true の値を持ち、残りは false にします。

<Todo name="Eat" completed={true} />
<Todo name="Sleep" completed={false} />
<Todo name="Repeat" completed={false} />

先ほどと同様に、これらのプロップを実際に使用するためには Todo.js に戻る必要があります。<input />defaultChecked 属性の値が completed したプロップと同じになるように変更します。これで、Todo コンポーネントの <input /> 要素は次のようになります。

<input id="todo-0" type="checkbox" defaultChecked={props.completed} />

そして、ブラウザを更新すると、Eat だけがチェックされていることが表示されるようになるはずです。

Our todo list app, now with differing checked states - some checkboxes are checked, others not

<Todo /> コンポーネントのconpleted プロップを変更すると、ブラウザはそれに応じてレンダリングされた同等のチェックボックスをチェックしたり、チェックを外したりします。

id をください

現在、<Todo /> コンポーネントはすべてのタスクに todo-0 という id 属性を与えています。これは悪い HTML です、なぜなら id 属性は一意でなければならないからです (CSS や JavaScript などでドキュメントフラグメントの一意な識別子として使用されます)。つまり、各 Todo に対して一意の値を取る id プロップをコンポーネントに与えるべきです。

最初と同じパターンに従うために、<Todo /> コンポーネントの各インスタンスに todo-i の形式で ID を与えてみましょう。i は毎回1つずつ大きくなっていきます。

<Todo name="Eat" completed={true} id="todo-0" />
<Todo name="Sleep" completed={false} id="todo-1" />
<Todo name="Repeat" completed={false} id="todo-2" />

ここで Todo.js に戻り、id プロップを使うようにします。これは <input /> 要素の id 属性の値とラベルの htmlFor 属性の値を置き換える必要があります。

<div className="c-cb">
  <input id={props.id} type="checkbox" defaultChecked={props.completed} />
  <label className="todo-label" htmlFor={props.id}>
    {props.name}
  </label>
</div>

ここまでは順調ですか?

今のところ React をうまく使っていますが、もっとうまくできるかもしれません!今のコードは反復的です。<Todo /> コンポーネントをレンダリングする3つの行はほぼ同じですが、1つだけ違う点があります: それぞれのプロップの値です。

JavaScript のコアな能力の一つであるイテレーション(反復処理)を使えば、コードをクリーンアップすることができます。イテレーションを使うためには、まず自分のタスクを再考する必要があります。

データとしてのタスク

それぞれのタスクには現在、3つの情報が含まれています: 名前、チェック済みかどうか、そして一意なIDです。このデータはうまくオブジェクトに変換されます。複数のタスクがあるので、このデータを表現するにはオブジェクトの配列が有効です。

import の後 ReactDOM.render() より前の行で以下の const を作成してください。

const DATA = [
  { id: "todo-0", name: "Eat", completed: true },
  { id: "todo-1", name: "Sleep", completed: false },
  { id: "todo-2", name: "Repeat", completed: false }
];

次に、tasks という名前のプロップとして <App />DATA を渡します。src/index.js の最終行は以下のようになるはずです。

ReactDOM.render(<App tasks={DATA} />, document.getElementById("root"));

この配列は、App コンポーネントで props.tasks として利用できるようになりました。よかったら console.log() で確認してください。

注意: ALL_CAPS 定数名は JavaScript では特別な意味はありません; 他の開発者に「このデータはここで定義された後は変更されません」と伝えるための慣習です。

イテレーションによるレンダリング

オブジェクトの配列をレンダリングするには、それぞれのオブジェクトを <Todo /> コンポーネントに変換しなければなりません。JavaScript では、データを別のものに変換するための配列メソッド Array.prototype.map() を提供しています。

Above the return statement of App(), make a new const called taskList and use map() to transform it. Let's start by turning our tasks array into something simple: the name of each task:

const taskList = props.tasks.map(task => task.name);

Let’s try replacing all the children of the <ul> with taskList:

<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading"
>
  {taskList}
</ul>

This gets us some of the way towards showing all the components again, but we’ve got more work to do: the browser currently renders each task's name as unstructured text. We’re missing our HTML structure — the <li> and its checkboxes and buttons!

Our todo list app with the todo item labels just shown bunched up on one line

To fix this, we need to return a <Todo /> component from our map() function — remember that JSX allows us to mix up JavaScript and markup structures! Let's try the following instead of what we have already:

 const taskList = props.tasks.map(task => <Todo />);

Look again at your app; now our tasks look more like they used to, but they’re missing the names of the tasks themselves.  Remember that each task we map over has the id, name, and checked properties we want to pass into our <Todo /> component. If we put that knowledge together, we get code like this:

const taskList = props.tasks.map(task => (
  <Todo id={task.id} name={task.name} completed={task.completed} />
));

Now the app looks like it did before, and our code is less repetitive.

Unique keys

Now that React is rendering our tasks out of an array, it has to keep track of which one is which in order to render them properly. React tries to do its own guesswork to keep track of things, but we can help it out by passing a key prop to our <Todo /> components. key is a special prop that's managed by React – you cannot use the word key for any other purpose.

Because keys should be unique, we're going to re-use the id of each task object as its key. Update your taskList constant like so:

const taskList = props.tasks.map(task => (
    <Todo
      id={task.id}
      name={task.name}
      completed={task.completed}
      key={task.id}
    />
  )
);

You should always pass a unique key to anything you render with iteration. Nothing obvious will change in your browser, but if you do not use unique keys, React will log warnings to your console and your app may behave strangely!

Componentizing the rest of the app

Now that we've got our most important component sorted out, we can turn the rest of our app into components. Remembering that components are either obvious pieces of UI, or reused pieces of UI, or both, we can make two more components:

  • <Form/>
  • <FilterButton/>

Since we know we need both, we can batch some of the file creation work together with a terminal command. Run this command in your terminal, taking care that you're in the root directory of your app:

touch src/components/Form.js src/components/FilterButton.js

The <Form />

Open components/Form.js and do the following:

  • Import React at the top of the file, like we did in Todo.js.
  • Make yourself a new Form() component with the same basic structure as Todo(), and export that component.
  • Copy the <form> tags and everything between them from inside App.js, and paste them inside Form()’s return statement.
  • Export Form at the end of the file.

Your Form.js file should read like this:

import React from "react";

function Form(props) {
  return (
    <form>
      <h2 className="label-wrapper">
        <label htmlFor="new-todo-input" className="label__lg">
          What needs to be done?
        </label>
      </h2>
      <input
        type="text"
        id="new-todo-input"
        className="input input__lg"
        name="text"
        autoComplete="off"
      />
      <button type="submit" className="btn btn__primary btn__lg">
        Add
      </button>
    </form>
  );
}

export default Form;

The <FilterButton />

Do the same things you did to create Form.js inside FilterButton.js, but call the component FilterButton() and copy the HTML for the first button inside the <div> element with the class of filters from App.js into the return statement.

The file should read like this:

import React from "react";

function FilterButton(props) {
  return (
    <button type="button" className="btn toggle-btn" aria-pressed="true">
      <span className="visually-hidden">Show </span>
      <span>all </span>
      <span className="visually-hidden"> tasks</span>
    </button>
  );
}

export default FilterButton;

Note: You might notice that we are making the same mistake here as we first made for the <Todo /> component, in that each button will be the same. That’s fine! We’re going to fix up this component later on, in Back to the filter buttons.

Importing all our components

Let's make use of our new components.

Add some more import statements to the top of App.js, to import them.

Then, update the return statement of App() so that it renders our components. When you’re done, App.js will read like this:

import React from "react";
import Form from "./components/Form";
import FilterButton from "./components/FilterButton";
import Todo from "./components/Todo";

function App(props) {
  const taskList = props.tasks.map(task => (
    <Todo
        id={task.id}
        name={task.name}
        completed={task.completed}
        key={task.id}
      />
    )
  );
  return (
    <div className="todoapp stack-large">
      <Form />
      <div className="filters btn-group stack-exception">
        <FilterButton />
        <FilterButton />
        <FilterButton />
      </div>
      <h2 id="list-heading">3 tasks remaining</h2>
      <ul
        role="list"
        className="todo-list stack-large stack-exception"
        aria-labelledby="list-heading"
      >
        {taskList}
      </ul>
    </div>
  );
}

export default App;

With this in place, we’re almost ready to tackle some interactivity in our React app!

Summary

And that's it for this article — we've gone into some depth on how to break up your app nicely into components, end render them efficiently. Now we'll go on to look at how we handle events in React, and start adding some interactivity.

In this module