複合的なコンポーネントを組み立てる
前の章では、最初のコンポーネントを作成しました。この章では、学習した内容を基にタスクのリストである TaskList
を作成します。それではコンポーネントを組み合わせて、複雑になった場合にどうすればよいか見てみましょう。
TaskList (タスクリスト)
Taskbox はピン留めされたタスクを通常のタスクより上部に表示することで強調します。これにより TaskList
に、タスクのリストが、通常のタスクのみである場合と、ピン留めされたタスクとの組み合わせである場合という、ストーリーを追加するべき 2 つのバリエーションができます。
Task
のデータは非同期的に送信されるので、接続がないことを示すため、読み込み中の状態も必要となります。さらにタスクがない場合に備え、空の状態も必要です。
セットアップする
複合的なコンポーネントも基本的なコンポーネントと大きな違いはありません。TaskList
のコンポーネントとそのストーリーファイル、src/components/TaskList.js
と src/components/TaskList.stories.js
を作成しましょう。
まずは TaskList
の大まかな実装から始めます。前の章で作成した Task
コンポーネントをインポートし、属性とアクションを入力として渡します。
import React from 'react';
import Task from './Task';
export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
const events = {
onPinTask,
onArchiveTask,
};
if (loading) {
return <div className="list-items">loading</div>;
}
if (tasks.length === 0) {
return <div className="list-items">empty</div>;
}
return (
<div className="list-items">
{tasks.map(task => (
<Task key={task.id} task={task} {...events} />
))}
</div>
);
}
次に Tasklist
のテスト状態をストーリーファイルに記述します。
import React from 'react';
import TaskList from './TaskList';
import * as TaskStories from './Task.stories';
export default {
component: TaskList,
title: 'TaskList',
decorators: [story => <div style={{ padding: '3rem' }}>{story()}</div>],
};
const Template = args => <TaskList {...args} />;
export const Default = Template.bind({});
Default.args = {
// Shaping the stories through args composition.
// The data was inherited from the Default story in Task.stories.js.
tasks: [
{ ...TaskStories.Default.args.task, id: '1', title: 'Task 1' },
{ ...TaskStories.Default.args.task, id: '2', title: 'Task 2' },
{ ...TaskStories.Default.args.task, id: '3', title: 'Task 3' },
{ ...TaskStories.Default.args.task, id: '4', title: 'Task 4' },
{ ...TaskStories.Default.args.task, id: '5', title: 'Task 5' },
{ ...TaskStories.Default.args.task, id: '6', title: 'Task 6' },
],
};
export const WithPinnedTasks = Template.bind({});
WithPinnedTasks.args = {
// Shaping the stories through args composition.
// Inherited data coming from the Default story.
tasks: [
...Default.args.tasks.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
],
};
export const Loading = Template.bind({});
Loading.args = {
tasks: [],
loading: true,
};
export const Empty = Template.bind({});
Empty.args = {
// Shaping the stories through args composition.
// Inherited data coming from the Loading story.
...Loading.args,
loading: false,
};
decorators
というキーをデフォルトエクスポートに追加し、描画するコンポーネントの周りに padding
を設定してます。ストーリーで使用する「プロバイダー」(例えば、React のコンテキストを設定するライブラリコンポーネントなど) を使うためにも使用します。
TaskStories
をインポートすることで、ストーリーに必要な引数 (args) を最小限の労力で組み合わせることができます。そうすることで、2 つのコンポーネントが想定するデータとアクション (呼び出しのモック) の一貫性が保たれます。
それでは TaskList
の新しいストーリーを Storybook で確認してみましょう。
状態を作りこむ
今のコンポーネントはまだ粗削りですが、ストーリーは見えています。単に .list-items
だけのためにラッパーを作るのは単純すぎると思うかもしれません。実際にその通りです。ほとんどの場合単なるラッパーのためだけに新しいコンポーネントは作りません。TaskList
の本当の複雑さは withPinnedTasks
、loading
、empty
といったエッジケースに現れているのです。
import React from 'react';
import Task from './Task';
export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
const events = {
onPinTask,
onArchiveTask,
};
const LoadingRow = (
<div className="loading-item">
<span className="glow-checkbox" />
<span className="glow-text">
<span>Loading</span> <span>cool</span> <span>state</span>
</span>
</div>
);
if (loading) {
return (
<div className="list-items" data-testid="loading" key={"loading"}>
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
</div>
);
}
if (tasks.length === 0) {
return (
<div className="list-items" key={"empty"} data-testid="empty">
<div className="wrapper-message">
<span className="icon-check" />
<div className="title-message">You have no tasks</div>
<div className="subtitle-message">Sit back and relax</div>
</div>
</div>
);
}
const tasksInOrder = [
...tasks.filter((t) => t.state === "TASK_PINNED"),
...tasks.filter((t) => t.state !== "TASK_PINNED"),
];
return (
<div className="list-items">
{tasksInOrder.map((task) => (
<Task key={task.id} task={task} {...events} />
))}
</div>
);
}
追加したマークアップで UI は以下のようになります:
リスト内のピン留めされたタスクの位置に注目してください。ピン留めされたタスクはユーザーにとって優先度を高くするため、リストの先頭に描画されます。
データ要件とプロパティ
コンポーネントが大きくなるにつれ、入力の要件も増えていきます。TaskList
のプロパティの要件を定義しましょう。Task
が子供のコンポーネントなので、Task
を表示するのに正しいデータ構造が渡されていることを確認しましょう。時間を節約するため、前の章で Task
に定義した propTypes
を再利用しましょう。
import React from 'react';
+ import PropTypes from 'prop-types';
import Task from './Task';
export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
...
}
+ TaskList.propTypes = {
+ /** Checks if it's in loading state */
+ loading: PropTypes.bool,
+ /** The list of tasks */
+ tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
+ /** Event to change the task to pinned */
+ onPinTask: PropTypes.func,
+ /** Event to change the task to archived */
+ onArchiveTask: PropTypes.func,
+ };
+ TaskList.defaultProps = {
+ loading: false,
+ };