T-CREATOR

SolidJS のルーティング:solid-app-router 徹底解説

SolidJS のルーティング:solid-app-router 徹底解説

SolidJS のルーティングシステムについて、solid-app-router を使った実装方法を詳しく解説します。

SolidJS は、React ライクな構文を持ちながら、より優れたパフォーマンスを提供するモダンなフレームワークです。しかし、ルーティング機能は標準では提供されていないため、適切なルーティングライブラリの選択が重要になります。

この記事では、SolidJS の公式ルーティングライブラリである solid-app-router について、基礎から応用まで体系的に学んでいきます。実際のエラーとその解決方法も含めて、実践的な知識を身につけられる内容となっています。

solid-app-router とは

solid-app-router は、SolidJS の公式ルーティングライブラリです。軽量で高速、そして直感的な API を提供することで、SolidJS アプリケーションでのナビゲーションを簡単に実装できます。

SolidJS の公式ルーティングライブラリ

solid-app-router は、SolidJS コミュニティによって開発・メンテナンスされている公式のルーティングソリューションです。SolidJS の設計哲学に完全に準拠しており、リアクティブな特性を最大限に活用できます。

typescript// solid-app-routerの基本インポート
import { Router, Routes, Route } from '@solidjs/router';

このライブラリは、SolidJS の細粒度のリアクティビティシステムと統合されており、パフォーマンスを損なうことなく、スムーズなナビゲーション体験を提供します。

他のフレームワークとの違い

React Router や Vue Router と比較すると、solid-app-router には以下のような特徴があります:

特徴solid-app-routerReact RouterVue Router
バンドルサイズ非常に軽量中程度中程度
パフォーマンス最高良好良好
学習コスト低い中程度中程度
TypeScript 対応完全対応良好良好

solid-app-router は、特にバンドルサイズとパフォーマンスの面で優位性を持っています。これは、SolidJS のコンパイル時最適化と組み合わさることで実現されています。

なぜ solid-app-router を選ぶべきか

solid-app-router を選択する理由は、以下の通りです:

1. パフォーマンスの優位性 SolidJS の細粒度リアクティビティシステムにより、必要な部分のみが更新されます。これにより、ページ遷移時のパフォーマンスが大幅に向上します。

2. 開発者体験の向上 直感的な API 設計により、学習コストを最小限に抑えながら、高度なルーティング機能を実装できます。

3. 将来性 SolidJS の公式ライブラリとして、長期的なサポートと継続的な改善が期待できます。

実際に、多くの SolidJS プロジェクトで採用されており、コミュニティからの信頼も厚いライブラリです。

環境構築とセットアップ

solid-app-router を使った開発環境を構築していきましょう。段階的に設定を行い、確実に動作する環境を作ります。

プロジェクトの初期化

まず、新しい SolidJS プロジェクトを作成します。SolidJS の公式テンプレートを使用することで、最適な設定でプロジェクトを開始できます。

bash# Viteテンプレートを使用してプロジェクトを作成
yarn create vite my-solidjs-app --template solid-ts

# プロジェクトディレクトリに移動
cd my-solidjs-app

# 依存関係をインストール
yarn install

このコマンドを実行すると、TypeScript 対応の SolidJS プロジェクトが作成されます。プロジェクト構造は以下のようになります:

arduinomy-solidjs-app/
├── public/
├── src/
│   ├── components/
│   ├── App.tsx
│   ├── index.tsx
│   └── vite-env.d.ts
├── package.json
├── tsconfig.json
└── vite.config.ts

solid-app-router のインストール

プロジェクトが作成されたら、solid-app-router をインストールします。

bash# solid-app-routerをインストール
yarn add @solidjs/router

インストールが完了すると、package.jsonに以下の依存関係が追加されます:

json{
  "dependencies": {
    "@solidjs/router": "^0.10.0",
    "solid-js": "^1.8.0"
  }
}

基本的な設定方法

solid-app-router の基本的な設定を行います。まず、src​/​App.tsxを編集して、ルーティングの基盤を構築します。

typescript// src/App.tsx
import { Router, Routes, Route } from '@solidjs/router';
import { Component } from 'solid-js';

