ブログ記事

SolidJS実践完全ガイド2025 - Reactの代替フレームワーク決定版

パフォーマンス革命をもたらすSolidJSの実践的活用法を徹底解説。React比3倍高速を実現する仕組みから実際のプロジェクト構築、移行戦略まで、具体的なコード例とベンチマークデータで詳しく紹介します。

14分で読めます
R
Rina
Daily Hack 編集長
Web開発
SolidJS React代替 フロントエンド パフォーマンス JSX リアクティブ
SolidJS実践完全ガイド2025 - Reactの代替フレームワーク決定版のヒーロー画像

SolidJS は 2025 年、フロントエンド開発における真のパフォーマンス革命として確固たる地位を築きました。React比 3 倍の高速化を実現しながらも、慣れ親しんだ JSX 記法を保持するこのフレームワークは、多くの開発者と企業から注目を集めています。本記事では、SolidJS の核心機能から実践的な活用法、Reactからの移行戦略まで、包括的に解説します。

この記事で学べること

  • SolidJS の核心となるリアクティブシステムとパフォーマンス最適化原理
  • Reactと SolidJS の詳細な比較とベンチマーク分析
  • 実践的なプロジェクト構築手順と開発ワークフロー
  • 状態管理、ルーティング、テストの実装パターン
  • Reactプロジェクトから段階的な移行戦略とリスク管理

SolidJSとは何か

革新的リアクティブシステム

SolidJS は「真のリアクティブ」を実現する革新的なフロントエンドフレームワークです。従来のバーチャル DOM による差分更新ではなく、変更された部分のみを直接更新する「細粒度リアクティビティ」を実装しています。

SolidJS vs React パフォーマンス比較
特徴 SolidJS React 優位性
更新方式 細粒度リアクティビティ Virtual DOM差分 3-5倍高速
バンドルサイズ ~7KB ~45KB 85%削減
初期描画 50ms未満 150ms未満 3倍高速
メモリ使用量 中-高 40%削減
学習コスト 低(JSX慣れ) 基準 React知識活用可
エコシステム 急成長中 成熟 必要十分

パフォーマンスの秘密

SolidJS リアクティブシステム

チャートを読み込み中...

基本概念と実装パターン

Signal:リアクティブの基礎

SolidJS の核心となる Signal システムを理解しましょう:

import { createSignal, createEffect, createMemo } from 'solid-js';

// 基本的なSignal
const [count, setCount] = createSignal(0);

// 計算されたSignal(Memo)
const doubleCount = createMemo(() => count() * 2);

// 副作用(Effect)
createEffect(() => {
  console.log(`現在のカウント: ${count()}, 2倍: ${doubleCount()}`);
});

// 値の更新
setCount(5); // Effect が自動実行される

リアクティブな状態管理

// React での状態管理
function Counter() {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(2);
  
  // 依存関係を明示的に指定
  const result = useMemo(() => 
    count * multiplier, [count, multiplier]
  );
  
  // 副作用の依存関係を管理
  useEffect(() => {
    document.title = `カウント: ${result}`;
  }, [result]);
  
  return (
    <div>
      <p>結果: {result}</p>
      <button onClick={() => setCount(c => c + 1)}>
        +1
      </button>
    </div>
  );
}
// SolidJS での状態管理
function Counter() {
  const [count, setCount] = createSignal(0);
  const [multiplier, setMultiplier] = createSignal(2);
  
  // 自動的な依存関係追跡
  const result = createMemo(() => 
    count() * multiplier()
  );
  
  // 依存関係は自動検出
  createEffect(() => {
    document.title = `カウント: ${result()}`;
  });
  
  return (
    <div>
      <p>結果: {result()}</p>
      <button onClick={() => setCount(c => c + 1)}>
        +1
      </button>
    </div>
  );
}
React フック
// React での状態管理
function Counter() {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(2);
  
  // 依存関係を明示的に指定
  const result = useMemo(() => 
    count * multiplier, [count, multiplier]
  );
  
  // 副作用の依存関係を管理
  useEffect(() => {
    document.title = `カウント: ${result}`;
  }, [result]);
  
  return (
    <div>
      <p>結果: {result}</p>
      <button onClick={() => setCount(c => c + 1)}>
        +1
      </button>
    </div>
  );
}
SolidJS シグナル
// SolidJS での状態管理
function Counter() {
  const [count, setCount] = createSignal(0);
  const [multiplier, setMultiplier] = createSignal(2);
  
  // 自動的な依存関係追跡
  const result = createMemo(() => 
    count() * multiplier()
  );
  
  // 依存関係は自動検出
  createEffect(() => {
    document.title = `カウント: ${result()}`;
  });
  
  return (
    <div>
      <p>結果: {result()}</p>
      <button onClick={() => setCount(c => c + 1)}>
        +1
      </button>
    </div>
  );
}

