T-CREATOR

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

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 バケツリレーの問題を解決し、効率的で型安全な状態管理を実現できます。

この記事で学んだポイントを振り返ると:

  1. Context の基本概念: コンポーネントツリー全体でデータを共有する仕組み
  2. SolidJS の特徴: リアクティブな値の自動追跡と型安全性
  3. 実装方法: createContext、Provider、useContext の組み合わせ
  4. 最適化テクニック: 値の分割、メモ化、条件付きレンダリング
  5. 実践的な活用: E コマースアプリケーションでのカート管理
  6. 問題解決: よくあるエラーとその解決策

SolidJS の Context を活用することで、コードの可読性が向上し、メンテナンスしやすいアプリケーションを構築できます。特に、リアクティブシステムとの組み合わせにより、パフォーマンスを損なうことなく、直感的な状態管理を実現できる点が大きな魅力です。

実際のプロジェクトで Context を導入する際は、まず小さな範囲から始めて、徐々に拡張していくことをお勧めします。そうすることで、Context の利点を実感しながら、適切な設計パターンを身につけることができます。

SolidJS の Context は、モダンなフロントエンド開発において、状態管理の新しい可能性を開いてくれる素晴らしい機能です。この記事が、あなたの SolidJS 開発の一助となれば幸いです。

関連リンク