SolidJS アプリのディレクトリ構成と設計原則

SolidJS は高いパフォーマンスと優れた開発体験を提供するモダンなフロントエンドフレームワークです。しかし、その真価を発揮するためには、適切なディレクトリ構成と設計原則の理解が欠かせません。
本記事では、SolidJS アプリケーションにおける効果的なディレクトリ構成の方法と、実践的な設計原則について詳しく解説いたします。初心者の方から経験豊富な開発者まで、すぐに実践できる知識をお伝えします。
背景
SolidJS の特徴とディレクトリ構成の重要性
SolidJS は React と似た記法を持ちながら、仮想 DOM を使わないリアクティブシステムを採用しています。この特徴により、従来のフレームワークとは異なるアプローチでディレクトリ構成を考える必要があります。
mermaidflowchart LR
component[コンポーネント] -->|リアクティブ| signal[Signal]
signal -->|自動更新| dom[実DOM]
component -->|直接操作| dom
style signal fill:#e1f5fe
style dom fill:#f3e5f5
SolidJS のリアクティブシステムでは、状態変更が直接 DOM に反映されるため、コンポーネントの責務分離がより重要になります。適切な構成により、このメリットを最大限活用できるでしょう。
React との違いと構成上の考慮点
React では仮想 DOM の diff による最適化が行われますが、SolidJS では Signal ベースの細粒度リアクティビティが採用されています。
項目 | React | SolidJS |
---|---|---|
レンダリング | 仮想 DOM + diff | 直接 DOM 更新 |
状態管理 | useState, useEffect | createSignal, createEffect |
最適化 | React.memo, useMemo | 自動最適化 |
バンドルサイズ | 大きめ | 小さめ |
この違いにより、SolidJS では以下の点を考慮した構成が必要です。
- Signal の適切な配置と管理
- エフェクトの依存関係の明確化
- コンポーネントの再利用性の向上
パフォーマンスを意識した設計の必要性
SolidJS の優れたパフォーマンスを活かすためには、構成レベルでの配慮が重要です。
以下の図は、パフォーマンスに影響する構成要素を示しています。
mermaidflowchart TD
app[アプリケーション] --> components[コンポーネント分割]
app --> state[状態管理]
app --> routing[ルーティング]
components --> lazy[遅延読み込み]
components --> memo[メモ化]
state --> global[グローバル状態]
state --> local[ローカル状態]
routing --> code[コード分割]
routing --> preload[プリロード]
style components fill:#e8f5e8
style state fill:#fff3e0
style routing fill:#e3f2fd
図で理解できる要点:
- コンポーネント分割により遅延読み込みとメモ化を効率化
- 状態管理の適切な配置でパフォーマンス向上
- ルーティング設計によるコード分割とプリロード最適化
課題
従来のフレームワークでの構成の問題点
多くの開発者が React や Vue.js の経験を SolidJS に持ち込むことで、以下のような問題が発生しています。
過度なコンポーネント分割 React の最適化手法をそのまま適用し、不要な細分化を行ってしまうケースが見られます。
typescript// 問題のある例:React の思考で作られた過度な分割
const UserCard = (props) => {
return (
<div>
<UserAvatar user={props.user} />
<UserName user={props.user} />
<UserEmail user={props.user} />
</div>
);
};
状態管理の混乱 React の useState パターンを無理に適用し、Signal の恩恵を受けられない構成になってしまいます。
SolidJS 特有の構成課題
Signal の管理範囲の曖昧さ どこで Signal を定義し、どのように共有するかの指針が不明確になりがちです。
typescript// よくない例:Signal の責務が不明確
const [userData, setUserData] = createSignal();
const [userSettings, setUserSettings] = createSignal();
const [userPermissions, setUserPermissions] = createSignal();
リアクティブ依存関係の複雑化 Effect の依存関係が複雑になり、意図しない更新が発生する問題があります。
チーム開発における構成統一の難しさ
開発チーム内でのディレクトリ構成の統一は、プロジェクトの成功に直結する重要な要素です。
課題 | 影響 | 対策の必要性 |
---|---|---|
命名規則の不統一 | 可読性低下 | 高 |
ファイル配置の曖昧さ | 保守性悪化 | 高 |
責務分離の不明確さ | バグ増加 | 中 |
解決策
SolidJS 推奨ディレクトリ構成
実践的で保守性の高いディレクトリ構成をご紹介します。
bashsrc/
├── components/ # 再利用可能なコンポーネント
│ ├── ui/ # UI コンポーネント
│ ├── forms/ # フォーム関連
│ └── layout/ # レイアウト関連
├── pages/ # ページコンポーネント
├── stores/ # 状態管理
├── utils/ # ユーティリティ関数
├── types/ # TypeScript 型定義
├── hooks/ # カスタムフック
├── services/ # API 通信
└── assets/ # 静的ファイル
この構成では、各ディレクトリが明確な責務を持ち、開発者が迷わずにファイルを配置できます。
Feature-based vs Layer-based アプローチ
Layer-based アプローチ(技術層による分割)
csssrc/
├── components/
├── services/
├── stores/
└── utils/
小規模〜中規模プロジェクトに適しており、技術的な関心事で分離されています。
Feature-based アプローチ(機能による分割)
csssrc/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ ├── stores/
│ │ └── services/
│ └── dashboard/
│ ├── components/
│ ├── stores/
│ └── services/
└── shared/
├── components/
├── utils/
└── types/
大規模プロジェクトや複数チーム開発に適しており、ビジネス機能単位で分離されています。
設計原則とベストプラクティス
単一責任の原則 各コンポーネントは一つの明確な責任を持つべきです。
typescript// 良い例:単一責任を持つコンポーネント
const UserAvatar = (props: { user: User; size: 'sm' | 'md' | 'lg' }) => {
return (
<img
src={props.user.avatar}
alt={`${props.user.name}のアバター`}
class={`avatar avatar-${props.size}`}
/>
);
};
Signal の適切な配置 Signal は使用される最小のスコープで定義し、必要に応じて上位に移動させます。
typescript// コンポーネントレベルの Signal
const TodoItem = () => {
const [completed, setCompleted] = createSignal(false);
return (
<div class={`todo-item ${completed() ? 'completed' : ''}`}>
{/* コンポーネント内容 */}
</div>
);
};
依存関係の明確化 Effect の依存関係を明確にし、予期しない更新を防ぎます。
typescript// 依存関係が明確な Effect
createEffect(() => {
const user = currentUser();
const settings = userSettings();
if (user && settings) {
updateUserPreferences(user.id, settings);
}
});
具体例
小規模アプリの構成例
Todo アプリを例に、小規模アプリケーションの構成を示します。
javatodo-app/
├── src/
│ ├── components/
│ │ ├── TodoItem.tsx
│ │ ├── TodoList.tsx
│ │ └── AddTodo.tsx
│ ├── stores/
│ │ └── todoStore.ts
│ ├── types/
│ │ └── todo.ts
│ ├── App.tsx
│ └── index.tsx
├── public/
└── package.json
Todo の型定義
typescript// types/todo.ts
export interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: Date;
}
Store の実装
typescript// stores/todoStore.ts
import { createSignal } from 'solid-js';
import type { Todo } from '../types/todo';
export const [todos, setTodos] = createSignal<Todo[]>([]);
export const addTodo = (text: string) => {
const newTodo: Todo = {
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date()
};
setTodos(prev => [...prev, newTodo]);
};
コンポーネントの実装
typescript// components/TodoItem.tsx
import { Todo } from '../types/todo';
interface TodoItemProps {
todo: Todo;
onToggle: (id: string) => void;
}
export const TodoItem = (props: TodoItemProps) => {
return (
<div class={`todo-item ${props.todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={props.todo.completed}
onChange={() => props.onToggle(props.todo.id)}
/>
<span class="todo-text">{props.todo.text}</span>
</div>
);
};
中規模〜大規模アプリの構成例
E コマースアプリケーションを例に、Feature-based アプローチの構成を示します。
cssecommerce-app/
├── src/
│ ├── features/
│ │ ├── auth/
│ │ │ ├── components/
│ │ │ │ ├── LoginForm.tsx
│ │ │ │ └── SignupForm.tsx
│ │ │ ├── stores/
│ │ │ │ └── authStore.ts
│ │ │ ├── services/
│ │ │ │ └── authApi.ts
│ │ │ └── types/
│ │ │ └── auth.ts
│ │ ├── products/
│ │ │ ├── components/
│ │ │ ├── stores/
│ │ │ ├── services/
│ │ │ └── types/
│ │ └── cart/
│ │ ├── components/
│ │ ├── stores/
│ │ ├── services/
│ │ └── types/
│ ├── shared/
│ │ ├── components/
│ │ │ ├── ui/
│ │ │ └── layout/
│ │ ├── utils/
│ │ ├── hooks/
│ │ └── types/
│ ├── pages/
│ └── App.tsx
└── package.json
認証機能の Store
typescript// features/auth/stores/authStore.ts
import { createSignal, createEffect } from 'solid-js';
import type { User } from '../types/auth';
import { authApi } from '../services/authApi';
export const [user, setUser] = createSignal<User | null>(null);
export const [isLoading, setIsLoading] = createSignal(false);
export const login = async (email: string, password: string) => {
setIsLoading(true);
try {
const userData = await authApi.login({ email, password });
setUser(userData);
localStorage.setItem('authToken', userData.token);
} catch (error) {
console.error('ログインエラー:', error);
throw error;
} finally {
setIsLoading(false);
}
};
共有 UI コンポーネント
typescript// shared/components/ui/Button.tsx
import { JSX, splitProps } from 'solid-js';
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
}
export const Button = (props: ButtonProps) => {
const [local, others] = splitProps(props, ['variant', 'size', 'class']);
const baseClass = 'btn';
const variantClass = `btn-${local.variant || 'primary'}`;
const sizeClass = `btn-${local.size || 'md'}`;
return (
<button
class={`${baseClass} ${variantClass} ${sizeClass} ${local.class || ''}`}
{...others}
/>
);
};
各ディレクトリの役割と実装例
components ディレクトリ 再利用可能な UI コンポーネントを配置します。
typescript// components/ui/Modal.tsx
import { JSX, Show, createEffect } from 'solid-js';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: JSX.Element;
}
export const Modal = (props: ModalProps) => {
let dialogRef: HTMLDialogElement;
createEffect(() => {
if (props.isOpen) {
dialogRef?.showModal();
} else {
dialogRef?.close();
}
});
return (
<dialog
ref={dialogRef!}
class="modal"
onClick={(e) => e.target === dialogRef && props.onClose()}
>
<div class="modal-content">
<Show when={props.title}>
<header class="modal-header">
<h2>{props.title}</h2>
<button class="modal-close" onClick={props.onClose}>
×
</button>
</header>
</Show>
<main class="modal-body">
{props.children}
</main>
</div>
</dialog>
);
};
stores ディレクトリ アプリケーションの状態管理を担当します。
typescript// stores/appStore.ts
import { createSignal, createContext, useContext, JSX } from 'solid-js';
interface AppState {
theme: 'light' | 'dark';
language: 'ja' | 'en';
isLoading: boolean;
}
const AppContext = createContext<{
state: () => AppState;
setTheme: (theme: AppState['theme']) => void;
setLanguage: (language: AppState['language']) => void;
setLoading: (loading: boolean) => void;
}>();
export const AppProvider = (props: { children: JSX.Element }) => {
const [state, setState] = createSignal<AppState>({
theme: 'light',
language: 'ja',
isLoading: false
});
const setTheme = (theme: AppState['theme']) => {
setState(prev => ({ ...prev, theme }));
};
const setLanguage = (language: AppState['language']) => {
setState(prev => ({ ...prev, language }));
};
const setLoading = (isLoading: boolean) => {
setState(prev => ({ ...prev, isLoading }));
};
return (
<AppContext.Provider value={{ state, setTheme, setLanguage, setLoading }}>
{props.children}
</AppContext.Provider>
);
};
export const useApp = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp は AppProvider 内で使用してください');
}
return context;
};
services ディレクトリ API 通信やビジネスロジックを配置します。
typescript// services/apiClient.ts
class ApiClient {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
async get<T>(endpoint: string): Promise<T> {
const response = await fetch(`${this.baseURL}${endpoint}`, {
headers: this.getHeaders()
});
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
}
async post<T>(endpoint: string, data: unknown): Promise<T> {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
}
private getHeaders() {
const token = localStorage.getItem('authToken');
return {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` })
};
}
}
export const apiClient = new ApiClient(import.meta.env.VITE_API_URL);
図で理解できる要点:
- 各ディレクトリが明確な責務を持ち、依存関係が整理されている
- 共有コンポーネントと機能固有コンポーネントが適切に分離されている
- 状態管理とビジネスロジックが適切に分離され、テスト可能な構成になっている
まとめ
SolidJS アプリケーションの適切なディレクトリ構成と設計原則について解説いたしました。
重要なポイントをまとめると以下の通りです。
SolidJS の特性を活かした構成
- Signal ベースのリアクティブシステムを考慮した状態管理の配置
- コンポーネントの適切な責務分離による保守性の向上
- パフォーマンスを意識した構成設計
実践的なアプローチ
- プロジェクト規模に応じた柔軟な構成選択
- Feature-based と Layer-based の使い分け
- チーム開発での統一性確保
継続的な改善
- プロジェクトの成長に合わせた構成の見直し
- 設計原則に基づく定期的なリファクタリング
- パフォーマンス測定による最適化
適切なディレクトリ構成は、開発効率とコード品質の向上に直結します。本記事でご紹介した手法を参考に、皆様のプロジェクトに最適な構成を見つけていただければと思います。
継続的な学習と改善により、より良い SolidJS アプリケーションを構築していきましょう。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来