コンポーネント設計パターン

// 高パフォーマンスなコンポーネント設計
import { createSignal, createMemo, For, Show } from 'solid-js';

function TodoApp() {
  const [todos, setTodos] = createSignal([]);
  const [filter, setFilter] = createSignal('all'); // 'all' | 'active' | 'completed'
  
  // 効率的なフィルタリング
  const filteredTodos = createMemo(() => {
    const currentTodos = todos();
    const currentFilter = filter();
    
    switch (currentFilter) {
      case 'active':
        return currentTodos.filter(todo => !todo.completed);
      case 'completed':
        return currentTodos.filter(todo => todo.completed);
      default:
        return currentTodos;
    }
  });
  
  // 統計情報の計算
  const stats = createMemo(() => {
    const currentTodos = todos();
    return {
      total: currentTodos.length,
      completed: currentTodos.filter(todo => todo.completed).length,
      active: currentTodos.filter(todo => !todo.completed).length
    };
  });
  
  const addTodo = (text) => {
    setTodos(prev => [...prev, {
      id: Date.now(),
      text,
      completed: false,
      createdAt: new Date()
    }]);
  };
  
  const toggleTodo = (id) => {
    setTodos(prev => prev.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };
  
  const deleteTodo = (id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };
  
  return (
    <div class="todo-app">
      <TodoInput onAdd={addTodo} />
      
      <TodoStats stats={stats()} />
      
      <TodoFilters 
        current={filter()} 
        onChange={setFilter} 
      />
      
      <div class="todo-list">
        <For each={filteredTodos()}>
          {(todo) => (
            <TodoItem
              todo={todo}
              onToggle={() => toggleTodo(todo.id)}
              onDelete={() => deleteTodo(todo.id)}
            />
          )}
        </For>
        
        <Show when={filteredTodos().length === 0}>
          <EmptyState filter={filter()} />
        </Show>
      </div>
    </div>
  );
}

// 最適化されたTodoItemコンポーネント
function TodoItem(props) {
  // プロパティは自動的にリアクティブ
  const todo = () => props.todo;
  
  return (
    <div 
      class="todo-item" 
      classList={{ 
        completed: todo().completed,
        recent: isRecent(todo().createdAt)
      }}
    >
      <input
        type="checkbox"
        checked={todo().completed}
        onChange={props.onToggle}
      />
      <span class="todo-text">{todo().text}</span>
      <button 
        class="delete-btn"
        onClick={props.onDelete}
      >
        削除
      </button>
    </div>
  );
}

function isRecent(date) {
  return Date.now() - date.getTime() < 5 * 60 * 1000; // 5分以内
}

実践的なプロジェクト構築

プロジェクト構造とセットアップ

プロジェクト初期化

Vite + SolidJS テンプレートの設定

開発環境構築

TypeScript、ESLint、Prettier設定

ルーティング設定

solid-router による SPA 構築

状態管理実装

グローバル状態とローカル状態の設計

プロジェクト初期化

# Vite + SolidJS テンプレート
npm create solid@latest my-solid-app

# またはTypeScriptテンプレート
npm create solid@latest my-solid-app -- --template ts

cd my-solid-app
npm install

# 開発サーバー起動
npm run dev

推奨プロジェクト構造

src/
├── components/          # 再利用可能コンポーネント
│   ├── ui/             # UI基盤コンポーネント
│   ├── forms/          # フォーム関連
│   └── layout/         # レイアウトコンポーネント
├── pages/              # ページコンポーネント
├── stores/             # グローバル状態管理
├── utils/              # ユーティリティ関数
├── types/              # TypeScript型定義
├── styles/             # スタイル定義
└── App.tsx             # アプリケーションルート

ルーティングの実装

// App.tsx - ルーティング設定
import { Router, Route, Routes } from '@solidjs/router';
import { lazy } from 'solid-js';

// 遅延ローディング
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Profile = lazy(() => import('./pages/Profile'));

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" component={Home} />
        <Route path="/products" component={Products} />
        <Route path="/products/:id" component={ProductDetail} />
        <Route path="/profile" component={Profile} />
        <Route path="*" component={() => <div>404 Not Found</div>} />
      </Routes>
    </Router>
  );
}

