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の使い方を理解しておくと、自作する必要が出た際に便利なので、ぜひ活用してみてください。
参考
- https://storybook.js.org/docs/writing-stories/loaders
- https://github.com/mswjs/msw-storybook-addon/blob/v2.0.4/packages/msw-addon/src/loader.ts
一緒に二次元業界を盛り上げていきませんか?
株式会社viviONでは、フロントエンドエンジニアを募集しています。
また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。