記事一覧へ
react
プログラミング
recoil
storybook
作成日 : 2022-04-24

React×Recoil×Storybook を動かす

なぜこの記事を書いたか

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 },
}

ソース


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 を使うのが目的
次の記事

毎日文章を書き続けたいと思いもう数年が経っている

前の記事

storybookのArgsの型推論をオプショナルじゃなくする

記事一覧へ