logo
/
React×Recoil×Storybook を動かす
2022-04-24
ブログ

なぜこの記事を書いたか

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
taskstaskStore.tsで定義して Recoil で管理する。
TaskList.tsxtasksの状態を変化させる。
Task.tsxtasksの 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 を使うのが目的