export default App;
// pages/ProductDetail.tsx - 動的ルーティング
import { useParams, useNavigate } from '@solidjs/router';
import { createResource, Show, Suspense } from 'solid-js';

async function fetchProduct(id: string) {
  const response = await fetch(`/api/products/${id}`);
  if (!response.ok) throw new Error('Product not found');
  return response.json();
}

function ProductDetail() {
  const params = useParams();
  const navigate = useNavigate();
  
  // リソースによる非同期データ取得
  const [product] = createResource(
    () => params.id,
    fetchProduct
  );
  
  return (
    <div class="product-detail">
      <button onClick={() => navigate('/products')}>
        ← 商品一覧に戻る
      </button>
      
      <Suspense fallback={<ProductSkeleton />}>
        <Show 
          when={product()} 
          fallback={<ProductNotFound />}
        >
          {(product) => (
            <div class="product-content">
              <h1>{product().name}</h1>
              <img src={product().image} alt={product().name} />
              <p class="price">¥{product().price.toLocaleString()}</p>
              <p class="description">{product().description}</p>
              
              <ProductActions product={product()} />
            </div>
          )}
        </Show>
      </Suspense>
    </div>
  );
}

グローバル状態管理

// stores/userStore.ts - Store パターン
import { createSignal, createMemo } from 'solid-js';

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

// グローバル状態
const [user, setUser] = createSignal<User | null>(null);
const [isLoading, setIsLoading] = createSignal(false);
const [error, setError] = createSignal<string | null>(null);

// 計算された値
export const isAuthenticated = createMemo(() => user() !== null);
export const isAdmin = createMemo(() => user()?.role === 'admin');

// アクション
export const userActions = {
  async login(email: string, password: string) {
    setIsLoading(true);
    setError(null);
    
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });
      
      if (!response.ok) {
        throw new Error('ログインに失敗しました');
      }
      
      const userData = await response.json();
      setUser(userData);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'エラーが発生しました');
    } finally {
      setIsLoading(false);
    }
  },
  
  logout() {
    setUser(null);
    localStorage.removeItem('auth-token');
  },
  
  updateProfile(updates: Partial<User>) {
    setUser(prev => prev ? { ...prev, ...updates } : null);
  }
};

// エクスポート
export { user, isLoading, error };
// contexts/AppContext.tsx - Context パターン
import { createContext, useContext, ParentComponent } from 'solid-js';
import { createStore } from 'solid-js/store';

interface AppState {
  theme: 'light' | 'dark';
  language: 'ja' | 'en';
  notifications: Notification[];
}

interface AppContextType {
  state: AppState;
  actions: {
    setTheme: (theme: AppState['theme']) => void;
    setLanguage: (lang: AppState['language']) => void;
    addNotification: (notification: Notification) => void;
  };
}

const AppContext = createContext<AppContextType>();

export const AppProvider: ParentComponent = (props) => {
  const [state, setState] = createStore<AppState>({
    theme: 'light',
    language: 'ja',
    notifications: []
  });
  
  const actions = {
    setTheme: (theme: AppState['theme']) => {
      setState('theme', theme);
      localStorage.setItem('theme', theme);
    },
    
    setLanguage: (language: AppState['language']) => {
      setState('language', language);
      localStorage.setItem('language', language);
    },
    
    addNotification: (notification: Notification) => {
      setState('notifications', prev => [...prev, notification]);
      
      // 自動削除
      setTimeout(() => {
        setState('notifications', prev => 
          prev.filter(n => n.id !== notification.id)
        );
      }, 5000);
    }
  };
  
  return (
    <AppContext.Provider value={{ state, actions }}>
      {props.children}
    </AppContext.Provider>
  );
};

