Signal・Store・Memo 完全解説:SolidJS の状態管理パターン

SolidJS では、状態管理において 3 つの主要な仕組みが提供されています。Signal、Store、そして Memo です。これらはそれぞれ異なる特性を持ち、適切に使い分けることで高パフォーマンスなアプリケーションを構築できます。
本記事では、それぞれの仕組みを詳しく解説し、実践的な使い分けパターンまでご紹介いたします。React や Vue.js からの移行を検討している方にとっても、SolidJS の状態管理の魅力を理解していただけるでしょう。
Signal の基本概念と仕組み
Signal とは何か
Signal は SolidJS における最も基本的な状態管理の仕組みです。リアクティブなデータの格納と更新を担当し、値が変更された際に自動的に依存するコンポーネントを再レンダリングします。
React の useState
に似ていますが、SolidJS では Virtual DOM を使わない細粒度更新を実現しているため、より効率的な更新が可能です。
typescriptimport { createSignal } from 'solid-js';
// 基本的なSignalの作成
const [count, setCount] = createSignal(0);
// 現在の値を取得
console.log(count()); // 0
// 値を更新
setCount(5);
console.log(count()); // 5
createSignal の基本的な使い方
createSignal
は配列を返し、最初の要素がゲッター関数、2 番目の要素がセッター関数となります。この設計により、値の取得と設定を明確に分離できます。
typescriptimport { createSignal, Component } from 'solid-js';
const Counter: Component = () => {
// 初期値0でSignalを作成
const [count, setCount] = createSignal(0);
// インクリメント関数
const increment = () => {
setCount(count() + 1);
};
// デクリメント関数
const decrement = () => {
setCount(count() - 1);
};
return (
<div>
<p>カウント: {count()}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
);
};
Signal のリアクティブシステム
SolidJS の Signal は、依存関係を自動的に追跡するリアクティブシステムを持っています。Signal の値が変更されると、その値を参照しているすべてのコンポーネントや計算が自動的に更新されます。
typescriptimport { createSignal, createEffect } from 'solid-js';
const App = () => {
const [name, setName] = createSignal('太郎');
const [age, setAge] = createSignal(25);
// Signalの変更を監視するEffect
createEffect(() => {
console.log(`${name()}さんは${age()}歳です`);
});
// nameが変更されると、自動的にeffectが実行される
setTimeout(() => setName('花子'), 2000);
// ageが変更されても、同様にeffectが実行される
setTimeout(() => setAge(30), 4000);
return (
<div>
<h1>{name()}さんのプロフィール</h1>
<p>年齢: {age()}歳</p>
<button onClick={() => setAge(age() + 1)}>
誕生日
</button>
</div>
);
};
よくある使用例と注意点
Signal は様々な場面で活用できますが、適切な使い方を理解することが重要です。
フォーム状態の管理
typescriptconst LoginForm = () => {
const [email, setEmail] = createSignal('');
const [password, setPassword] = createSignal('');
const [isLoading, setIsLoading] = createSignal(false);
const [error, setError] = createSignal<string | null>(
null
);
const handleSubmit = async (e: Event) => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
// API呼び出しの例
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email(),
password: password(),
}),
});
if (!response.ok) {
throw new Error('ログインに失敗しました');
}
// 成功時の処理
console.log('ログイン成功');
} catch (err) {
setError(
err instanceof Error
? err.message
: '不明なエラーが発生しました'
);
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>メールアドレス:</label>
<input
type='email'
value={email()}
onInput={(e) => setEmail(e.target.value)}
disabled={isLoading()}
/>
</div>
<div>
<label>パスワード:</label>
<input
type='password'
value={password()}
onInput={(e) => setPassword(e.target.value)}
disabled={isLoading()}
/>
</div>
{error() && (
<div style={{ color: 'red' }}>
エラー: {error()}
</div>
)}
<button type='submit' disabled={isLoading()}>
{isLoading() ? 'ログイン中...' : 'ログイン'}
</button>
</form>
);
};
よくあるエラーと対処法
エラー 1: Signal is not a function
vbnetTypeError: count is not a function
このエラーは、Signal の値を取得する際に関数呼び出し ()
を忘れた場合に発生します。
typescript// ❌ 間違い
const [count, setCount] = createSignal(0);
console.log(count); // Signalオブジェクトが出力される
// ✅ 正しい
console.log(count()); // 0が出力される
エラー 2: Cannot call a class as a function
vbnetTypeError: Cannot call a class as a function
セッター関数を誤って呼び出し忘れた場合に発生します。
typescript// ❌ 間違い
setCount = 5; // 代入してしまっている
// ✅ 正しい
setCount(5); // 関数として呼び出す
エラー 3: Maximum call stack size exceeded
arduinoRangeError: Maximum call stack size exceeded
Signal の更新時に無限ループが発生した場合のエラーです。
typescript// ❌ 無限ループを引き起こす例
createEffect(() => {
setCount(count() + 1); // Effect内でSignalを更新すると無限ループ
});
// ✅ 正しい例
const increment = () => {
setCount(count() + 1);
};
Store による複雑な状態管理
Store の概念と利点
Signal が単一の値を管理するのに対し、Store は複雑なオブジェクトや配列などの構造化されたデータの管理に特化しています。Store を使用することで、ネストしたオブジェクトの部分更新を効率的に行えます。
Store の主な利点は以下の通りです:
特徴 | Signal | Store |
---|---|---|
データ構造 | 単一の値 | オブジェクト・配列 |
更新粒度 | 全体更新 | 部分更新 |
パフォーマンス | シンプルな値に最適 | 複雑な構造に最適 |
メモリ効率 | 高い | 更新された部分のみ |
createStore の基本操作
createStore
は、オブジェクトをリアクティブにし、その変更を効率的に追跡します。
typescriptimport { createStore } from 'solid-js/store';
// 基本的なStoreの作成
const [user, setUser] = createStore({
name: '田中太郎',
age: 30,
email: 'tanaka@example.com',
preferences: {
theme: 'light',
language: 'ja',
},
});
// 値の読み取り
console.log(user.name); // '田中太郎'
console.log(user.preferences.theme); // 'light'
// 値の更新
setUser('name', '佐藤花子');
setUser('preferences', 'theme', 'dark');
// 複数のプロパティを同時に更新
setUser({
age: 31,
email: 'sato@example.com',
});
ネストした状態の管理
Store は深くネストしたオブジェクトの管理において真価を発揮します。従来の状態管理では困難だった、深い階層の部分更新を簡単に実現できます。
typescriptinterface TodoItem {
id: number;
text: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
tags: string[];
createdAt: Date;
updatedAt: Date;
}
interface AppState {
todos: TodoItem[];
filters: {
status: 'all' | 'active' | 'completed';
priority: 'all' | 'low' | 'medium' | 'high';
tags: string[];
};
ui: {
isLoading: boolean;
error: string | null;
selectedTodoId: number | null;
};
}
const TodoApp = () => {
const [state, setState] = createStore<AppState>({
todos: [],
filters: {
status: 'all',
priority: 'all',
tags: [],
},
ui: {
isLoading: false,
error: null,
selectedTodoId: null,
},
});
// Todo追加関数
const addTodo = (text: string) => {
const newTodo: TodoItem = {
id: Date.now(),
text,
completed: false,
priority: 'medium',
tags: [],
createdAt: new Date(),
updatedAt: new Date(),
};
setState('todos', (todos) => [...todos, newTodo]);
};
// Todo完了切り替え関数
const toggleTodo = (id: number) => {
setState(
'todos',
(todo) => todo.id === id,
'completed',
(completed) => !completed
);
// 更新日時も同時に更新
setState(
'todos',
(todo) => todo.id === id,
'updatedAt',
new Date()
);
};
// フィルター更新関数
const updateFilter = (
filterType: keyof AppState['filters'],
value: any
) => {
setState('filters', filterType, value);
};
// エラー設定関数
const setError = (error: string | null) => {
setState('ui', 'error', error);
};
// ローディング状態切り替え
const setLoading = (isLoading: boolean) => {
setState('ui', 'isLoading', isLoading);
};
return (
<div>
{state.ui.isLoading && <div>読み込み中...</div>}
{state.ui.error && (
<div style={{ color: 'red' }}>
エラー: {state.ui.error}
</div>
)}
<div>
<button onClick={() => addTodo('新しいタスク')}>
タスク追加
</button>
</div>
<div>
{state.todos.map((todo) => (
<div key={todo.id}>
<input
type='checkbox'
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span
style={{
textDecoration: todo.completed
? 'line-through'
: 'none',
}}
>
{todo.text}
</span>
<span>優先度: {todo.priority}</span>
</div>
))}
</div>
</div>
);
};
Store のパフォーマンス特性
Store は、変更された部分のみを追跡する細粒度更新により、高いパフォーマンスを実現します。
typescript// パフォーマンス測定の例
const PerformanceTest = () => {
const [largeState, setLargeState] = createStore({
items: Array.from({ length: 10000 }, (_, i) => ({
id: i,
value: Math.random(),
selected: false,
})),
metadata: {
totalCount: 10000,
selectedCount: 0,
},
});
// 特定のアイテムのみを更新
const updateSingleItem = (id: number) => {
const start = performance.now();
setLargeState(
'items',
(item) => item.id === id,
'selected',
(selected) => !selected
);
const end = performance.now();
console.log(`更新時間: ${end - start}ms`);
};
// よくあるパフォーマンスの問題と解決策
// ❌ 非効率な全体更新
const inefficientUpdate = () => {
setLargeState(
'items',
largeState.items.map((item) =>
item.id === 100
? { ...item, selected: !item.selected }
: item
)
);
};
// ✅ 効率的な部分更新
const efficientUpdate = () => {
setLargeState(
'items',
(item) => item.id === 100,
'selected',
(selected) => !selected
);
};
return (
<div>
<button onClick={() => updateSingleItem(100)}>
アイテム100を更新
</button>
<div>
選択されたアイテム数:{' '}
{
largeState.items.filter((item) => item.selected)
.length
}
</div>
</div>
);
};
Store でよくあるエラーと対処法
エラー 1: Cannot add property, object is not extensible
csharpTypeError: Cannot add property 'newField', object is not extensible
Store オブジェクトに存在しないプロパティを追加しようとした場合のエラーです。
typescript// ❌ 存在しないプロパティへの追加
const [state, setState] = createStore({ name: '太郎' });
setState('age', 30); // エラー
// ✅ 初期状態で定義するか、reconcile を使用
const [state, setState] = createStore({
name: '太郎',
age: undefined as number | undefined,
});
setState('age', 30); // OK
// または reconcile を使用
import { reconcile } from 'solid-js/store';
setState(reconcile({ ...state, age: 30 }));
エラー 2: Cannot read property of undefined
javascriptTypeError: Cannot read property 'theme' of undefined
ネストしたオブジェクトが未定義の場合に発生するエラーです。
typescript// ❌ ネストしたオブジェクトが未定義
const [state, setState] = createStore<{
user?: { preferences?: { theme: string } };
}>({});
console.log(state.user.preferences.theme); // エラー
// ✅ 適切なデフォルト値の設定
const [state, setState] = createStore({
user: {
preferences: {
theme: 'light',
},
},
});
// または条件付きアクセス
console.log(state.user?.preferences?.theme);
Memo による計算値の最適化
createMemo の役割と重要性
createMemo
は、計算コストが高い処理をキャッシュし、依存する値が変更された時のみ再計算を行う仕組みです。React の useMemo
に似ていますが、SolidJS では自動的な依存関係追跡により、より効率的な動作を実現します。
Memo が特に有効な場面:
シーン | 従来の問題 | Memo による解決 |
---|---|---|
重い計算処理 | 毎回再計算される | 依存値変更時のみ実行 |
フィルタリング | 全データを毎回処理 | 元データ変更時のみ |
API レスポンス加工 | レンダリング毎に処理 | データ更新時のみ |
複雑な条件判定 | 毎回全条件をチェック | 関連値変更時のみ |
依存関係の自動追跡
SolidJS の Memo は、実行時に参照される Signal や Store を自動的に追跡し、それらの値が変更された際にのみ再計算を行います。
typescriptimport { createSignal, createMemo } from 'solid-js';
const ShoppingCart = () => {
const [items, setItems] = createSignal([
{ id: 1, name: 'リンゴ', price: 150, quantity: 3 },
{ id: 2, name: 'バナナ', price: 200, quantity: 2 },
{ id: 3, name: 'オレンジ', price: 180, quantity: 1 },
]);
const [taxRate, setTaxRate] = createSignal(0.1);
const [discountRate, setDiscountRate] =
createSignal(0.05);
// 小計の計算(重い処理をシミュレート)
const subtotal = createMemo(() => {
console.log('小計を計算中...'); // この処理は必要な時のみ実行される
return items().reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
});
// 税込み価格の計算
const totalWithTax = createMemo(() => {
console.log('税込み価格を計算中...');
return subtotal() * (1 + taxRate());
});
// 最終価格の計算(割引適用後)
const finalPrice = createMemo(() => {
console.log('最終価格を計算中...');
return totalWithTax() * (1 - discountRate());
});
// アイテム数の計算
const totalItems = createMemo(() => {
return items().reduce(
(total, item) => total + item.quantity,
0
);
});
// 商品追加関数
const addItem = (name: string, price: number) => {
setItems((prev) => [
...prev,
{
id: Date.now(),
name,
price,
quantity: 1,
},
]);
};
// 数量更新関数
const updateQuantity = (id: number, quantity: number) => {
setItems((prev) =>
prev.map((item) =>
item.id === id ? { ...item, quantity } : item
)
);
};
return (
<div>
<h2>ショッピングカート</h2>
<div>
{items().map((item) => (
<div key={item.id}>
<span>
{item.name} - ¥{item.price}
</span>
<input
type='number'
value={item.quantity}
min='0'
onChange={(e) =>
updateQuantity(
item.id,
parseInt(e.target.value) || 0
)
}
/>
</div>
))}
</div>
<button onClick={() => addItem('ぶどう', 300)}>
商品追加
</button>
<div>
<p>商品数: {totalItems()}個</p>
<p>小計: ¥{subtotal().toLocaleString()}</p>
<p>税込み: ¥{totalWithTax().toLocaleString()}</p>
<p>最終価格: ¥{finalPrice().toLocaleString()}</p>
</div>
<div>
<label>
税率:
<input
type='number'
value={taxRate() * 100}
onChange={(e) =>
setTaxRate(parseFloat(e.target.value) / 100)
}
step='0.1'
/>%
</label>
</div>
<div>
<label>
割引率:
<input
type='number'
value={discountRate() * 100}
onChange={(e) =>
setDiscountRate(
parseFloat(e.target.value) / 100
)
}
step='0.1'
/>%
</label>
</div>
</div>
);
};
パフォーマンス最適化のポイント
Memo を効果的に活用するための重要なポイントをご紹介します。
重い計算処理のメモ化
typescriptimport { createSignal, createMemo } from 'solid-js';
const DataAnalysis = () => {
const [rawData, setRawData] = createSignal<number[]>([]);
const [filterThreshold, setFilterThreshold] =
createSignal(50);
// 生データから統計情報を計算(重い処理)
const statistics = createMemo(() => {
const data = rawData();
if (data.length === 0) return null;
console.log('統計計算を実行中...'); // デバッグ用
// 重い計算処理をシミュレート
const sorted = [...data].sort((a, b) => a - b);
const sum = data.reduce((acc, val) => acc + val, 0);
const mean = sum / data.length;
// 分散の計算
const variance =
data.reduce((acc, val) => {
return acc + Math.pow(val - mean, 2);
}, 0) / data.length;
const standardDeviation = Math.sqrt(variance);
return {
count: data.length,
sum,
mean,
median: sorted[Math.floor(sorted.length / 2)],
min: sorted[0],
max: sorted[sorted.length - 1],
variance,
standardDeviation,
};
});
// フィルタリングされたデータ
const filteredData = createMemo(() => {
const threshold = filterThreshold();
return rawData().filter((value) => value >= threshold);
});
// フィルタリング後の統計
const filteredStatistics = createMemo(() => {
const data = filteredData();
if (data.length === 0) return null;
const sum = data.reduce((acc, val) => acc + val, 0);
return {
count: data.length,
sum,
mean: sum / data.length,
min: Math.min(...data),
max: Math.max(...data),
};
});
// データ生成関数
const generateRandomData = (count: number) => {
const data = Array.from({ length: count }, () =>
Math.floor(Math.random() * 100)
);
setRawData(data);
};
return (
<div>
<h2>データ分析ダッシュボード</h2>
<div>
<button onClick={() => generateRandomData(1000)}>
1000件のデータを生成
</button>
<button onClick={() => generateRandomData(10000)}>
10000件のデータを生成
</button>
</div>
<div>
<label>
フィルター閾値:
<input
type='range'
min='0'
max='100'
value={filterThreshold()}
onInput={(e) =>
setFilterThreshold(parseInt(e.target.value))
}
/>
{filterThreshold()}
</label>
</div>
{statistics() && (
<div>
<h3>全データ統計</h3>
<table>
<tbody>
<tr>
<td>件数</td>
<td>{statistics()!.count}</td>
</tr>
<tr>
<td>合計</td>
<td>
{statistics()!.sum.toLocaleString()}
</td>
</tr>
<tr>
<td>平均</td>
<td>{statistics()!.mean.toFixed(2)}</td>
</tr>
<tr>
<td>中央値</td>
<td>{statistics()!.median}</td>
</tr>
<tr>
<td>最小値</td>
<td>{statistics()!.min}</td>
</tr>
<tr>
<td>最大値</td>
<td>{statistics()!.max}</td>
</tr>
<tr>
<td>標準偏差</td>
<td>
{statistics()!.standardDeviation.toFixed(
2
)}
</td>
</tr>
</tbody>
</table>
</div>
)}
{filteredStatistics() && (
<div>
<h3>フィルター後統計</h3>
<table>
<tbody>
<tr>
<td>件数</td>
<td>{filteredStatistics()!.count}</td>
</tr>
<tr>
<td>平均</td>
<td>
{filteredStatistics()!.mean.toFixed(2)}
</td>
</tr>
<tr>
<td>最小値</td>
<td>{filteredStatistics()!.min}</td>
</tr>
<tr>
<td>最大値</td>
<td>{filteredStatistics()!.max}</td>
</tr>
</tbody>
</table>
</div>
)}
</div>
);
};
メモ化のベストプラクティス
1. 適切な粒度でのメモ化
typescript// ❌ 過度に細かいメモ化
const TooGranular = () => {
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
// これらは軽い計算なので、メモ化の必要性は低い
const aPlusOne = createMemo(() => a() + 1);
const bMinusOne = createMemo(() => b() - 1);
const sum = createMemo(() => aPlusOne() + bMinusOne());
// ...
};
// ✅ 適切な粒度でのメモ化
const Appropriate = () => {
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
// 複数の計算をまとめてメモ化
const calculations = createMemo(() => ({
aPlusOne: a() + 1,
bMinusOne: b() - 1,
sum: a() + 1 + (b() - 1),
}));
// ...
};
2. 非同期処理との組み合わせ
typescriptimport {
createSignal,
createMemo,
createResource,
} from 'solid-js';
const AsyncMemoExample = () => {
const [userId, setUserId] = createSignal(1);
// ユーザーデータを取得するResource
const [userData] = createResource(userId, async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
// ユーザーデータが取得できた時のみ計算を実行
const userProfile = createMemo(() => {
const user = userData();
if (!user) return null;
return {
fullName: `${user.firstName} ${user.lastName}`,
age:
new Date().getFullYear() -
new Date(user.birthDate).getFullYear(),
isAdult:
new Date().getFullYear() -
new Date(user.birthDate).getFullYear() >=
20,
profileComplete: Boolean(
user.firstName && user.lastName && user.email
),
};
});
return (
<div>
<select
value={userId()}
onChange={(e) =>
setUserId(parseInt(e.target.value))
}
>
<option value={1}>ユーザー1</option>
<option value={2}>ユーザー2</option>
<option value={3}>ユーザー3</option>
</select>
{userData.loading && <div>読み込み中...</div>}
{userData.error && (
<div>エラー: {userData.error.message}</div>
)}
{userProfile() && (
<div>
<h3>{userProfile()!.fullName}</h3>
<p>年齢: {userProfile()!.age}歳</p>
<p>
成人:{' '}
{userProfile()!.isAdult ? 'はい' : 'いいえ'}
</p>
<p>
プロフィール:{' '}
{userProfile()!.profileComplete
? '完了'
: '未完了'}
</p>
</div>
)}
</div>
);
};
よくある Memo のエラーと対処法
エラー 1: Maximum call stack size exceeded (createMemo)
arduinoRangeError: Maximum call stack size exceeded
Memo の中で自分自身を参照することで発生する循環参照エラーです。
typescript// ❌ 循環参照を引き起こす例
const BadMemo = () => {
const [value, setValue] = createSignal(0);
const memoValue = createMemo(() => {
return memoValue() + value(); // 自分自身を参照
});
// ...
};
// ✅ 正しい実装
const GoodMemo = () => {
const [value, setValue] = createSignal(0);
const [multiplier, setMultiplier] = createSignal(2);
const memoValue = createMemo(() => {
return value() * multiplier();
});
// ...
};
エラー 2: Cannot read property of undefined in memo
javascriptTypeError: Cannot read property 'length' of undefined
Memo 内で undefined の可能性がある値にアクセスしようとした場合のエラーです。
typescript// ❌ undefined チェックを忘れた例
const UnsafeMemo = () => {
const [data, setData] = createSignal<
string[] | undefined
>();
const processedData = createMemo(() => {
return data().map((item) => item.toUpperCase()); // data()がundefinedの場合エラー
});
};
// ✅ 適切なガード句を含む実装
const SafeMemo = () => {
const [data, setData] = createSignal<
string[] | undefined
>();
const processedData = createMemo(() => {
const currentData = data();
if (!currentData) return [];
return currentData.map((item) => item.toUpperCase());
});
};
3 つの使い分けパターン
どの状態管理を選ぶべきか
適切な状態管理手法を選択することで、パフォーマンスと開発効率を大幅に向上させることができます。以下の判断基準を参考にしてください。
状況 | 推奨手法 | 理由 |
---|---|---|
単一の値(文字列、数値、真偽値) | Signal | シンプルで軽量、オーバーヘッドが少ない |
複雑なオブジェクト・配列 | Store | 部分更新による高いパフォーマンス |
重い計算処理が必要 | Memo | 不要な再計算を避けられる |
フォームの入力値 | Signal | 各フィールドごとに独立した管理 |
ユーザー情報、設定データ | Store | ネストした構造の効率的な更新 |
API レスポンスの加工 | Memo | データ変更時のみ再処理 |
判断フローチャート
typescript// 状態管理選択の実例
const StateManagementExample = () => {
// 1. 単純な値 → Signal
const [isLoading, setIsLoading] = createSignal(false);
const [currentPage, setCurrentPage] = createSignal(1);
const [searchQuery, setSearchQuery] = createSignal('');
// 2. 複雑な構造 → Store
const [appState, setAppState] = createStore({
user: {
id: null as number | null,
name: '',
email: '',
preferences: {
theme: 'light' as 'light' | 'dark',
notifications: true,
},
},
posts: [] as Array<{
id: number;
title: string;
content: string;
tags: string[];
createdAt: Date;
}>,
ui: {
sidebarOpen: false,
selectedPostId: null as number | null,
},
});
// 3. 計算処理 → Memo
const filteredPosts = createMemo(() => {
const query = searchQuery().toLowerCase();
if (!query) return appState.posts;
return appState.posts.filter(
(post) =>
post.title.toLowerCase().includes(query) ||
post.content.toLowerCase().includes(query) ||
post.tags.some((tag) =>
tag.toLowerCase().includes(query)
)
);
});
const pageCount = createMemo(() => {
return Math.ceil(filteredPosts().length / 10);
});
const currentPagePosts = createMemo(() => {
const start = (currentPage() - 1) * 10;
return filteredPosts().slice(start, start + 10);
});
return (
<div>
{/* 実装例 */}
<input
type='text'
placeholder='検索...'
value={searchQuery()}
onInput={(e) => setSearchQuery(e.target.value)}
/>
<div>
{currentPagePosts().map((post) => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</article>
))}
</div>
<div>
ページ {currentPage()} / {pageCount()}
</div>
</div>
);
};
組み合わせて使う実践例
実際のアプリケーションでは、3 つの手法を組み合わせて使用することが一般的です。
typescript// 実践的な組み合わせ例:ブログ管理システム
const BlogManagementSystem = () => {
// Signal: 単純な状態管理
const [isEditMode, setIsEditMode] = createSignal(false);
const [selectedFilter, setSelectedFilter] =
createSignal('all');
const [sortOrder, setSortOrder] = createSignal<
'asc' | 'desc'
>('desc');
// Store: 複雑なデータ構造
const [blogState, setBlogState] = createStore({
posts: [] as BlogPost[],
categories: [] as Category[],
tags: [] as Tag[],
currentPost: null as BlogPost | null,
drafts: [] as BlogPost[],
publishedPosts: [] as BlogPost[],
});
// API から データを取得する関数
const loadPosts = async () => {
try {
const response = await fetch('/api/posts');
const posts = await response.json();
setBlogState('posts', posts);
} catch (error) {
console.error('投稿の読み込みに失敗:', error);
}
};
// Memo: 計算処理の最適化
const filteredPosts = createMemo(() => {
let posts = blogState.posts;
// フィルタリング
if (selectedFilter() !== 'all') {
posts = posts.filter(
(post) => post.status === selectedFilter()
);
}
// ソート
posts = [...posts].sort((a, b) => {
const compareValue =
new Date(a.createdAt).getTime() -
new Date(b.createdAt).getTime();
return sortOrder() === 'asc'
? compareValue
: -compareValue;
});
return posts;
});
const postsByCategory = createMemo(() => {
const categorizedPosts = new Map<string, BlogPost[]>();
filteredPosts().forEach((post) => {
const category = post.category || 'uncategorized';
if (!categorizedPosts.has(category)) {
categorizedPosts.set(category, []);
}
categorizedPosts.get(category)!.push(post);
});
return categorizedPosts;
});
const postStatistics = createMemo(() => {
const posts = blogState.posts;
return {
total: posts.length,
published: posts.filter(
(p) => p.status === 'published'
).length,
draft: posts.filter((p) => p.status === 'draft')
.length,
scheduled: posts.filter(
(p) => p.status === 'scheduled'
).length,
totalViews: posts.reduce(
(sum, p) => sum + (p.views || 0),
0
),
avgViews:
posts.length > 0
? posts.reduce(
(sum, p) => sum + (p.views || 0),
0
) / posts.length
: 0,
};
});
// 投稿作成・更新関数
const createPost = (
postData: Omit<BlogPost, 'id' | 'createdAt'>
) => {
const newPost: BlogPost = {
...postData,
id: Date.now(),
createdAt: new Date(),
};
setBlogState('posts', (posts) => [...posts, newPost]);
};
const updatePost = (
postId: number,
updates: Partial<BlogPost>
) => {
setBlogState('posts', (post) => post.id === postId, {
...updates,
updatedAt: new Date(),
});
};
const deletePost = (postId: number) => {
setBlogState('posts', (posts) =>
posts.filter((p) => p.id !== postId)
);
};
return (
<div class='blog-management'>
{/* 統計情報 */}
<div class='stats-panel'>
<h2>統計情報</h2>
<div class='stats-grid'>
<div>総投稿数: {postStatistics().total}</div>
<div>公開済み: {postStatistics().published}</div>
<div>下書き: {postStatistics().draft}</div>
<div>
総閲覧数:{' '}
{postStatistics().totalViews.toLocaleString()}
</div>
<div>
平均閲覧数:{' '}
{postStatistics().avgViews.toFixed(1)}
</div>
</div>
</div>
{/* フィルター・ソート */}
<div class='controls'>
<select
value={selectedFilter()}
onChange={(e) =>
setSelectedFilter(e.target.value)
}
>
<option value='all'>すべて</option>
<option value='published'>公開済み</option>
<option value='draft'>下書き</option>
<option value='scheduled'>予約投稿</option>
</select>
<button
onClick={() =>
setSortOrder(
sortOrder() === 'asc' ? 'desc' : 'asc'
)
}
>
並び順: {sortOrder() === 'asc' ? '昇順' : '降順'}
</button>
<button
onClick={() => setIsEditMode(!isEditMode())}
>
{isEditMode() ? '閲覧モード' : '編集モード'}
</button>
</div>
{/* 投稿一覧 */}
<div class='posts-container'>
{Array.from(postsByCategory().entries()).map(
([category, posts]) => (
<div key={category} class='category-section'>
<h3>{category}</h3>
<div class='posts-grid'>
{posts.map((post) => (
<article key={post.id} class='post-card'>
<h4>{post.title}</h4>
<p>{post.excerpt}</p>
<div class='post-meta'>
<span>状態: {post.status}</span>
<span>閲覧数: {post.views || 0}</span>
<span>
作成日:{' '}
{post.createdAt.toLocaleDateString()}
</span>
</div>
{isEditMode() && (
<div class='post-actions'>
<button
onClick={() =>
updatePost(post.id, {
status: 'published',
})
}
>
公開
</button>
<button
onClick={() =>
deletePost(post.id)
}
>
削除
</button>
</div>
)}
</article>
))}
</div>
</div>
)
)}
</div>
</div>
);
};
// 型定義
interface BlogPost {
id: number;
title: string;
content: string;
excerpt: string;
status: 'draft' | 'published' | 'scheduled';
category?: string;
tags: string[];
views?: number;
createdAt: Date;
updatedAt?: Date;
}
interface Category {
id: number;
name: string;
slug: string;
}
interface Tag {
id: number;
name: string;
count: number;
}
パフォーマンスを考慮した選択指針
パフォーマンスを最大化するための選択指針をご紹介します。
1. メモリ使用量の考慮
typescript// メモリ効率を考慮した実装例
const MemoryEfficientComponent = () => {
// ❌ 大量のSignalを作成(メモリ無駄遣い)
const [field1, setField1] = createSignal('');
const [field2, setField2] = createSignal('');
const [field3, setField3] = createSignal('');
// ... 50個のフィールド
// ✅ Storeで一括管理(メモリ効率良い)
const [formData, setFormData] = createStore({
field1: '',
field2: '',
field3: '',
// ... 他のフィールド
});
// 必要な場合のみMemoで計算
const formValidation = createMemo(() => {
// 全フィールドが入力されている場合のみ重いバリデーションを実行
const hasAllRequiredFields = Boolean(
formData.field1 && formData.field2 && formData.field3
);
if (!hasAllRequiredFields) {
return {
isValid: false,
errors: ['必須フィールドが未入力です'],
};
}
// 重いバリデーション処理
return performComplexValidation(formData);
});
return (
<form>
<input
value={formData.field1}
onInput={(e) =>
setFormData('field1', e.target.value)
}
/>
{/* 他のフィールド */}
</form>
);
};
2. 更新頻度による選択
typescript// 更新頻度に応じた最適化
const FrequencyOptimizedComponent = () => {
// 高頻度更新 → Signal(軽量)
const [mousePosition, setMousePosition] = createSignal({
x: 0,
y: 0,
});
const [scrollPosition, setScrollPosition] =
createSignal(0);
// 中頻度更新 → Store(構造化データ)
const [uiState, setUiState] = createStore({
modalOpen: false,
activeTab: 'home',
notifications: [] as Notification[],
});
// 低頻度・重い計算 → Memo
const expensiveCalculation = createMemo(() => {
// mousePositionが変更されても、この計算は実行されない
return performExpensiveOperation(uiState.notifications);
});
// イベントリスナーの設定
onMount(() => {
const handleMouseMove = (e: MouseEvent) => {
setMousePosition({ x: e.clientX, y: e.clientY });
};
const handleScroll = () => {
setScrollPosition(window.scrollY);
};
document.addEventListener('mousemove', handleMouseMove);
window.addEventListener('scroll', handleScroll);
onCleanup(() => {
document.removeEventListener(
'mousemove',
handleMouseMove
);
window.removeEventListener('scroll', handleScroll);
});
});
return (
<div>
<div>
マウス位置: {mousePosition().x}, {mousePosition().y}
</div>
<div>スクロール位置: {scrollPosition()}</div>
<div>重い計算結果: {expensiveCalculation()}</div>
</div>
);
};
まとめ
SolidJS の状態管理システムは、Signal、Store、Memo の 3 つの柱で構成されており、それぞれが特定の用途に最適化されています。適切な使い分けにより、高パフォーマンスで保守性の高いアプリケーションを構築できます。
主要なポイント
手法 | 適用場面 | 主な利点 | 注意点 |
---|---|---|---|
Signal | 単純な値の管理 | 軽量、高速、シンプル | 複雑な構造には不向き |
Store | 構造化データ | 部分更新、メモリ効率 | 初期学習コスト |
Memo | 計算処理の最適化 | 不要な再計算を回避 | 過度な使用は逆効果 |
選択の基本原則
-
シンプルな値は Signal を使用
- 文字列、数値、真偽値などの単純なデータ
- フォームの入力値、UI の状態フラグ
-
複雑な構造は Store を使用
- オブジェクト、配列、ネストしたデータ
- ユーザー情報、アプリケーション設定
-
重い計算は Memo で最適化
- フィルタリング、ソート、統計計算
- API レスポンスの加工処理
開発効率を向上させるコツ
- 型安全性を活用:TypeScript と組み合わせて、コンパイル時エラーを活用
- デバッグツールの使用:SolidJS DevTools でリアクティブシステムを可視化
- パフォーマンス測定:
performance.now()
を使って実際の改善効果を測定 - 段階的な導入:既存のコードベースに少しずつ導入して効果を確認
SolidJS の状態管理は、React や Vue.js とは異なるアプローチを取っていますが、一度慣れてしまえば非常に強力なツールとなります。特に、自動的な依存関係追跡により、開発者が明示的に依存配列を管理する必要がないため、バグの発生を大幅に減らせるでしょう。
実際のプロジェクトでは、これら 3 つの手法を組み合わせて使用することで、パフォーマンスと開発体験の両方を最適化できます。ぜひ、小さなプロジェクトから始めて、SolidJS の状態管理の威力を体感してみてください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来