SignalDB: リアクティブローカルファーストデータベース入門
SignalDBは、ブラウザとNode.jsで動作するリアクティブなローカルファーストデータベースです。信号(Signal)ベースのリアクティビティにより、データ変更を自動的にUIに反映できます。
SignalDBとは
従来のクライアントサイドデータベース(IndexedDB、LocalStorage)と異なり、SignalDBは以下の特徴を持ちます。
主な特徴:
- リアクティブクエリ: データ変更を自動検知してUIを更新
- MongoDB風API: 直感的なクエリ構文
- TypeScript完全対応: 型安全なデータアクセス
- 永続化オプション: LocalStorage、IndexedDB、メモリ
- フレームワーク統合: React、Vue、Solidに対応
基本セットアップ
まずはSignalDBをインストールします。
npm install signaldb
# Reactで使う場合
npm install signaldb-plugin-react
シンプルなコレクション作成
import { Collection } from 'signaldb'
interface User {
id: string
name: string
email: string
createdAt: Date
}
// メモリ内コレクション
const users = new Collection<User>()
// データ追加
users.insert({
id: '1',
name: '山田太郎',
email: 'yamada@example.com',
createdAt: new Date()
})
CRUD操作
SignalDBはMongoDBと似たAPIを提供します。
Create(作成)
// 単一ドキュメント挿入
const userId = users.insert({
id: '2',
name: '佐藤花子',
email: 'sato@example.com',
createdAt: new Date()
})
// 複数ドキュメント挿入
users.insert([
{ id: '3', name: '鈴木一郎', email: 'suzuki@example.com', createdAt: new Date() },
{ id: '4', name: '田中次郎', email: 'tanaka@example.com', createdAt: new Date() }
])
Read(読み取り)
// 全件取得
const allUsers = users.find().fetch()
// 条件検索
const yamada = users.findOne({ name: '山田太郎' })
// 複雑なクエリ
const recentUsers = users.find({
createdAt: { $gte: new Date('2025-01-01') }
}).fetch()
// ソートと制限
const topUsers = users.find()
.sort({ createdAt: -1 })
.limit(10)
.fetch()
Update(更新)
// 単一ドキュメント更新
users.updateOne(
{ id: '1' },
{ $set: { name: '山田太郎(更新)' } }
)
// 複数ドキュメント更新
users.updateMany(
{ createdAt: { $lt: new Date('2024-01-01') } },
{ $set: { status: 'inactive' } }
)
// 増減操作
users.updateOne(
{ id: '1' },
{ $inc: { loginCount: 1 } }
)
Delete(削除)
// 条件に一致する最初のドキュメントを削除
users.removeOne({ id: '1' })
// 条件に一致する全ドキュメントを削除
users.removeMany({ status: 'inactive' })
// 全件削除
users.removeMany({})
リアクティブクエリ
SignalDBの真骨頂はリアクティブクエリです。データが変更されると、自動的にクエリ結果が更新されます。
Reactとの統合
import { useReactivityAdapter } from 'signaldb-plugin-react'
import { Collection } from 'signaldb'
// Reactアダプターを設定
const users = new Collection<User>({
reactivity: useReactivityAdapter()
})
リアクティブコンポーネント
import { useFind, useOne } from 'signaldb-plugin-react'
function UserList() {
// リアクティブクエリ - データ変更時に自動再レンダリング
const users = useFind(users.find())
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
)
}
function UserDetail({ userId }: { userId: string }) {
// 単一ドキュメントのリアクティブ取得
const user = useOne(users.findOne({ id: userId }))
if (!user) return <div>ユーザーが見つかりません</div>
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
}
リアルタイム検索
function SearchableUserList() {
const [query, setQuery] = useState('')
// クエリが変更されても自動的に再計算
const filteredUsers = useFind(
users.find({
$or: [
{ name: { $regex: query, $options: 'i' } },
{ email: { $regex: query, $options: 'i' } }
]
})
)
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="ユーザー検索..."
/>
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
永続化
データをブラウザに永続化することで、ページリロード後もデータを保持できます。
LocalStorageでの永続化
import { Collection } from 'signaldb'
import { LocalStorageAdapter } from 'signaldb/adapters'
const users = new Collection<User>({
persistence: new LocalStorageAdapter('users')
})
// データは自動的にLocalStorageに保存される
users.insert({ id: '1', name: '山田太郎', email: 'yamada@example.com', createdAt: new Date() })
IndexedDBでの永続化
import { IndexedDBAdapter } from 'signaldb/adapters'
const users = new Collection<User>({
persistence: new IndexedDBAdapter('myapp', 'users')
})
// より大きなデータセットに対応
for (let i = 0; i < 10000; i++) {
users.insert({
id: `${i}`,
name: `User ${i}`,
email: `user${i}@example.com`,
createdAt: new Date()
})
}
高度なクエリ
MongoDBスタイルのクエリオペレーターが使えます。
比較オペレーター
// 等価
users.find({ status: 'active' }).fetch()
// 不等価
users.find({ status: { $ne: 'deleted' } }).fetch()
// 大なり・小なり
users.find({ age: { $gte: 20, $lt: 30 } }).fetch()
// 配列内検索
users.find({ tags: { $in: ['premium', 'enterprise'] } }).fetch()
論理オペレーター
// AND(デフォルト)
users.find({
status: 'active',
age: { $gte: 18 }
}).fetch()
// OR
users.find({
$or: [
{ status: 'active' },
{ status: 'pending' }
]
}).fetch()
// NOT
users.find({
status: { $not: { $in: ['deleted', 'banned'] } }
}).fetch()
配列クエリ
interface Post {
id: string
title: string
tags: string[]
likes: number
}
const posts = new Collection<Post>()
// 配列に特定要素を含む
posts.find({ tags: 'JavaScript' }).fetch()
// すべての要素が条件を満たす
posts.find({ tags: { $all: ['JavaScript', 'TypeScript'] } }).fetch()
// 配列サイズ
posts.find({ tags: { $size: 3 } }).fetch()
パフォーマンス最適化
インデックス作成
// 検索頻度の高いフィールドにインデックスを作成
users.createIndex('email', { unique: true })
users.createIndex('name')
// 複合インデックス
users.createIndex(['status', 'createdAt'])
バッチ操作
// 複数操作をまとめて実行
users.bulkWrite([
{ insertOne: { document: { id: '1', name: 'User 1', email: 'user1@example.com', createdAt: new Date() } } },
{ updateOne: { filter: { id: '2' }, update: { $set: { name: 'Updated' } } } },
{ deleteOne: { filter: { id: '3' } } }
])
エラーハンドリング
try {
users.insert({ id: '1', name: 'Test', email: 'test@example.com', createdAt: new Date() })
users.insert({ id: '1', name: 'Duplicate', email: 'dup@example.com', createdAt: new Date() }) // エラー
} catch (error) {
console.error('重複IDエラー:', error)
}
実践例: ToDoアプリ
interface Todo {
id: string
text: string
completed: boolean
createdAt: Date
}
const todos = new Collection<Todo>({
reactivity: useReactivityAdapter(),
persistence: new LocalStorageAdapter('todos')
})
function TodoApp() {
const allTodos = useFind(todos.find().sort({ createdAt: -1 }))
const activeTodos = useFind(todos.find({ completed: false }))
const addTodo = (text: string) => {
todos.insert({
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date()
})
}
const toggleTodo = (id: string) => {
const todo = todos.findOne({ id })
if (todo) {
todos.updateOne({ id }, { $set: { completed: !todo.completed } })
}
}
const deleteTodo = (id: string) => {
todos.removeOne({ id })
}
return (
<div>
<h1>ToDoリスト({activeTodos.length}件)</h1>
{/* UI実装 */}
</div>
)
}
まとめ
SignalDBは、リアクティブなローカルファーストアプリケーションを構築するための強力なツールです。
メリット:
- リアクティブクエリによる自動UI更新
- 直感的なMongoDBスタイルAPI
- TypeScript完全対応
- 柔軟な永続化オプション
適したユースケース:
- オフライン対応アプリ
- リアルタイムダッシュボード
- ローカルファーストアプリ
- プロトタイピング
バックエンドAPIとの同期が必要な場合は、SignalDBとRESTful APIを組み合わせることで、オフライン機能を持つ堅牢なアプリケーションを構築できます。