export function useApp() {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useApp must be used within AppProvider');
  }
  return context;
}
// stores/cartStore.ts - External Store パターン
import { createStore } from 'solid-js/store';
import { createEffect } from 'solid-js';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
  image: string;
}

interface CartStore {
  items: CartItem[];
  total: number;
  tax: number;
  grandTotal: number;
}

// Store の作成
const [cartStore, setCartStore] = createStore<CartStore>({
  items: [],
  total: 0,
  tax: 0,
  grandTotal: 0
});

// 計算の自動更新
createEffect(() => {
  const total = cartStore.items.reduce(
    (sum, item) => sum + (item.price * item.quantity), 0
  );
  const tax = total * 0.1;
  const grandTotal = total + tax;
  
  setCartStore({
    total,
    tax,
    grandTotal
  });
});

// ローカルストレージとの同期
createEffect(() => {
  localStorage.setItem('cart', JSON.stringify(cartStore.items));
});

// アクション
export const cartActions = {
  addItem(product: Omit<CartItem, 'quantity'>) {
    setCartStore('items', prev => {
      const existingIndex = prev.findIndex(item => item.id === product.id);
      
      if (existingIndex >= 0) {
        return prev.map((item, index) =>
          index === existingIndex 
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        return [...prev, { ...product, quantity: 1 }];
      }
    });
  },
  
  removeItem(id: string) {
    setCartStore('items', prev => prev.filter(item => item.id !== id));
  },
  
  updateQuantity(id: string, quantity: number) {
    if (quantity <= 0) {
      this.removeItem(id);
      return;
    }
    
    setCartStore('items', item => item.id === id, 'quantity', quantity);
  },
  
  clearCart() {
    setCartStore('items', []);
  }
};

export { cartStore };

パフォーマンス最適化の実践

レンダリング最適化

初期描画速度 95 %
更新パフォーマンス 98 %
メモリ効率 92 %
// パフォーマンス最適化のベストプラクティス
import { 
  createSignal, 
  createMemo, 
  createComputed,
  For, 
  Index,
  Show,
  Switch,
  Match,
  batch
} from 'solid-js';

function OptimizedList() {
  const [items, setItems] = createSignal([]);
  const [filter, setFilter] = createSignal('');
  const [sortBy, setSortBy] = createSignal('name');
  
  // 効率的なフィルタリングとソート
  const processedItems = createMemo(() => {
    const currentItems = items();
    const currentFilter = filter().toLowerCase();
    const currentSortBy = sortBy();
    
    return currentItems
      .filter(item => 
        !currentFilter || 
        item.name.toLowerCase().includes(currentFilter)
      )
      .sort((a, b) => {
        switch (currentSortBy) {
          case 'name':
            return a.name.localeCompare(b.name);
          case 'date':
            return new Date(b.createdAt) - new Date(a.createdAt);
          case 'price':
            return a.price - b.price;
          default:
            return 0;
        }
      });
  });
  
  // バッチ更新による最適化
  const updateMultipleFilters = (newFilter: string, newSort: string) => {
    batch(() => {
      setFilter(newFilter);
      setSortBy(newSort);
    });
  };
  
  return (
    <div class="optimized-list">
      <div class="controls">
        <input
          type="text"
          placeholder="検索..."
          value={filter()}
          onInput={(e) => setFilter(e.target.value)}
        />
        
        <select
          value={sortBy()}
          onChange={(e) => setSortBy(e.target.value)}
        >
          <option value="name">名前順</option>
          <option value="date">日付順</option>
          <option value="price">価格順</option>
        </select>
      </div>
      
      {/* キーベースのリストレンダリング */}
      <For each={processedItems()}>
        {(item) => <OptimizedItem item={item} />}
      </For>
      
      {/* インデックスベース(より高速、アイテムが変わらない場合)*/}
      <Index each={processedItems()}>
        {(item, index) => (
          <div class="item-wrapper">
            <span class="index">{index + 1}</span>
            <OptimizedItem item={item()} />
          </div>
        )}
      </Index>
    </div>
  );
}

// 条件分岐の最適化
function ConditionalRendering(props) {
  return (
    <div>
      {/* 単純な条件分岐 */}
      <Show when={props.user} fallback={<LoginForm />}>
        <UserDashboard user={props.user} />
      </Show>
      
      {/* 複数の条件分岐 */}
      <Switch>
        <Match when={props.status === 'loading'}>
          <LoadingSpinner />
        </Match>
        <Match when={props.status === 'error'}>
          <ErrorMessage error={props.error} />
        </Match>
        <Match when={props.status === 'success'}>
          <SuccessContent data={props.data} />
        </Match>
      </Switch>
    </div>
  );
}

メモリリーク防止

// リソース管理とクリーンアップ
import { onCleanup, createEffect } from 'solid-js';

function WebSocketComponent() {
  let ws: WebSocket;
  
  createEffect(() => {
    // WebSocket接続
    ws = new WebSocket('ws://localhost:8080');
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      handleMessage(data);
    };
    
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
    
    // クリーンアップの登録
    onCleanup(() => {
      if (ws) {
        ws.close();
      }
    });
  });
  
  // DOM イベントリスナーの管理
  createEffect(() => {
    const handleResize = () => {
      console.log('Window resized');
    };
    
    window.addEventListener('resize', handleResize);
    
    onCleanup(() => {
      window.removeEventListener('resize', handleResize);
    });
  });
  
  return <div>WebSocket Component</div>;
}