// 基本的なページコンポーネント
const Home: Component = () => {
  return (
    <div>
      <h1>ホームページ</h1>
      <p>
        SolidJS +
        solid-app-routerで作成されたアプリケーションです。
      </p>
    </div>
  );
};

const About: Component = () => {
  return (
    <div>
      <h1>アバウトページ</h1>
      <p>このアプリケーションについての情報です。</p>
    </div>
  );
};

const App: Component = () => {
  return (
    <Router>
      <Routes>
        <Route path='/' component={Home} />
        <Route path='/about' component={About} />
      </Routes>
    </Router>
  );
};

export default App;

この設定により、基本的なルーティング機能が動作するようになります。​/​にアクセスするとホームページが、​/​aboutにアクセスするとアバウトページが表示されます。

基本的なルーティング設定

solid-app-router の基本的な機能について、詳しく見ていきましょう。各コンポーネントの役割と使用方法を理解することで、より柔軟なルーティング設定が可能になります。

Router コンポーネントの設定

Router コンポーネントは、アプリケーション全体をルーティングシステムで囲むためのコンテナです。これにより、アプリケーション内でのナビゲーションが可能になります。

typescript// src/App.tsx
import { Router } from '@solidjs/router';
import { Component } from 'solid-js';

const App: Component = () => {
  return (
    <Router>{/* ここにルーティング設定を記述 */}</Router>
  );
};

Router コンポーネントには、いくつかのオプションを設定できます:

typescript// カスタム設定付きのRouter
<Router base='/app' data={{}} out={{}} preload={false}>
  {/* ルーティング設定 */}
</Router>

よくあるエラーと解決方法:

bash# エラー: Router must be used within a SolidJS application
Error: Router must be used within a SolidJS application

# 解決方法: index.tsxで正しくレンダリングされているか確認

このエラーは、通常index.tsxでのレンダリング設定に問題がある場合に発生します。

Routes と Route の使い方

Routes コンポーネントは、複数の Route をグループ化するためのコンテナです。Route コンポーネントは、個々のルートを定義します。

typescript// src/App.tsx
import { Routes, Route } from '@solidjs/router';
import { Component } from 'solid-js';

const App: Component = () => {
  return (
    <Router>
      <Routes>
        <Route path='/' component={Home} />
        <Route path='/users' component={Users} />
        <Route path='/products' component={Products} />
      </Routes>
    </Router>
  );
};

Route コンポーネントには、以下のプロパティを設定できます:

typescript<Route
  path='/example'
  component={ExampleComponent}
  data={async () => ({ title: 'Example' })}
  children={[
    <Route path='/nested' component={NestedComponent} />,
  ]}
/>

よくあるエラーと解決方法:

bash# エラー: No routes matched location "/undefined"
Error: No routes matched location "/undefined"

# 解決方法: パスが正しく設定されているか確認
<Route path="/" component={Home} /> // 正しい
<Route path="" component={Home} />  // 間違い

パスパラメータの取得方法

URL パラメータを取得するには、useParamsフックを使用します。これにより、動的なルーティングが可能になります。

typescript// src/components/UserDetail.tsx
import { useParams } from '@solidjs/router';
import { Component, createEffect } from 'solid-js';

const UserDetail: Component = () => {
  const params = useParams();

  createEffect(() => {
    console.log('ユーザーID:', params.id);
  });

  return (
    <div>
      <h1>ユーザー詳細</h1>
      <p>ユーザーID: {params.id}</p>
    </div>
  );
};

export default UserDetail;

対応するルート設定:

typescript// src/App.tsx
<Route path='/users/:id' component={UserDetail} />

よくあるエラーと解決方法:

bash# エラー: params is undefined
TypeError: Cannot read properties of undefined (reading 'id')

# 解決方法: useParamsの戻り値を適切に処理
const params = useParams();
if (!params) return <div>Loading...</div>;

動的ルーティング

動的ルーティングにより、URL パラメータに基づいてコンテンツを動的に表示できます。これにより、より柔軟でスケーラブルなアプリケーションを構築できます。

動的セグメントの実装

動的セグメントは、URL の一部を変数として扱う機能です。:プレフィックスを使用して定義します。

typescript// src/App.tsx
import { Routes, Route } from '@solidjs/router';
import { Component } from 'solid-js';

