0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

StorybookでIndexedDBを使えるようにする

Last updated at Posted at 2025-01-31

viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方はこちらまで。

はじめに

プロダクトでIndexedDBを使用しており、Storybook上でもIndexedDBの初期値を設定したい場面がありました。
しかし、サードパーティーのアドオンg存在しないので自作してみます。
また、今回はIndexedDBのラッパーライブラリのDexie.jsを使用します。

実際の実装はこちらのサンプルリポジトリをご参照ください。

環境

  • React@18.3.1
  • dexie@4.0.4
  • dexie-react-hook@1.1.7
  • storybook@8.5.2

DBを用意する

まず、アプリケーションで使用するデータベースのセットアップを行います。
最初にデータベースのテーブルの型を定義します。

export type User = {
  id: number;
  name: string;
};

export type Tables = {
  users: User[];
};

次にDexie.jsで使用するスキーマを定義します。

import { Tables } from "../types/tables";

export const schema = {
  users: "++id, name",
} as const satisfies Record<keyof Tables, string>;

最後に、データベースのクラスとインスタンスを作成する関数を実装します。
今回はReactを想定していますが、サンプルではContextなどを用いたインスタンス管理は行いません。

import Dexie, { Table } from "dexie";
import { User } from "../types/user";
import { schema } from "./schema";

export class MyDB extends Dexie {
  users!: Table<User>;

  constructor() {
    super(DB_NAME);
    this.migrate();
  }

  async clear() {
    await this.users.clear();
  }

  private migrate() {
    this.version(1).stores(schema);
  }
}

export let db: MyDB;

export function initializeDB(dbName: string) {
  db = new MyDB(dbName);
};

export function getDB() {
  if (!db) {
    throw new Error("Database not initialized");
  }

  return db;
}

IndexedDBのStorybookアドオンを作成する

StorybookでIndexedDBを使用するためにloaderを実装します。
本来、loaderはアセットの読み込み、APIの遅延読み込みなどを目的としていますが、msw-storybook-addonの実装を参考にloaderを用いて実装します。

アドオンの実装

まず、IndexedDBを初期化する関数を作成します。
Storybook終了時に可能な限りindexedDBのデータを破棄したいため、beforeunloadイベントでデータを破棄します。

import { initializeDB, getDB } from "../../src/persistence/db";
import { Tables } from "../../src/types/tables";

export const initializeIndexedDB = () => {
  try {
    initializeDB("db-for-storybook");
    const db = getDB();

    window.addEventListener("beforeunload", (e) => {
      db.delete();
    });
  } catch {
    console.error("Could not create database");
  }
};

次にloaderを実装します。
実装としてはparametersから渡されるシードデータをIndexedDBに保存するだけです。

また、Storybook上でツールバーを操作した際にも毎回ローダーが呼ばれます。
Dexie.jsのuseLiveQueryのようなリアクティブなフックを呼び出している場合、loaderによってデータが初期化され一瞬undefinedになるため、意図しないエラーになることがあります。

その対策として、前回と現在のparametersの値を比較し、変更がなければ処理をスキップしています。

export type IndexedDBParameters = {
  idb?: Tables;
};

type Context = {
  parameters: IndexedDBParameters;
};

let currentIdbSettings: Partial<Tables> = {};

export const indexedDBLoader = async ({ parameters }: Context) => {
  const { idb = {} } = parameters;
  const db = getDB();

  const previousIdbSettings = currentIdbSettings;
  currentIdbSettings = idb;

  if (currentIdbSettings === previousIdbSettings) {
    return {};
  }

  await Promise.all(db.tables.map((table) => table.clear()));
  await Promise.all(
    Object.entries(idb).map(([tableName, values]) =>
      db.table(tableName).bulkPut(values)
    )
  );

  return {};
};

最後に preview.tsでIndexedDBの初期化と作成したloaderを読み込めb完了です。

import type { Preview } from "@storybook/react";
import { initializeIndexedDB, indexedDBLoader } from "./addon-indexed-db";

initializeIndexedDB('app-for-storybook');

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
  },
  loaders: [indexedDBLoader],
};

export default preview;

各Storyのparameters.idbに使用したいデータを渡すことで、IndexedDBにデータを追加することができます。

import type { Meta, StoryObj } from "@storybook/react";

const UserList: FC = () => {}

const meta = {
  component: UserList,
} satisfies Meta<typeof UserList>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
  parameters: {
    idb: {
      users: [
        { id: 1, name: "foo" },
        { id: 2, name: "bar" },
        { id: 3, name: "hoge" },
      ],
    },
  },
};

export const Empty: Story = {};

また、parameters.idbの型推論が効かないので、storybook.d.tsを用意してParametersの型を上書きします。

import { Tables } from "../types/tables";

declare module "@storybook/types" {
  interface Parameters {
    idb?: Partial<Tables>;
  }
}

まとめ

今回はStorybookでIndexedDBを使用するアドオンを作成してみました。
loaderの使い方を理解しておくと、自作する必要が出た際に便利なので、ぜひ活用してみてください。

参考

一緒に二次元業界を盛り上げていきませんか?

株式会社viviONでは、フロントエンドエンジニアを募集しています。

また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?