Reactからの移行戦略

段階的移行アプローチ

評価・計画

移行対象の選定と技術検証

新機能でのテスト

小規模機能での SolidJS 導入

段階的移行

ページ単位での移行実行

最適化・完了

全体最適化と移行完了

移行互換性ライブラリ

// react-to-solid-adapter.ts
// React コンポーネントを SolidJS で使用するためのアダプター

import { createSignal, createEffect, onCleanup } from 'solid-js';

// React useState の SolidJS 版
export function useState<T>(initialValue: T): [() => T, (value: T | ((prev: T) => T)) => void] {
  const [signal, setSignal] = createSignal(initialValue);
  
  const setState = (value: T | ((prev: T) => T)) => {
    if (typeof value === 'function') {
      setSignal(prev => (value as (prev: T) => T)(prev));
    } else {
      setSignal(value);
    }
  };
  
  return [signal, setState];
}

// React useEffect の SolidJS 版
export function useEffect(fn: () => void | (() => void), deps?: any[]) {
  createEffect(() => {
    const cleanup = fn();
    
    if (cleanup && typeof cleanup === 'function') {
      onCleanup(cleanup);
    }
  });
}

// React useMemo の SolidJS 版
export function useMemo<T>(fn: () => T, deps?: any[]): () => T {
  return createMemo(fn);
}

// React useCallback の SolidJS 版  
export function useCallback<T extends (...args: any[]) => any>(
  callback: T, 
  deps?: any[]
): T {
  return createMemo(() => callback);
}

実践的移行例

// React版 UserProfile
import React, { useState, useEffect, useMemo } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
}

const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('User not found');
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Error');
      } finally {
        setLoading(false);
      }
    }
    
    fetchUser();
  }, [userId]);
  
  const displayName = useMemo(() => {
    return user ? `${user.name} (${user.email})` : '';
  }, [user]);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>User not found</div>;
  
  return (
    <div className="user-profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{displayName}</h2>
    </div>
  );
};

export default UserProfile;
// SolidJS版 UserProfile
import { createResource, createMemo, Show } from 'solid-js';

interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
}

async function fetchUser(userId: string): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) throw new Error('User not found');
  return response.json();
}

interface UserProfileProps {
  userId: string;
}