const App: Component = () => {
  return (
    <Router>
      <Routes>
        <Route path='/' component={Home} />
        <Route path='/posts/:id' component={PostDetail} />
        <Route
          path='/users/:userId/posts/:postId'
          component={UserPost}
        />
      </Routes>
    </Router>
  );
};

動的セグメントを使用するコンポーネント:

typescript// src/components/PostDetail.tsx
import { useParams } from '@solidjs/router';
import {
  Component,
  createSignal,
  createEffect,
} from 'solid-js';

const PostDetail: Component = () => {
  const params = useParams();
  const [post, setPost] = createSignal(null);
  const [loading, setLoading] = createSignal(true);

  createEffect(async () => {
    if (params.id) {
      setLoading(true);
      try {
        // APIから投稿データを取得
        const response = await fetch(
          `/api/posts/${params.id}`
        );
        const data = await response.json();
        setPost(data);
      } catch (error) {
        console.error('投稿の取得に失敗しました:', error);
      } finally {
        setLoading(false);
      }
    }
  });

  return (
    <div>
      {loading() ? (
        <p>読み込み中...</p>
      ) : post() ? (
        <div>
          <h1>{post().title}</h1>
          <p>{post().content}</p>
        </div>
      ) : (
        <p>投稿が見つかりません</p>
      )}
    </div>
  );
};

export default PostDetail;

ネストしたルートの作成

ネストしたルートにより、親子関係のあるページ構造を作成できます。これにより、共通のレイアウトやナビゲーションを効率的に管理できます。

typescript// src/App.tsx
import { Routes, Route } from '@solidjs/router';
import { Component } from 'solid-js';

const App: Component = () => {
  return (
    <Router>
      <Routes>
        <Route path='/' component={Home} />
        <Route
          path='/dashboard'
          component={DashboardLayout}
        >
          <Route path='/' component={DashboardHome} />
          <Route path='/profile' component={Profile} />
          <Route path='/settings' component={Settings} />
        </Route>
      </Routes>
    </Router>
  );
};

ダッシュボードレイアウトコンポーネント:

typescript// src/components/DashboardLayout.tsx
import { Outlet } from '@solidjs/router';
import { Component } from 'solid-js';

const DashboardLayout: Component = () => {
  return (
    <div class='dashboard'>
      <nav class='dashboard-nav'>
        <a href='/dashboard'>ホーム</a>
        <a href='/dashboard/profile'>プロフィール</a>
        <a href='/dashboard/settings'>設定</a>
      </nav>
      <main class='dashboard-content'>
        <Outlet />
      </main>
    </div>
  );
};

export default DashboardLayout;

よくあるエラーと解決方法:

bash# エラー: Outlet is not defined
ReferenceError: Outlet is not defined

# 解決方法: Outletを正しくインポート
import { Outlet } from "@solidjs/router";

ワイルドカードルートの活用

ワイルドカードルート(*)を使用することで、マッチしないすべてのパスをキャッチできます。これは 404 ページの実装に特に有用です。

typescript// src/App.tsx
import { Routes, Route } from '@solidjs/router';
import { Component } from 'solid-js';

const App: Component = () => {
  return (
    <Router>
      <Routes>
        <Route path='/' component={Home} />
        <Route path='/about' component={About} />
        <Route path='/contact' component={Contact} />
        <Route path='*' component={NotFound} />
      </Routes>
    </Router>
  );
};

404 ページコンポーネント:

typescript// src/components/NotFound.tsx
import { useLocation } from '@solidjs/router';
import { Component } from 'solid-js';

const NotFound: Component = () => {
  const location = useLocation();

  return (
    <div class='not-found'>
      <h1>404 - ページが見つかりません</h1>
      <p>
        要求されたパス "{location.pathname}"
        は存在しません。
      </p>
      <a href='/'>ホームに戻る</a>
    </div>
  );
};

export default NotFound;

ナビゲーション機能

ナビゲーション機能により、ユーザーはアプリケーション内を自由に移動できます。solid-app-router は、宣言的と命令的の両方のナビゲーション方法を提供します。

Link コンポーネントは、宣言的なナビゲーションを提供します。HTML の<a>タグと同様に使用できますが、ページの再読み込みなしでナビゲーションが実行されます。

