ブログ
なぜこの記事を書いたか
Recoil でのデータ管理が含まれているコンポーネントを Storybook で動かしたかった。
ちゃんと探せばあったと思うけど、軽く探しても見つからなかったので雛形を作った。
※Recoil も Storybook も今日はじめて触ったので多分に間違っている可能性あり
構成
Storybook Get startedを流用したタスクリストがベース。
- /src
- /components
- Task.stories.tsx
- Task.tsx
- TaskList.stories.tsx
- TaskList.tsx
- stores
- taskStore.ts
- types
- storybook.d.ts
- App.tsx
tasks
をtaskStore.ts
で定義して Recoil で管理する。
TaskList.tsx
でtasks
の状態を変化させる。
Task.tsx
はtasks
の map から作成される。
解決策(TaskList.stories.tsx
)
モックコンポーネントを作る
const MockedComponent: React.FC<Props> = (props) => {};
recoil にセットしたい変数を props の定義に追加する
type Props = ComponentProps<typeof TaskList> & { tasks: TaskType[] };
props の値を recoil にセットする
// ※useSetRecoilStateの実装を隠蔽している
const setTasks = useTasksMutators().setTasks; // ※useSetRecoilStateの実装を隠蔽している
setTasks(props.tasks);
元のコンポーネントを返す
return <TaskList {...props} />;
モックコンポーネントを Default Export する
const meta: ComponentMeta<typeof MockedComponent> = {
component: MockedComponent,
decorators: [(story) => <RecoilRoot>{story()}</RecoilRoot>],
};
export default meta;
各ストーリーオブジェクトで props として recoil にセットしたい値を渡す
export const Default: ComponentStoryObj<typeof MockedComponent> = {
args: { tasks: defaultTasks, loading: false, onPinTask, onArchiveTask },
};
ソース
全体:https://github.com/k4a-dev/react-recoil-storybook
TaskList.stories.tsx
// ------------------ Import NPM Modules ------------------ //
import React, { ComponentProps } from "react";
import { ComponentMeta, ComponentStoryObj } from "@storybook/react";
import { RecoilRoot } from "recoil";
// ------------------ Import Modules ------------------ //
import TaskList from "./TaskList";
import { TaskType } from "./Task";
import { useTasksMutators } from "../stores/taskStore";
// ------------------ Recoil Mock------------------ //
type Props = ComponentProps<typeof TaskList> & { tasks: TaskType[] };
const MockedComponent: React.FC<Props> = (props) => {
const setTasks = useTasksMutators().setTasks;
setTasks(props.tasks);
return <TaskList {...props} />;
};
// ------------------ Define Args ------------------ //
const defaultTasks: TaskType[] = [
{ id: 1, title: "Task 1", state: "TASK_INBOX", updatedAt: "" },
{ id: 2, title: "Task 2", state: "TASK_INBOX", updatedAt: "" },
{ id: 3, title: "Task 3", state: "TASK_INBOX", updatedAt: "" },
{ id: 4, title: "Task 4", state: "TASK_INBOX", updatedAt: "" },
];
const onPinTask = () => {};
const onArchiveTask = () => {};
// ------------------ Export(Default) Meta ------------------ //
const meta: ComponentMeta<typeof MockedComponent> = {
component: MockedComponent,
decorators: [(story) => <RecoilRoot>{story()}</RecoilRoot>],
};
export default meta;
// ------------------ Export Stories ------------------ //
export const Default: ComponentStoryObj<typeof MockedComponent> = {
args: { tasks: defaultTasks, loading: false, onPinTask, onArchiveTask },
};
TaskList.tsx
import React from "react";
import Task, { EventProps } from "./Task";
import { useTasksState } from "../stores/taskStore";
type Props = {
loading: boolean;
} & EventProps;
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>
);
const TaskList: React.FC<Props> = (props) => {
// recoilで管理されたtasksState(※実装は隠蔽)
const tasks = useTasksState();
if (props.loading) {
return (
<div className="list-items">
{new Array(6).fill(null).map(() => LoadingRow)}
</div>
);
}
if (tasks.length === 0) {
return (
<div className="list-items">
<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>
);
}
return (
<div className="list-items">
{[
...tasks.filter((t) => t.state === "TASK_PINNED"),
...tasks.filter((t) => t.state !== "TASK_PINNED"),
].map((task) => (
<Task
key={task.id}
task={task}
{...{
onPinTask: props.onPinTask,
onArchiveTask: props.onArchiveTask,
}}
/>
))}
</div>
);
};
export default TaskList;
免責
-
実際の運用だと tasks はサーバサイドで管理するが、今回はローカルのグローバルステートとして扱う
-
この程度の実装なら もとから props で渡しちゃったほうが楽だけど、今回は recoil を使うのが目的