function UserProfile(props: UserProfileProps) {
  // createResource で非同期データ取得
  const [user] = createResource(
    () => props.userId,
    fetchUser
  );
  
  // 自動的な依存関係追跡
  const displayName = createMemo(() => {
    const userData = user();
    return userData ? `${userData.name} (${userData.email})` : '';
  });
  
  return (
    <div class="user-profile">
      <Show
        when={!user.loading}
        fallback={<div>Loading...</div>}
      >
        <Show
          when={!user.error}
          fallback={<div>Error: {user.error}</div>}
        >
          <Show
            when={user()}
            fallback={<div>User not found</div>}
          >
            {(user) => (
              <>
                <img src={user().avatar} alt={user().name} />
                <h2>{displayName()}</h2>
              </>
            )}
          </Show>
        </Show>
      </Show>
    </div>
  );
}

export default UserProfile;
React Component
// React版 UserProfile
import React, { useState, useEffect, useMemo } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
}

const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('User not found');
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Error');
      } finally {
        setLoading(false);
      }
    }
    
    fetchUser();
  }, [userId]);
  
  const displayName = useMemo(() => {
    return user ? `${user.name} (${user.email})` : '';
  }, [user]);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>User not found</div>;
  
  return (
    <div className="user-profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{displayName}</h2>
    </div>
  );
};

export default UserProfile;
SolidJS Component
// SolidJS版 UserProfile
import { createResource, createMemo, Show } from 'solid-js';

interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
}

async function fetchUser(userId: string): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) throw new Error('User not found');
  return response.json();
}

interface UserProfileProps {
  userId: string;
}

function UserProfile(props: UserProfileProps) {
  // createResource で非同期データ取得
  const [user] = createResource(
    () => props.userId,
    fetchUser
  );
  
  // 自動的な依存関係追跡
  const displayName = createMemo(() => {
    const userData = user();
    return userData ? `${userData.name} (${userData.email})` : '';
  });
  
  return (
    <div class="user-profile">
      <Show
        when={!user.loading}
        fallback={<div>Loading...</div>}
      >
        <Show
          when={!user.error}
          fallback={<div>Error: {user.error}</div>}
        >
          <Show
            when={user()}
            fallback={<div>User not found</div>}
          >
            {(user) => (
              <>
                <img src={user().avatar} alt={user().name} />
                <h2>{displayName()}</h2>
              </>
            )}
          </Show>
        </Show>
      </Show>
    </div>
  );
}

export default UserProfile;

テストとデバッグ

テスト戦略

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import solid from 'vite-plugin-solid';

export default defineConfig({
  plugins: [solid()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test-setup.ts']
  }
});
// __tests__/Counter.test.tsx
import { render, fireEvent, screen } from '@solidjs/testing-library';
import '@testing-library/jest-dom';
import Counter from '../components/Counter';

describe('Counter', () => {
  test('renders initial count', () => {
    render(() => <Counter initialCount={5} />);
    expect(screen.getByText('Count: 5')).toBeInTheDocument();
  });
  
  test('increments count on button click', async () => {
    render(() => <Counter initialCount={0} />);
    
    const button = screen.getByRole('button', { name: /increment/i });
    await fireEvent.click(button);
    
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });
  
  test('updates display name reactively', async () => {
    render(() => <Counter initialCount={0} />);
    
    const input = screen.getByLabelText(/name/i);
    await fireEvent.input(input, { target: { value: 'Test User' } });
    
    expect(screen.getByText('Hello, Test User!')).toBeInTheDocument();
  });
});

パフォーマンステスト

// performance.test.ts
import { render } from '@solidjs/testing-library';
import { performance } from 'perf_hooks';

describe('Performance Tests', () => {
  test('large list rendering performance', () => {
    const items = Array.from({ length: 10000 }, (_, i) => ({
      id: i,
      name: `Item ${i}`,
      value: Math.random()
    }));
    
    const startTime = performance.now();
    
    render(() => <LargeList items={items} />);
    
    const endTime = performance.now();
    const renderTime = endTime - startTime;
    
    // 10,000 アイテムのレンダリングが100ms以内で完了することを確認
    expect(renderTime).toBeLessThan(100);
  });
  
  test('rapid updates performance', async () => {
    const { container } = render(() => <RapidUpdateComponent />);
    
    const startTime = performance.now();
    
    // 1000回の連続更新をシミュレート
    for (let i = 0; i < 1000; i++) {
      const button = container.querySelector('button');
      await fireEvent.click(button);
    }
    
    const endTime = performance.now();
    const updateTime = endTime - startTime;
    
    // 1000回の更新が500ms以内で完了することを確認
    expect(updateTime).toBeLessThan(500);
  });
});