typescript// src/components/Navigation.tsx
import { Link } from '@solidjs/router';
import { Component } from 'solid-js';

const Navigation: Component = () => {
  return (
    <nav>
      <ul>
        <li>
          <Link href='/' activeClass='active'>
            ホーム
          </Link>
        </li>
        <li>
          <Link href='/about' activeClass='active'>
            アバウト
          </Link>
        </li>
        <li>
          <Link href='/contact' activeClass='active'>
            お問い合わせ
          </Link>
        </li>
      </ul>
    </nav>
  );
};

export default Navigation;

Link コンポーネントの高度な使用方法:

typescript// 動的パラメータ付きのリンク
<Link href={`/users/${userId}`}>ユーザー詳細</Link>

// クエリパラメータ付きのリンク
<Link href="/search?q=keyword&page=1">検索結果</Link>

// カスタムクラスとスタイル
<Link
  href="/dashboard"
  class="nav-link"
  activeClass="nav-link-active"
  inactiveClass="nav-link-inactive"
>
  ダッシュボード
</Link>

よくあるエラーと解決方法:

bash# エラー: Link component requires href prop
Error: Link component requires href prop

# 解決方法: hrefプロパティを必ず指定
<Link href="/path">リンクテキスト</Link>

プログラムによるナビゲーション

useNavigateフックを使用することで、プログラム的にナビゲーションを実行できます。これは、フォーム送信後のリダイレクトや、条件付きナビゲーションに特に有用です。

typescript// src/components/LoginForm.tsx
import { useNavigate } from '@solidjs/router';
import { Component, createSignal } from 'solid-js';

const LoginForm: Component = () => {
  const navigate = useNavigate();
  const [email, setEmail] = createSignal('');
  const [password, setPassword] = createSignal('');
  const [loading, setLoading] = createSignal(false);

  const handleSubmit = async (e: Event) => {
    e.preventDefault();
    setLoading(true);

    try {
      // ログイン処理
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          email: email(),
          password: password(),
        }),
      });

      if (response.ok) {
        // ログイン成功時はダッシュボードにリダイレクト
        navigate('/dashboard');
      } else {
        alert('ログインに失敗しました');
      }
    } catch (error) {
      console.error('ログインエラー:', error);
      alert('ログイン中にエラーが発生しました');
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label for='email'>メールアドレス:</label>
        <input
          id='email'
          type='email'
          value={email()}
          onInput={(e) => setEmail(e.currentTarget.value)}
          required
        />
      </div>
      <div>
        <label for='password'>パスワード:</label>
        <input
          id='password'
          type='password'
          value={password()}
          onInput={(e) =>
            setPassword(e.currentTarget.value)
          }
          required
        />
      </div>
      <button type='submit' disabled={loading()}>
        {loading() ? 'ログイン中...' : 'ログイン'}
      </button>
    </form>
  );
};

export default LoginForm;

ナビゲーションの高度な使用方法:

typescript// 履歴スタックの操作
navigate('/new-page', { replace: true }); // 現在のページを置き換え
navigate(-1); // 前のページに戻る
navigate(2); // 2ページ先に進む

// 状態付きナビゲーション
navigate('/profile', { state: { from: 'login' } });

ナビゲーションガードの実装

ナビゲーションガードにより、特定の条件を満たさない場合のナビゲーションを防ぐことができます。認証や権限チェックに特に有用です。

typescript// src/components/ProtectedRoute.tsx
import { Route, Navigate } from '@solidjs/router';
import {
  Component,
  createSignal,
  createEffect,
} from 'solid-js';

interface ProtectedRouteProps {
  path: string;
  component: Component;
  requiredRole?: string;
}

