この時点では、アプリは一枚岩です。アプリに何かをさせる前に、管理しやすく、記述しやすいコンポーネントに分解する必要があります。React には、何がコンポーネントで何がコンポーネントでないかという難しいルールはありません。それはあなた次第なのです!この記事では、アプリをコンポーネントに分解するための賢明な方法を紹介します。
前提条件: |
HTML、CSS、JavaScript のコア言語に精通していること、ターミナル/コマンドラインの知識があること。 |
---|---|
目的: | 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回繰り返すようになりました!
私たちは食べたいだけではなく、他にもやるべきこと — そう、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
を取得していることを確認したら、Eat
を name
のプロップで置き換えることができます。覚えておいてください: 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つの一意のタスクが表示されるようになりました。しかし、もう一つの問題が残っています: これらはすべてデフォルトでチェックされています。
それは 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
だけがチェックされていることが表示されるようになるはずです。
各 <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!
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 inTodo.js
. - Make yourself a new
Form()
component with the same basic structure asTodo()
, and export that component. - Copy the
<form>
tags and everything between them from insideApp.js
, and paste them insideForm()
’sreturn
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
- Introduction to client-side frameworks
- Framework main features
- React
- Ember
- Vue
- Getting started with Vue
- Creating our first Vue component
- Rendering a list of Vue components
- Adding a new todo form: Vue events, methods, and models
- Styling Vue components with CSS
- Using Vue computed properties
- Vue conditional rendering: editing existing todos
- Focus management with Vue refs
- Vue resources
- Svelte