SolidJS の Context でグローバル状態を管理する

SolidJS の開発で最も感動的な瞬間の一つは、Context を使ったグローバル状態管理を実装した時です。コンポーネント間でデータを共有する必要がある時、props のバケツリレーに悩まされていた開発者は、Context の登場で劇的に開発体験が向上します。
SolidJS の Context は、React の Context API に似ていますが、SolidJS のリアクティブシステムと組み合わさることで、より効率的で直感的な状態管理を実現します。この記事では、SolidJS の Context を使ったグローバル状態管理の実装方法から、実際のプロジェクトでの活用例まで、実践的な内容をお届けします。
Context の基本概念
Context とは、コンポーネントツリー全体でデータを共有するための仕組みです。従来の props によるデータ受け渡しでは、中間のコンポーネントが不要な props を経由する必要がありました。
typescript// 従来の props バケツリレーの例
function App() {
const [user, setUser] = createSignal({
name: '田中',
role: 'admin',
});
return (
<div>
<Header user={user()} />
<Main user={user()} />
<Footer user={user()} />
</div>
);
}
この方法では、Main
コンポーネントが user
を実際に使用しなくても、props として受け取る必要があります。Context を使うことで、この問題を解決できます。
SolidJS の Context は createContext
関数で作成され、Context.Provider
で値を提供し、useContext
フックで値を取得します。
SolidJS の Context の特徴
SolidJS の Context には、他のフレームワークにはない特徴があります。
1. リアクティブな値の自動追跡
SolidJS の Context は、Signal や Store などのリアクティブな値を自動的に追跡します。値が変更されると、その値を使用しているコンポーネントのみが再レンダリングされます。
2. 型安全性
TypeScript との相性が抜群で、コンテキストの値の型を明確に定義できます。
3. パフォーマンスの最適化
不要な再レンダリングを防ぎ、必要な部分のみを更新するため、パフォーマンスが向上します。
4. 開発者体験の向上
SolidJS の開発者ツールと連携し、Context の値の変更を追跡できます。
基本的な Context の作成と使用
まず、基本的な Context の作成から始めましょう。
Context の作成
typescript// contexts/UserContext.tsx
import {
createContext,
useContext,
createSignal,
JSX,
} from 'solid-js';
// ユーザー情報の型定義
interface User {
id: number;
name: string;
email: string;
role: 'user' | 'admin';
}
// Context の型定義
interface UserContextType {
user: () => User | null;
setUser: (user: User | null) => void;
isLoggedIn: () => boolean;
}
// Context の作成
const UserContext = createContext<UserContextType>();
// Context のデフォルト値
const defaultUserContext: UserContextType = {
user: () => null,
setUser: () => {},
isLoggedIn: () => false,
};
Provider コンポーネントの作成
typescript// contexts/UserProvider.tsx
import { createSignal, JSX } from 'solid-js';
import { UserContext } from './UserContext';
interface UserProviderProps {
children: JSX.Element;
}
export function UserProvider(props: UserProviderProps) {
// ユーザー情報の状態管理
const [user, setUser] = createSignal<User | null>(null);
// ログイン状態の計算
const isLoggedIn = () => user() !== null;
// Context の値
const contextValue = {
user,
setUser,
isLoggedIn,
};
return (
<UserContext.Provider value={contextValue}>
{props.children}
</UserContext.Provider>
);
}
Context の使用
typescript// components/UserProfile.tsx
import { useContext } from 'solid-js';
import { UserContext } from '../contexts/UserContext';
export function UserProfile() {
const userContext = useContext(UserContext);
if (!userContext) {
throw new Error(
'UserProfile must be used within UserProvider'
);
}
const { user, isLoggedIn } = userContext;
return (
<div class='user-profile'>
{isLoggedIn() ? (
<div>
<h3>ようこそ、{user()?.name}さん</h3>
<p>メール: {user()?.email}</p>
<p>権限: {user()?.role}</p>
</div>
) : (
<p>ログインしてください</p>
)}
</div>
);
}
アプリケーションでの使用
typescript// App.tsx
import { UserProvider } from './contexts/UserProvider';
import { UserProfile } from './components/UserProfile';
import { LoginForm } from './components/LoginForm';
export default function App() {
return (
<UserProvider>
<div class='app'>
<header>
<h1>SolidJS Context デモ</h1>
</header>
<main>
<UserProfile />
<LoginForm />
</main>
</div>
</UserProvider>
);
}
複数の Context を組み合わせる方法
実際のアプリケーションでは、複数の Context を組み合わせて使用することが一般的です。
テーマ Context の作成
typescript// contexts/ThemeContext.tsx
import {
createContext,
useContext,
createSignal,
JSX,
} from 'solid-js';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: () => Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType>();
export function ThemeProvider(props: {
children: JSX.Element;
}) {
const [theme, setTheme] = createSignal<Theme>('light');
const toggleTheme = () => {
setTheme(theme() === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider
value={{ theme, setTheme, toggleTheme }}
>
{props.children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error(
'useTheme must be used within ThemeProvider'
);
}
return context;
}
複数の Provider をネスト
typescript// App.tsx
import { UserProvider } from './contexts/UserProvider';
import { ThemeProvider } from './contexts/ThemeContext';
import { UserProfile } from './components/UserProfile';
import { ThemeToggle } from './components/ThemeToggle';
export default function App() {
return (
<ThemeProvider>
<UserProvider>
<div class='app'>
<header>
<h1>SolidJS Context デモ</h1>
<ThemeToggle />
</header>
<main>
<UserProfile />
</main>
</div>
</UserProvider>
</ThemeProvider>
);
}
複数の Context を使用するコンポーネント
typescript// components/UserProfile.tsx
import { useContext } from 'solid-js';
import { UserContext } from '../contexts/UserContext';
import { useTheme } from '../contexts/ThemeContext';
export function UserProfile() {
const userContext = useContext(UserContext);
const { theme } = useTheme();
if (!userContext) {
throw new Error(
'UserProfile must be used within UserProvider'
);
}
const { user, isLoggedIn } = userContext;
return (
<div class={`user-profile theme-${theme()}`}>
{isLoggedIn() ? (
<div>
<h3>ようこそ、{user()?.name}さん</h3>
<p>メール: {user()?.email}</p>
<p>権限: {user()?.role}</p>
</div>
) : (
<p>ログインしてください</p>
)}
</div>
);
}
パフォーマンス最適化のテクニック
SolidJS の Context は既に効率的ですが、さらに最適化するためのテクニックがあります。
1. 値の分割
大きなオブジェクトを Context で管理する場合、値を分割することで不要な再レンダリングを防げます。
typescript// contexts/AppContext.tsx
import {
createContext,
useContext,
createSignal,
JSX,
} from 'solid-js';
interface AppState {
user: {
name: string;
email: string;
};
settings: {
theme: string;
language: string;
};
notifications: {
count: number;
items: string[];
};
}
// 悪い例:大きなオブジェクトを一つの Context で管理
const AppContext = createContext<AppState>();
// 良い例:値を分割して複数の Context で管理
const UserContext = createContext<AppState['user']>();
const SettingsContext =
createContext<AppState['settings']>();
const NotificationsContext =
createContext<AppState['notifications']>();
2. メモ化の活用
typescript// contexts/OptimizedContext.tsx
import {
createContext,
useContext,
createSignal,
createMemo,
JSX,
} from 'solid-js';
interface User {
id: number;
name: string;
email: string;
role: string;
permissions: string[];
}
interface UserContextType {
user: () => User | null;
setUser: (user: User | null) => void;
isAdmin: () => boolean;
hasPermission: (permission: string) => boolean;
}
const UserContext = createContext<UserContextType>();
export function UserProvider(props: {
children: JSX.Element;
}) {
const [user, setUser] = createSignal<User | null>(null);
// メモ化でパフォーマンスを最適化
const isAdmin = createMemo(
() => user()?.role === 'admin'
);
const hasPermission = createMemo(() => {
return (permission: string) => {
const currentUser = user();
return (
currentUser?.permissions.includes(permission) ||
false
);
};
});
return (
<UserContext.Provider
value={{
user,
setUser,
isAdmin,
hasPermission: hasPermission(),
}}
>
{props.children}
</UserContext.Provider>
);
}
3. 条件付きレンダリングの最適化
typescript// components/OptimizedComponent.tsx
import { useContext, Show } from 'solid-js';
import { UserContext } from '../contexts/UserContext';
export function OptimizedComponent() {
const userContext = useContext(UserContext);
if (!userContext) {
throw new Error(
'OptimizedComponent must be used within UserProvider'
);
}
const { user, isAdmin } = userContext;
return (
<div>
<Show
when={user()}
fallback={<p>ローディング中...</p>}
>
{(userData) => (
<div>
<h3>{userData.name}</h3>
<Show when={isAdmin()}>
<p>管理者権限があります</p>
</Show>
</div>
)}
</Show>
</div>
);
}
実際のプロジェクトでの活用例
実際のプロジェクトで Context を活用する例を見てみましょう。
E コマースアプリケーションの例
typescript// contexts/CartContext.tsx
import {
createContext,
useContext,
createSignal,
createMemo,
JSX,
} from 'solid-js';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartContextType {
items: () => CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: number) => void;
updateQuantity: (id: number, quantity: number) => void;
clearCart: () => void;
totalItems: () => number;
totalPrice: () => number;
}
const CartContext = createContext<CartContextType>();
export function CartProvider(props: {
children: JSX.Element;
}) {
const [items, setItems] = createSignal<CartItem[]>([]);
const addItem = (newItem: Omit<CartItem, 'quantity'>) => {
setItems((prev) => {
const existingItem = prev.find(
(item) => item.id === newItem.id
);
if (existingItem) {
return prev.map((item) =>
item.id === newItem.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...newItem, quantity: 1 }];
});
};
const removeItem = (id: number) => {
setItems((prev) =>
prev.filter((item) => item.id !== id)
);
};
const updateQuantity = (id: number, quantity: number) => {
if (quantity <= 0) {
removeItem(id);
return;
}
setItems((prev) =>
prev.map((item) =>
item.id === id ? { ...item, quantity } : item
)
);
};
const clearCart = () => setItems([]);
const totalItems = createMemo(() =>
items().reduce((sum, item) => sum + item.quantity, 0)
);
const totalPrice = createMemo(() =>
items().reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
);
return (
<CartContext.Provider
value={{
items,
addItem,
removeItem,
updateQuantity,
clearCart,
totalItems,
totalPrice,
}}
>
{props.children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error(
'useCart must be used within CartProvider'
);
}
return context;
}
カートコンポーネントの実装
typescript// components/Cart.tsx
import { For, Show } from 'solid-js';
import { useCart } from '../contexts/CartContext';
export function Cart() {
const {
items,
removeItem,
updateQuantity,
totalItems,
totalPrice,
} = useCart();
return (
<div class='cart'>
<h2>ショッピングカート ({totalItems()} アイテム)</h2>
<Show
when={items().length > 0}
fallback={<p>カートは空です</p>}
>
<div class='cart-items'>
<For each={items()}>
{(item) => (
<div class='cart-item'>
<h3>{item.name}</h3>
<p>価格: ¥{item.price.toLocaleString()}</p>
<div class='quantity-controls'>
<button
onClick={() =>
updateQuantity(
item.id,
item.quantity - 1
)
}
disabled={item.quantity <= 1}
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() =>
updateQuantity(
item.id,
item.quantity + 1
)
}
>
+
</button>
<button
onClick={() => removeItem(item.id)}
class='remove-btn'
>
削除
</button>
</div>
</div>
)}
</For>
</div>
<div class='cart-summary'>
<h3>合計: ¥{totalPrice().toLocaleString()}</h3>
</div>
</Show>
</div>
);
}
よくある問題と解決策
SolidJS の Context を使用する際によく遭遇する問題とその解決策をご紹介します。
1. Context が undefined エラー
typescript// エラー例
function MyComponent() {
const context = useContext(MyContext);
// context が undefined の場合がある
return <div>{context.value}</div>; // エラー!
}
// 解決策1: エラーハンドリング
function MyComponent() {
const context = useContext(MyContext);
if (!context) {
throw new Error(
'MyComponent must be used within MyProvider'
);
}
return <div>{context.value}</div>;
}
// 解決策2: デフォルト値の提供
const MyContext = createContext<MyContextType>({
value: 'default',
setValue: () => {},
});
2. 無限ループの発生
typescript// 問題のあるコード
function UserProvider(props: { children: JSX.Element }) {
const [user, setUser] = createSignal<User | null>(null);
// 毎回新しいオブジェクトを作成している
const contextValue = {
user,
setUser,
};
return (
<UserContext.Provider value={contextValue}>
{props.children}
</UserContext.Provider>
);
}
// 解決策: オブジェクトを固定
function UserProvider(props: { children: JSX.Element }) {
const [user, setUser] = createSignal<User | null>(null);
// オブジェクトを一度だけ作成
const contextValue: UserContextType = {
user,
setUser,
};
return (
<UserContext.Provider value={contextValue}>
{props.children}
</UserContext.Provider>
);
}
3. メモリリークの防止
typescript// contexts/SafeContext.tsx
import {
createContext,
useContext,
createSignal,
onCleanup,
JSX,
} from 'solid-js';
interface TimerContextType {
time: () => number;
start: () => void;
stop: () => void;
}
const TimerContext = createContext<TimerContextType>();
export function TimerProvider(props: {
children: JSX.Element;
}) {
const [time, setTime] = createSignal(0);
let intervalId: number | undefined;
const start = () => {
if (intervalId) return; // 既に実行中
intervalId = setInterval(() => {
setTime((prev) => prev + 1);
}, 1000);
};
const stop = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = undefined;
}
};
// クリーンアップでメモリリークを防止
onCleanup(() => {
if (intervalId) {
clearInterval(intervalId);
}
});
return (
<TimerContext.Provider value={{ time, start, stop }}>
{props.children}
</TimerContext.Provider>
);
}
4. 型安全性の確保
typescript// contexts/TypedContext.tsx
import {
createContext,
useContext,
createSignal,
JSX,
} from 'solid-js';
// 厳密な型定義
interface User {
id: number;
name: string;
email: string;
}
interface UserContextType {
user: () => User | null;
setUser: (user: User | null) => void;
updateUser: (updates: Partial<User>) => void;
}
const UserContext = createContext<UserContextType>();
// 型安全なフック
export function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error(
'useUser must be used within UserProvider'
);
}
return context;
}
// 使用例
function UserComponent() {
const { user, updateUser } = useUser();
const handleNameChange = (newName: string) => {
const currentUser = user();
if (currentUser) {
updateUser({ name: newName }); // 型安全
}
};
return (
<div>
<input
value={user()?.name || ''}
onInput={(e) =>
handleNameChange(e.currentTarget.value)
}
/>
</div>
);
}
まとめ
SolidJS の Context を使ったグローバル状態管理は、開発者にとって非常に強力なツールです。従来の props バケツリレーの問題を解決し、効率的で型安全な状態管理を実現できます。
この記事で学んだポイントを振り返ると:
- Context の基本概念: コンポーネントツリー全体でデータを共有する仕組み
- SolidJS の特徴: リアクティブな値の自動追跡と型安全性
- 実装方法: createContext、Provider、useContext の組み合わせ
- 最適化テクニック: 値の分割、メモ化、条件付きレンダリング
- 実践的な活用: E コマースアプリケーションでのカート管理
- 問題解決: よくあるエラーとその解決策
SolidJS の Context を活用することで、コードの可読性が向上し、メンテナンスしやすいアプリケーションを構築できます。特に、リアクティブシステムとの組み合わせにより、パフォーマンスを損なうことなく、直感的な状態管理を実現できる点が大きな魅力です。
実際のプロジェクトで Context を導入する際は、まず小さな範囲から始めて、徐々に拡張していくことをお勧めします。そうすることで、Context の利点を実感しながら、適切な設計パターンを身につけることができます。
SolidJS の Context は、モダンなフロントエンド開発において、状態管理の新しい可能性を開いてくれる素晴らしい機能です。この記事が、あなたの SolidJS 開発の一助となれば幸いです。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来