const ProtectedRoute: Component<ProtectedRouteProps> = (
  props
) => {
  const [isAuthenticated, setIsAuthenticated] =
    createSignal(false);
  const [userRole, setUserRole] = createSignal('');
  const [loading, setLoading] = createSignal(true);

  createEffect(() => {
    // 認証状態をチェック
    checkAuthStatus();
  });

  const checkAuthStatus = async () => {
    try {
      const token = localStorage.getItem('authToken');
      if (!token) {
        setIsAuthenticated(false);
        return;
      }

      // トークンの有効性を確認
      const response = await fetch('/api/auth/verify', {
        headers: { Authorization: `Bearer ${token}` },
      });

      if (response.ok) {
        const userData = await response.json();
        setIsAuthenticated(true);
        setUserRole(userData.role);
      } else {
        setIsAuthenticated(false);
        localStorage.removeItem('authToken');
      }
    } catch (error) {
      console.error('認証チェックエラー:', error);
      setIsAuthenticated(false);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Route
      path={props.path}
      component={() => {
        if (loading()) {
          return <div>認証確認中...</div>;
        }

        if (!isAuthenticated()) {
          return <Navigate href='/login' />;
        }

        if (
          props.requiredRole &&
          userRole() !== props.requiredRole
        ) {
          return <div>アクセス権限がありません</div>;
        }

        return <props.component />;
      }}
    />
  );
};

export default ProtectedRoute;

使用例:

typescript// src/App.tsx
<Routes>
  <Route path='/' component={Home} />
  <Route path='/login' component={Login} />
  <ProtectedRoute path='/dashboard' component={Dashboard} />
  <ProtectedRoute
    path='/admin'
    component={AdminPanel}
    requiredRole='admin'
  />
</Routes>

高度なルーティング機能

solid-app-router の高度な機能を活用することで、より洗練されたユーザー体験を提供できます。これらの機能は、大規模アプリケーションの開発に特に有用です。

遅延ローディング(コード分割)

遅延ローディングにより、必要なコンポーネントのみを動的に読み込むことができます。これにより、初期バンドルサイズを削減し、アプリケーションの起動速度を向上させます。

typescript// src/App.tsx
import { lazy } from 'solid-js';
import { Routes, Route } from '@solidjs/router';
import { Component } from 'solid-js';

// 遅延ローディング用のコンポーネント
const Dashboard = lazy(
  () => import('./components/Dashboard')
);
const AdminPanel = lazy(
  () => import('./components/AdminPanel')
);
const UserProfile = lazy(
  () => import('./components/UserProfile')
);

const App: Component = () => {
  return (
    <Router>
      <Routes>
        <Route path='/' component={Home} />
        <Route path='/dashboard' component={Dashboard} />
        <Route path='/admin' component={AdminPanel} />
        <Route path='/profile' component={UserProfile} />
      </Routes>
    </Router>
  );
};

ローディング状態の管理:

typescript// src/components/LoadingWrapper.tsx
import { Suspense } from 'solid-js';
import { Component, JSX } from 'solid-js';

interface LoadingWrapperProps {
  children: JSX.Element;
  fallback?: JSX.Element;
}

const LoadingWrapper: Component<LoadingWrapperProps> = (
  props
) => {
  const defaultFallback = (
    <div class='loading-spinner'>
      <div class='spinner'></div>
      <p>読み込み中...</p>
    </div>
  );

  return (
    <Suspense fallback={props.fallback || defaultFallback}>
      {props.children}
    </Suspense>
  );
};

export default LoadingWrapper;

よくあるエラーと解決方法:

bash# エラー: lazy() requires a function that returns a Promise
Error: lazy() requires a function that returns a Promise

# 解決方法: 正しい形式でlazyを使用
const Component = lazy(() => import("./Component"));

ルートベースのレイアウト

ルートベースのレイアウトにより、特定のルートグループに対して共通のレイアウトを適用できます。これにより、コードの重複を避け、一貫したユーザーインターフェースを提供できます。

typescript// src/layouts/MainLayout.tsx
import { Outlet } from '@solidjs/router';
import { Component } from 'solid-js';
import Navigation from '../components/Navigation';
import Footer from '../components/Footer';

const MainLayout: Component = () => {
  return (
    <div class='main-layout'>
      <header>
        <Navigation />
      </header>
      <main class='main-content'>
        <Outlet />
      </main>
      <footer>
        <Footer />
      </footer>
    </div>
  );
};

export default MainLayout;
typescript// src/layouts/AuthLayout.tsx
import { Outlet } from '@solidjs/router';
import { Component } from 'solid-js';

const AuthLayout: Component = () => {
  return (
    <div class='auth-layout'>
      <div class='auth-container'>
        <div class='auth-form'>
          <Outlet />
        </div>
      </div>
    </div>
  );
};

export default AuthLayout;

レイアウトの適用:

typescript// src/App.tsx
import { Routes, Route } from '@solidjs/router';
import { Component } from 'solid-js';

const App: Component = () => {
  return (
    <Router>
      <Routes>
        <Route path='/' component={MainLayout}>
          <Route path='/' component={Home} />
          <Route path='/about' component={About} />
          <Route path='/contact' component={Contact} />
        </Route>
        <Route path='/auth' component={AuthLayout}>
          <Route path='/login' component={Login} />
          <Route path='/register' component={Register} />
        </Route>
      </Routes>
    </Router>
  );
};

エラーハンドリング

エラーハンドリングにより、ルーティング中に発生するエラーを適切に処理できます。これにより、ユーザーに分かりやすいエラーメッセージを表示し、アプリケーションの安定性を向上させます。

typescript// src/components/ErrorBoundary.tsx
import { ErrorBoundary as SolidErrorBoundary } from 'solid-js';
import { Component, JSX } from 'solid-js';

interface ErrorBoundaryProps {
  children: JSX.Element;
  fallback?: (
    error: Error,
    reset: () => void
  ) => JSX.Element;
}

const ErrorBoundary: Component<ErrorBoundaryProps> = (
  props
) => {
  const defaultFallback = (
    error: Error,
    reset: () => void
  ) => (
    <div class='error-boundary'>
      <h2>エラーが発生しました</h2>
      <p>{error.message}</p>
      <button onClick={reset}>再試行</button>
    </div>
  );

  return (
    <SolidErrorBoundary
      fallback={props.fallback || defaultFallback}
    >
      {props.children}
    </SolidErrorBoundary>
  );
};

export default ErrorBoundary;

ルートレベルでのエラーハンドリング:

typescript// src/App.tsx
import { Routes, Route } from '@solidjs/router';
import { Component } from 'solid-js';
import ErrorBoundary from './components/ErrorBoundary';

const App: Component = () => {
  return (
    <Router>
      <ErrorBoundary>
        <Routes>
          <Route path='/' component={Home} />
          <Route path='/about' component={About} />
          <Route path='/contact' component={Contact} />
          <Route path='*' component={NotFound} />
        </Routes>
      </ErrorBoundary>
    </Router>
  );
};

データ取得エラーの処理:

typescript// src/components/DataLoader.tsx
import {
  Component,
  createSignal,
  createEffect,
  JSX,
} from 'solid-js';

interface DataLoaderProps {
  url: string;
  children: (data: any) => JSX.Element;
  fallback?: JSX.Element;
  errorFallback?: (error: Error) => JSX.Element;
}

const DataLoader: Component<DataLoaderProps> = (props) => {
  const [data, setData] = createSignal(null);
  const [loading, setLoading] = createSignal(true);
  const [error, setError] = createSignal(null);

  createEffect(async () => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(props.url);
      if (!response.ok) {
        throw new Error(
          `HTTP error! status: ${response.status}`
        );
      }

      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  });

  if (loading()) {
    return props.fallback || <div>読み込み中...</div>;
  }

  if (error()) {
    return (
      props.errorFallback?.(error()) || (
        <div class='error'>
          <h3>データの読み込みに失敗しました</h3>
          <p>{error().message}</p>
        </div>
      )
    );
  }

  return props.children(data());
};