実装上の注意点とベストプラクティス

よくある落とし穴

SolidJS開発での注意点

  1. Signal の直接参照: count ではなく count() で値を取得
  2. Effect の依存関係: 明示的な指定は不要、自動追跡される
  3. オブジェクトの更新: イミュータブルな更新を心がける
  4. コンポーネントの再実行: 初回のみ実行され、再レンダリングなし

パフォーマンスガイドライン

SolidJS では「再レンダリング」という概念自体が存在しません。変更があった部分だけが更新され、残りは一切触れられることがありません。これが真のリアクティビティです。

SolidJSコア開発者 Ryan Carniato
// ベストプラクティス集
import { createSignal, createMemo, createStore, batch } from 'solid-js';

// ✅ 良い例
function GoodPractices() {
  const [count, setCount] = createSignal(0);
  const [multiplier, setMultiplier] = createSignal(2);
  
  // 計算された値はmemoを使用
  const result = createMemo(() => count() * multiplier());
  
  // バッチ更新で不要な再計算を防ぐ
  const updateBoth = () => {
    batch(() => {
      setCount(c => c + 1);
      setMultiplier(m => m + 1);
    });
  };
  
  // 条件付きEffect(依存関係が変わった時のみ実行)
  createEffect(() => {
    if (count() > 10) {
      console.log('Count exceeded 10');
    }
  });
  
  return (
    <div>
      <p>Result: {result()}</p>
      <button onClick={updateBoth}>Update Both</button>
    </div>
  );
}

// ❌ 悪い例
function BadPractices() {
  const [count, setCount] = createSignal(0);
  
  // 毎回新しい関数を作成(非効率)
  const handleClick = () => setCount(count() + 1);
  
  // 不要な計算の繰り返し
  return (
    <div>
      <p>Count: {count()}</p>
      <p>Double: {count() * 2}</p> {/* memoを使うべき */}
      <p>Triple: {count() * 3}</p> {/* memoを使うべき */}
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

まとめと将来展望

SolidJS は 2025 年において、フロントエンド開発の新たなスタンダードとして確固たる地位を築いています。React比 3 倍の高速化を実現しながらも学習コストを最小限に抑えた設計は、多くの開発者にとって魅力的な選択肢となっています。

SolidJSの主要な利点:

  • 圧倒的なパフォーマンス: 細粒度リアクティビティによる最適化
  • 低い学習コスト: React開発者が短期間で習得可能
  • 小さなバンドルサイズ: パフォーマンス重視のアプリケーションに最適
  • 優れた開発体験: TypeScript完全対応と充実した開発ツール

採用を検討すべきケース:

  • パフォーマンスが重要なアプリケーション
  • モバイル環境での動作が重要なプロジェクト
  • 新規プロジェクトまたは段階的移行が可能な既存プロジェクト
  • チームが Reactの経験を持っている場合

2025 年後半から 2026 年にかけて、SolidJS はさらなる成長が予想されます。エンタープライズ向け機能の充実、エコシステムの拡大、そして大手企業での採用事例増加により、Reactの強力な代替選択肢として位置づけられることでしょう。

フロントエンド開発の未来を見据えた技術選択として、SolidJS は間違いなく検討に値するフレームワークです。

Rinaのプロフィール画像

Rina

Daily Hack 編集長

フルスタックエンジニアとして10年以上の経験を持つ。 大手IT企業やスタートアップでの開発経験を活かし、 実践的で即効性のある技術情報を日々発信中。 特にWeb開発、クラウド技術、AI活用に精通。

この記事は役に立ちましたか?

あなたのフィードバックが記事の改善に役立ちます

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。