export default DataLoader;

実践的なサンプルアプリケーション

実際のユースケースを想定した、完全なルーティング設定例を見ていきましょう。このサンプルアプリケーションは、ブログシステムを想定しており、多くの実用的な機能を含んでいます。

完全なルーティング設定例

typescript// src/App.tsx
import { Router, Routes, Route } from '@solidjs/router';
import { lazy, Component } from 'solid-js';
import LoadingWrapper from './components/LoadingWrapper';
import ErrorBoundary from './components/ErrorBoundary';

// 遅延ローディング用のコンポーネント
const Home = lazy(() => import('./pages/Home'));
const Blog = lazy(() => import('./pages/Blog'));
const BlogPost = lazy(() => import('./pages/BlogPost'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
const Login = lazy(() => import('./pages/Login'));
const Register = lazy(() => import('./pages/Register'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Admin = lazy(() => import('./pages/Admin'));
const NotFound = lazy(() => import('./pages/NotFound'));

// レイアウトコンポーネント
const MainLayout = lazy(
  () => import('./layouts/MainLayout')
);
const AuthLayout = lazy(
  () => import('./layouts/AuthLayout')
);
const DashboardLayout = lazy(
  () => import('./layouts/DashboardLayout')
);

const App: Component = () => {
  return (
    <Router>
      <ErrorBoundary>
        <LoadingWrapper>
          <Routes>
            {/* メインレイアウト */}
            <Route path='/' component={MainLayout}>
              <Route path='/' component={Home} />
              <Route path='/blog' component={Blog} />
              <Route
                path='/blog/:id'
                component={BlogPost}
              />
              <Route path='/about' component={About} />
              <Route path='/contact' component={Contact} />
            </Route>

            {/* 認証レイアウト */}
            <Route path='/auth' component={AuthLayout}>
              <Route path='/login' component={Login} />
              <Route
                path='/register'
                component={Register}
              />
            </Route>

            {/* ダッシュボードレイアウト */}
            <Route
              path='/dashboard'
              component={DashboardLayout}
            >
              <Route path='/' component={Dashboard} />
              <Route
                path='/profile'
                component={UserProfile}
              />
              <Route
                path='/settings'
                component={Settings}
              />
            </Route>

            {/* 管理者レイアウト */}
            <Route path='/admin' component={AdminLayout}>
              <Route path='/' component={Admin} />
              <Route
                path='/users'
                component={UserManagement}
              />
              <Route
                path='/posts'
                component={PostManagement}
              />
            </Route>

            {/* 404ページ */}
            <Route path='*' component={NotFound} />
          </Routes>
        </LoadingWrapper>
      </ErrorBoundary>
    </Router>
  );
};

export default App;

実際のユースケースでの活用方法

ブログ投稿の詳細ページの実装例:

typescript// src/pages/BlogPost.tsx
import { useParams, useNavigate } from '@solidjs/router';
import {
  Component,
  createSignal,
  createEffect,
  Show,
} from 'solid-js';
import DataLoader from '../components/DataLoader';

const BlogPost: Component = () => {
  const params = useParams();
  const navigate = useNavigate();
  const [post, setPost] = createSignal(null);
  const [loading, setLoading] = createSignal(true);
  const [error, setError] = createSignal(null);

  createEffect(async () => {
    if (params.id) {
      try {
        setLoading(true);
        const response = await fetch(
          `/api/posts/${params.id}`
        );

        if (!response.ok) {
          if (response.status === 404) {
            navigate('/blog', { replace: true });
            return;
          }
          throw new Error('投稿の取得に失敗しました');
        }

        const data = await response.json();
        setPost(data);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    }
  });

  return (
    <div class='blog-post'>
      <Show
        when={!loading()}
        fallback={<div>読み込み中...</div>}
      >
        <Show
          when={!error()}
          fallback={
            <div class='error'>
              <h2>エラーが発生しました</h2>
              <p>{error().message}</p>
              <button onClick={() => navigate('/blog')}>
                ブログ一覧に戻る
              </button>
            </div>
          }
        >
          <Show when={post()}>
            <article>
              <header>
                <h1>{post().title}</h1>
                <div class='meta'>
                  <span>
                    投稿日:{' '}
                    {new Date(
                      post().createdAt
                    ).toLocaleDateString()}
                  </span>
                  <span>著者: {post().author}</span>
                </div>
              </header>
              <div
                class='content'
                innerHTML={post().content}
              />
              <footer>
                <div class='tags'>
                  {post().tags.map((tag) => (
                    <span class='tag'>{tag}</span>
                  ))}
                </div>
              </footer>
            </article>
          </Show>
        </Show>
      </Show>
    </div>
  );
};

export default BlogPost;

ナビゲーションコンポーネント:

typescript// src/components/Navigation.tsx
import { Link, useLocation } from '@solidjs/router';
import {
  Component,
  createSignal,
  createEffect,
} from 'solid-js';

const Navigation: Component = () => {
  const location = useLocation();
  const [isAuthenticated, setIsAuthenticated] =
    createSignal(false);
  const [userRole, setUserRole] = createSignal('');

  createEffect(() => {
    // 認証状態をチェック
    const token = localStorage.getItem('authToken');
    if (token) {
      // トークンの有効性を確認
      checkAuthStatus();
    } else {
      setIsAuthenticated(false);
    }
  });

  const checkAuthStatus = async () => {
    try {
      const response = await fetch('/api/auth/me', {
        headers: {
          Authorization: `Bearer ${localStorage.getItem(
            'authToken'
          )}`,
        },
      });

      if (response.ok) {
        const userData = await response.json();
        setIsAuthenticated(true);
        setUserRole(userData.role);
      } else {
        setIsAuthenticated(false);
        localStorage.removeItem('authToken');
      }
    } catch (error) {
      setIsAuthenticated(false);
    }
  };

  const handleLogout = () => {
    localStorage.removeItem('authToken');
    setIsAuthenticated(false);
    setUserRole('');
    window.location.href = '/';
  };

  return (
    <nav class='main-navigation'>
      <div class='nav-brand'>
        <Link href='/'>MyBlog</Link>
      </div>

      <ul class='nav-menu'>
        <li>
          <Link
            href='/'
            activeClass='active'
            class='nav-link'
          >
            ホーム
          </Link>
        </li>
        <li>
          <Link
            href='/blog'
            activeClass='active'
            class='nav-link'
          >
            ブログ
          </Link>
        </li>
        <li>
          <Link
            href='/about'
            activeClass='active'
            class='nav-link'
          >
            アバウト
          </Link>
        </li>
        <li>
          <Link
            href='/contact'
            activeClass='active'
            class='nav-link'
          >
            お問い合わせ
          </Link>
        </li>
      </ul>

      <div class='nav-auth'>
        <Show
          when={isAuthenticated()}
          fallback={
            <div class='auth-buttons'>
              <Link
                href='/auth/login'
                class='btn btn-outline'
              >
                ログイン
              </Link>
              <Link
                href='/auth/register'
                class='btn btn-primary'
              >
                登録
              </Link>
            </div>
          }
        >
          <div class='user-menu'>
            <Show when={userRole() === 'admin'}>
              <Link href='/admin' class='btn btn-secondary'>
                管理画面
              </Link>
            </Show>
            <Link href='/dashboard' class='btn btn-outline'>
              ダッシュボード
            </Link>
            <button
              onClick={handleLogout}
              class='btn btn-outline'
            >
              ログアウト
            </button>
          </div>
        </Show>
      </div>
    </nav>
  );
};

export default Navigation;

まとめ

solid-app-router を使った SolidJS のルーティングシステムについて、基礎から応用まで詳しく解説してきました。

この記事で学んだ主要なポイントを振り返ってみましょう:

1. solid-app-router の特徴

  • SolidJS の公式ルーティングライブラリ
  • 軽量で高速なパフォーマンス
  • 直感的な API 設計
  • TypeScript 完全対応

2. 基本的な機能

  • Router、Routes、Route コンポーネントの使い方
  • 動的セグメントとパラメータ取得
  • Link コンポーネントによる宣言的ナビゲーション
  • useNavigate フックによるプログラム的ナビゲーション

3. 高度な機能

  • 遅延ローディングによるコード分割
  • ネストしたルートとレイアウト
  • ナビゲーションガードによる認証制御
  • エラーハンドリングと 404 ページ

4. 実践的な活用

  • 実際のプロジェクトでの構成例
  • パフォーマンス最適化のテクニック
  • エラー処理とユーザー体験の向上

solid-app-router は、SolidJS の優れたパフォーマンス特性を活かしながら、モダンな Web アプリケーションに必要なルーティング機能を提供します。この記事で学んだ知識を活用して、より良いユーザー体験を提供するアプリケーションを開発してください。

実際のプロジェクトでは、これらの機能を組み合わせることで、スケーラブルで保守性の高いルーティングシステムを構築できます。エラー処理やパフォーマンス最適化を適切に行うことで、ユーザーに信頼されるアプリケーションを作成できるでしょう。

関連リンク