SolidJS のコンポーネント設計ベストプラクティス

モダンなフロントエンド開発において、SolidJS は注目を集めている革新的なフレームワークです。React ライクな記法でありながら、実際の DOM を直接操作することで高いパフォーマンスを実現しています。しかし、その特徴を活かすためには、適切なコンポーネント設計が不可欠です。
この記事では、SolidJS を使った開発で実際に遭遇する課題や、効果的なコンポーネント設計の手法について、具体的なコード例とともに詳しく解説いたします。初心者の方でも理解しやすいよう、基礎から実践的な応用まで段階的にご紹介していきますね。
SolidJS コンポーネントの基礎知識
SolidJS の特徴と他フレームワークとの違い
SolidJS は、React の影響を受けながらも独自のアプローチでパフォーマンスを追求したフレームワークです。最大の特徴は、仮想 DOM を使用せずに直接 DOM を操作することで、高速な描画を実現している点です。
特徴 | SolidJS | React | Vue.js |
---|---|---|---|
DOM 操作 | 直接操作 | 仮想 DOM | 仮想 DOM |
再描画 | 細粒度 | コンポーネント単位 | コンポーネント単位 |
バンドルサイズ | 小さい | 中程度 | 中程度 |
学習コスト | 低い(React 経験者) | 中程度 | 中程度 |
シグナルベースの状態管理
SolidJS の核心となるのがシグナル(Signal)です。これは、値が変更されたときに依存する箇所だけを更新する仕組みです。
以下は、シグナルの基本的な使用例です:
typescriptimport { createSignal } from 'solid-js';
function Counter() {
// シグナルの作成 - [getter, setter]のペアを返す
const [count, setCount] = createSignal(0);
return (
<div>
<p>現在のカウント: {count()}</p>
<button onClick={() => setCount(count() + 1)}>
+1
</button>
</div>
);
}
このコードでは、count
シグナルが変更されるたびに、そのシグナルを参照している部分のみが更新されます。
よくある初心者エラー
SolidJS を始めたばかりの方によく見られるエラーをご紹介します:
エラー 1: シグナルを関数として呼び出し忘れ
typescript// ❌ 間違った使い方
function BadExample() {
const [count, setCount] = createSignal(0);
return <div>{count}</div>; // Error: count is not a function
}
// ✅ 正しい使い方
function GoodExample() {
const [count, setCount] = createSignal(0);
return <div>{count()}</div>; // count()として関数呼び出し
}
エラー 2: エフェクト内での依存関係の誤解
typescriptimport { createSignal, createEffect } from 'solid-js';
function EffectExample() {
const [count, setCount] = createSignal(0);
// ❌ エフェクト内でシグナルを使用しない
createEffect(() => {
console.log('This runs only once');
});
// ✅ エフェクト内でシグナルを使用する
createEffect(() => {
console.log('Count changed:', count());
});
return (
<button onClick={() => setCount(count() + 1)}>
Click
</button>
);
}
これらのエラーは、SolidJS の設計思想を理解することで避けることができます。シグナルは必ず関数として呼び出し、エフェクト内で使用することで自動的に依存関係が追跡されるのです。
基本的なコンポーネント設計パターン
関数コンポーネントの基本構造
SolidJS では、関数コンポーネントは一度だけ実行されます。これは React とは大きく異なる点で、パフォーマンスの向上に寄与しています。
基本的なコンポーネントの構造は以下のようになります:
typescriptimport {
createSignal,
createEffect,
onCleanup,
} from 'solid-js';
// 基本的なコンポーネント構造
function MyComponent() {
// 1. シグナルの定義
const [isVisible, setIsVisible] = createSignal(true);
// 2. エフェクトの定義
createEffect(() => {
console.log('可視状態が変更されました:', isVisible());
});
// 3. クリーンアップ処理
onCleanup(() => {
console.log('コンポーネントが破棄されました');
});
// 4. JSXの返却
return (
<div>
{isVisible() && <p>表示中です</p>}
<button onClick={() => setIsVisible(!isVisible())}>
切り替え
</button>
</div>
);
}
プロパティ(Props)の設計
SolidJS における Props は、コンポーネントの外部から渡される値です。TypeScript と組み合わせることで、型安全なコンポーネントを作成できます。
typescript// Props の型定義
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: () => void;
children: any;
}
// コンポーネントの実装
function Button(props: ButtonProps) {
return (
<button
class={`btn btn-${props.variant || 'primary'} btn-${
props.size || 'medium'
}`}
disabled={props.disabled}
onClick={props.onClick}
>
{props.children}
</button>
);
}
状態管理の基本
SolidJS では、状態管理にシグナルを使用します。複雑な状態の場合は、createStore
を使用してオブジェクトや配列を管理できます。
typescriptimport { createSignal, createStore } from 'solid-js';
function StateExample() {
// 単純な状態管理
const [count, setCount] = createSignal(0);
// 複雑な状態管理
const [user, setUser] = createStore({
name: '',
email: '',
preferences: {
theme: 'light',
notifications: true,
},
});
const updateUserName = (name: string) => {
setUser('name', name);
};
const updateTheme = (theme: 'light' | 'dark') => {
setUser('preferences', 'theme', theme);
};
return (
<div>
<p>カウント: {count()}</p>
<p>ユーザー名: {user.name}</p>
<p>テーマ: {user.preferences.theme}</p>
<button onClick={() => setCount(count() + 1)}>
カウントアップ
</button>
<input
type='text'
value={user.name}
onInput={(e) => updateUserName(e.target.value)}
placeholder='ユーザー名を入力'
/>
<button
onClick={() =>
updateTheme(
user.preferences.theme === 'light'
? 'dark'
: 'light'
)
}
>
テーマ切り替え
</button>
</div>
);
}
再利用可能なコンポーネント設計
コンポーネントの分割戦略
再利用可能なコンポーネントを設計するには、適切な分割戦略が重要です。以下の原則に従うことで、保守性の高いコードを作成できます:
分割の原則 | 説明 | 具体例 |
---|---|---|
単一責任 | 1 つのコンポーネントは 1 つの責任を持つ | Button コンポーネントはボタンの見た目と動作のみ |
疎結合 | 他のコンポーネントに依存しない | 外部 API の呼び出し処理を分離 |
高凝集 | 関連する機能をまとめる | フォーム関連の処理を 1 つのコンポーネントに |
実践的なコンポーネント分割の例:
typescript// ❌ 責任が多すぎる例
function TodoApp() {
const [todos, setTodos] = createStore([]);
const [filter, setFilter] = createSignal('all');
// API呼び出し
const fetchTodos = async () => {
// 複雑なAPI処理...
};
// フィルタリング
const filteredTodos = () => {
// 複雑なフィルタリング処理...
};
return <div>{/* 複雑なJSX構造 */}</div>;
}
typescript// ✅ 適切に分割された例
function TodoApp() {
const [todos, setTodos] = createStore([]);
const [filter, setFilter] = createSignal('all');
return (
<div>
<TodoFilter
filter={filter}
onFilterChange={setFilter}
/>
<TodoList todos={todos} filter={filter()} />
<TodoForm
onAddTodo={(todo) => setTodos([...todos, todo])}
/>
</div>
);
}
// 個別のコンポーネント
function TodoFilter(props: {
filter: Accessor<string>;
onFilterChange: Setter<string>;
}) {
return (
<div>
<button onClick={() => props.onFilterChange('all')}>
すべて
</button>
<button
onClick={() => props.onFilterChange('completed')}
>
完了済み
</button>
<button
onClick={() => props.onFilterChange('active')}
>
未完了
</button>
</div>
);
}
Props インターフェースの設計
型安全で使いやすい Props インターフェースを設計することで、コンポーネントの再利用性が向上します。
typescript// 基本的なProps設計
interface CardProps {
title: string;
description?: string;
image?: string;
actions?: Array<{
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
}>;
className?: string;
}
function Card(props: CardProps) {
return (
<div class={`card ${props.className || ''}`}>
{props.image && (
<img src={props.image} alt={props.title} />
)}
<div class='card-content'>
<h3 class='card-title'>{props.title}</h3>
{props.description && (
<p class='card-description'>
{props.description}
</p>
)}
{props.actions && (
<div class='card-actions'>
{props.actions.map((action, index) => (
<button
key={index}
class={`btn btn-${
action.variant || 'primary'
}`}
onClick={action.onClick}
>
{action.label}
</button>
))}
</div>
)}
</div>
</div>
);
}
デフォルト値の適切な設定
コンポーネントの使いやすさを向上させるため、適切なデフォルト値を設定することが重要です。
typescript// デフォルト値の設定方法
interface InputProps {
type?: 'text' | 'email' | 'password' | 'number';
placeholder?: string;
value?: string;
onInput?: (value: string) => void;
disabled?: boolean;
required?: boolean;
size?: 'small' | 'medium' | 'large';
}
function Input(props: InputProps) {
// デフォルト値の定義
const defaultProps = {
type: 'text',
placeholder: '',
disabled: false,
required: false,
size: 'medium',
};
// propsとデフォルト値のマージ
const mergedProps = { ...defaultProps, ...props };
return (
<input
type={mergedProps.type}
placeholder={mergedProps.placeholder}
value={mergedProps.value}
onInput={(e) => mergedProps.onInput?.(e.target.value)}
disabled={mergedProps.disabled}
required={mergedProps.required}
class={`input input-${mergedProps.size}`}
/>
);
}
より洗練されたアプローチとして、SolidJS のmergeProps
を使用することもできます:
typescriptimport { mergeProps } from 'solid-js';
function Input(props: InputProps) {
const merged = mergeProps(
{
type: 'text' as const,
placeholder: '',
disabled: false,
required: false,
size: 'medium' as const,
},
props
);
return (
<input
type={merged.type}
placeholder={merged.placeholder}
value={merged.value}
onInput={(e) => merged.onInput?.(e.target.value)}
disabled={merged.disabled}
required={merged.required}
class={`input input-${merged.size}`}
/>
);
}
パフォーマンス最適化のベストプラクティス
シグナルの効果的な使用
SolidJS のパフォーマンスを最大化するには、シグナルの適切な使用が重要です。以下のポイントを押さえましょう:
1. 細粒度な状態管理
typescript// ❌ 粗い粒度の状態管理
function BadExample() {
const [appState, setAppState] = createStore({
user: { name: 'John', email: 'john@example.com' },
ui: { theme: 'light', sidebarOpen: true },
data: { todos: [], loading: false },
});
// 一つの値の変更が全体に影響
const updateUserName = (name: string) => {
setAppState('user', 'name', name);
};
return <div>{appState.user.name}</div>;
}
// ✅ 細粒度な状態管理
function GoodExample() {
const [userName, setUserName] = createSignal('John');
const [theme, setTheme] = createSignal('light');
const [sidebarOpen, setSidebarOpen] = createSignal(true);
// 必要な部分のみ更新
return <div>{userName()}</div>;
}
2. 派生状態の活用
typescriptimport { createMemo } from 'solid-js';
function UserProfile() {
const [user, setUser] = createStore({
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
});
// 派生状態をメモ化
const fullName = createMemo(
() => `${user.firstName} ${user.lastName}`
);
const emailDomain = createMemo(
() => user.email.split('@')[1]
);
return (
<div>
<h1>{fullName()}</h1>
<p>Domain: {emailDomain()}</p>
<input
value={user.firstName}
onInput={(e) =>
setUser('firstName', e.target.value)
}
/>
</div>
);
}
メモ化の活用
createMemo
を使用して、計算コストの高い処理をメモ化することで、パフォーマンスを向上させることができます。
typescript// 重い計算処理の例
function ExpensiveCalculation() {
const [numbers, setNumbers] = createSignal([
1, 2, 3, 4, 5,
]);
const [multiplier, setMultiplier] = createSignal(1);
// ❌ 毎回計算される
const badSum = () => {
console.log('Heavy calculation running...');
return numbers().reduce(
(sum, num) => sum + num * multiplier(),
0
);
};
// ✅ 依存関係が変わったときのみ計算
const memoizedSum = createMemo(() => {
console.log('Memoized calculation running...');
return numbers().reduce(
(sum, num) => sum + num * multiplier(),
0
);
});
return (
<div>
<p>Bad sum: {badSum()}</p>
<p>Memoized sum: {memoizedSum()}</p>
<button
onClick={() => setMultiplier(multiplier() + 1)}
>
Increase multiplier
</button>
</div>
);
}
不要な再レンダリングの防止
1. 条件付きレンダリングの最適化
typescriptimport { Show, Switch, Match } from 'solid-js';
function ConditionalRendering() {
const [status, setStatus] = createSignal<
'loading' | 'success' | 'error'
>('loading');
// ❌ 毎回JSXが評価される
const badCondition = () => {
if (status() === 'loading')
return <div>Loading...</div>;
if (status() === 'success') return <div>Success!</div>;
return <div>Error!</div>;
};
return (
<div>
{/* ❌ 非効率 */}
{badCondition()}
{/* ✅ 効率的 */}
<Switch>
<Match when={status() === 'loading'}>
<div>Loading...</div>
</Match>
<Match when={status() === 'success'}>
<div>Success!</div>
</Match>
<Match when={status() === 'error'}>
<div>Error!</div>
</Match>
</Switch>
</div>
);
}
2. リストレンダリングの最適化
typescriptimport { For, Index } from 'solid-js';
function OptimizedList() {
const [items, setItems] = createSignal([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
]);
return (
<div>
{/* ❌ 配列の順序が変わると全て再レンダリング */}
<For each={items()}>
{(item) => <div>{item.name}</div>}
</For>
{/* ✅ インデックスベースで最適化 */}
<Index each={items()}>
{(item, index) => <div>{item().name}</div>}
</Index>
</div>
);
}
3. エラーハンドリングの最適化
typescriptimport { ErrorBoundary } from 'solid-js';
function ErrorHandling() {
const [count, setCount] = createSignal(0);
// エラーが発生する可能性のあるコンポーネント
const ProblematicComponent = () => {
if (count() > 5) {
throw new Error(`Count too high: ${count()}`);
}
return <div>Count: {count()}</div>;
};
return (
<div>
<ErrorBoundary
fallback={(err) => <div>Error: {err.message}</div>}
>
<ProblematicComponent />
</ErrorBoundary>
<button onClick={() => setCount(count() + 1)}>
Increment
</button>
</div>
);
}
このように、SolidJS の特性を理解し、適切な最適化手法を適用することで、高パフォーマンスなアプリケーションを構築できます。次のセクションでは、実践的なコンポーネント例を通じて、これらのベストプラクティスを具体的に見ていきましょう。
実践的なコンポーネント例
Button コンポーネント
再利用可能で拡張性の高い Button コンポーネントを作成してみましょう。このコンポーネントは、様々な場面で使用できるよう設計されています。
まず、Button コンポーネントの基本的な型定義から始めます:
typescriptimport { JSX, mergeProps, splitProps } from 'solid-js';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger' | 'outline';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
leftIcon?: JSX.Element;
rightIcon?: JSX.Element;
onClick?: (event: MouseEvent) => void;
children: JSX.Element;
class?: string;
type?: 'button' | 'submit' | 'reset';
}
次に、実際の Button コンポーネントの実装です:
typescriptfunction Button(props: ButtonProps) {
// デフォルト値の設定
const merged = mergeProps(
{
variant: 'primary' as const,
size: 'medium' as const,
disabled: false,
loading: false,
type: 'button' as const,
},
props
);
// propsの分割(HTML属性とカスタムpropsを分離)
const [local, others] = splitProps(merged, [
'variant',
'size',
'disabled',
'loading',
'leftIcon',
'rightIcon',
'onClick',
'children',
'class',
]);
// CSSクラスの動的生成
const buttonClass = () => {
const baseClass = 'btn';
const variantClass = `btn-${local.variant}`;
const sizeClass = `btn-${local.size}`;
const disabledClass =
local.disabled || local.loading ? 'btn-disabled' : '';
const loadingClass = local.loading ? 'btn-loading' : '';
const customClass = local.class || '';
return [
baseClass,
variantClass,
sizeClass,
disabledClass,
loadingClass,
customClass,
]
.filter(Boolean)
.join(' ');
};
// クリックハンドラー
const handleClick = (event: MouseEvent) => {
if (local.disabled || local.loading) {
event.preventDefault();
return;
}
local.onClick?.(event);
};
return (
<button
{...others}
class={buttonClass()}
disabled={local.disabled || local.loading}
onClick={handleClick}
>
{local.leftIcon && (
<span class='btn-icon-left'>{local.leftIcon}</span>
)}
{local.loading && <span class='btn-spinner'>⟳</span>}
<span class='btn-text'>{local.children}</span>
{local.rightIcon && (
<span class='btn-icon-right'>
{local.rightIcon}
</span>
)}
</button>
);
}
使用例とよくあるエラーの対処法:
typescript// ✅ 基本的な使用
function ButtonExample() {
const [isLoading, setIsLoading] = createSignal(false);
const handleSubmit = async () => {
setIsLoading(true);
try {
await new Promise((resolve) =>
setTimeout(resolve, 2000)
);
console.log('送信完了');
} catch (error) {
console.error('送信エラー:', error);
} finally {
setIsLoading(false);
}
};
return (
<div>
<Button
variant='primary'
size='medium'
loading={isLoading()}
onClick={handleSubmit}
>
送信
</Button>
</div>
);
}
Form コンポーネント
フォームの状態管理とバリデーションを統合した Form コンポーネントを作成します。このコンポーネントは、複雑なフォーム処理を簡潔に記述できるよう設計されています。
まず、フォームのバリデーション機能を実装します:
typescriptimport {
createSignal,
createStore,
createMemo,
} from 'solid-js';
interface FormField {
value: string;
error?: string;
touched: boolean;
}
interface ValidationRule {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
custom?: (value: string) => string | undefined;
}
interface FormConfig {
[key: string]: ValidationRule;
}
function createFormStore(config: FormConfig) {
const [fields, setFields] = createStore<
Record<string, FormField>
>({});
// フィールドの初期化
const initializeField = (
name: string,
initialValue = ''
) => {
if (!fields[name]) {
setFields(name, {
value: initialValue,
error: undefined,
touched: false,
});
}
};
// バリデーション関数
const validateField = (
name: string,
value: string
): string | undefined => {
const rules = config[name];
if (!rules) return undefined;
if (rules.required && !value.trim()) {
return 'この項目は必須です';
}
if (rules.minLength && value.length < rules.minLength) {
return `最低${rules.minLength}文字以上入力してください`;
}
if (rules.maxLength && value.length > rules.maxLength) {
return `最大${rules.maxLength}文字以下で入力してください`;
}
if (rules.pattern && !rules.pattern.test(value)) {
return '形式が正しくありません';
}
if (rules.custom) {
return rules.custom(value);
}
return undefined;
};
return {
fields,
setFields,
initializeField,
validateField,
};
}
次に、実際の Form コンポーネントの実装です:
typescriptinterface FormProps {
onSubmit: (
data: Record<string, string>
) => void | Promise<void>;
validationConfig: FormConfig;
children: any;
class?: string;
}
function Form(props: FormProps) {
const {
fields,
setFields,
initializeField,
validateField,
} = createFormStore(props.validationConfig);
const [isSubmitting, setIsSubmitting] =
createSignal(false);
// フォーム全体の妥当性を計算
const isFormValid = createMemo(() => {
const fieldNames = Object.keys(props.validationConfig);
return fieldNames.every((name) => {
const field = fields[name];
return field && !field.error && field.value.trim();
});
});
// フィールド値の更新
const updateField = (name: string, value: string) => {
initializeField(name);
const error = validateField(name, value);
setFields(name, { value, error, touched: true });
};
// フォーム送信処理
const handleSubmit = async (event: Event) => {
event.preventDefault();
if (!isFormValid()) {
console.error('Form validation failed');
return;
}
setIsSubmitting(true);
try {
const formData = Object.keys(fields).reduce(
(acc, key) => {
acc[key] = fields[key].value;
return acc;
},
{} as Record<string, string>
);
await props.onSubmit(formData);
} catch (error) {
console.error('Form submission failed:', error);
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} class={props.class}>
{props.children({
fields,
updateField,
isSubmitting: isSubmitting(),
isValid: isFormValid(),
})}
</form>
);
}
FormInput コンポーネントの実装:
typescriptinterface FormInputProps {
name: string;
type?: 'text' | 'email' | 'password' | 'tel';
placeholder?: string;
label?: string;
field?: FormField;
onInput?: (name: string, value: string) => void;
}
function FormInput(props: FormInputProps) {
const merged = mergeProps(
{
type: 'text' as const,
placeholder: '',
},
props
);
return (
<div class='form-group'>
{merged.label && (
<label class='form-label' for={merged.name}>
{merged.label}
</label>
)}
<input
id={merged.name}
name={merged.name}
type={merged.type}
placeholder={merged.placeholder}
value={merged.field?.value || ''}
onInput={(e) =>
merged.onInput?.(merged.name, e.target.value)
}
class={`form-input ${
merged.field?.error ? 'form-input-error' : ''
}`}
/>
{merged.field?.error && merged.field.touched && (
<span class='form-error'>{merged.field.error}</span>
)}
</div>
);
}
List コンポーネント
動的なリストを効率的に表示する List コンポーネントを作成します。このコンポーネントは、大量のデータを扱う際のパフォーマンスを考慮した設計になっています。
まず、基本的な List コンポーネントの実装:
typescriptimport {
For,
Index,
createSignal,
createMemo,
} from 'solid-js';
interface ListItem {
id: string | number;
[key: string]: any;
}
interface ListProps<T extends ListItem> {
items: T[];
renderItem: (item: T, index: number) => JSX.Element;
keyField?: keyof T;
emptyMessage?: string;
loading?: boolean;
onItemClick?: (item: T, index: number) => void;
class?: string;
}
function List<T extends ListItem>(props: ListProps<T>) {
const merged = mergeProps(
{
keyField: 'id' as keyof T,
emptyMessage: '項目がありません',
loading: false,
},
props
);
// アイテムが空の場合の表示
const isEmpty = createMemo(
() => merged.items.length === 0
);
// ローディング状態の表示
if (merged.loading) {
return (
<div class={`list ${merged.class || ''}`}>
<div class='list-loading'>読み込み中...</div>
</div>
);
}
// 空の場合の表示
if (isEmpty()) {
return (
<div class={`list ${merged.class || ''}`}>
<div class='list-empty'>{merged.emptyMessage}</div>
</div>
);
}
return (
<div class={`list ${merged.class || ''}`}>
<For each={merged.items}>
{(item, index) => (
<div
class='list-item'
onClick={() =>
merged.onItemClick?.(item, index())
}
data-id={item[merged.keyField]}
>
{merged.renderItem(item, index())}
</div>
)}
</For>
</div>
);
}
仮想化された List コンポーネント(大量データ対応):
typescriptinterface VirtualizedListProps<T extends ListItem>
extends ListProps<T> {
itemHeight: number;
containerHeight: number;
overscan?: number;
}
function VirtualizedList<T extends ListItem>(
props: VirtualizedListProps<T>
) {
const merged = mergeProps(
{
overscan: 5,
},
props
);
const [scrollTop, setScrollTop] = createSignal(0);
// 表示範囲の計算
const visibleRange = createMemo(() => {
const start = Math.floor(
scrollTop() / merged.itemHeight
);
const end = Math.min(
start +
Math.ceil(
merged.containerHeight / merged.itemHeight
) +
merged.overscan,
merged.items.length
);
return {
start: Math.max(0, start - merged.overscan),
end,
};
});
// 表示するアイテム
const visibleItems = createMemo(() => {
const range = visibleRange();
return merged.items.slice(range.start, range.end);
});
// スクロールハンドラー
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement;
setScrollTop(target.scrollTop);
};
return (
<div
class={`virtualized-list ${merged.class || ''}`}
style={{
height: `${merged.containerHeight}px`,
overflow: 'auto',
}}
onScroll={handleScroll}
>
<div
style={{
height: `${
merged.items.length * merged.itemHeight
}px`,
position: 'relative',
}}
>
<For each={visibleItems()}>
{(item, index) => {
const actualIndex =
visibleRange().start + index();
return (
<div
class='virtualized-list-item'
style={{
position: 'absolute',
top: `${
actualIndex * merged.itemHeight
}px`,
height: `${merged.itemHeight}px`,
width: '100%',
}}
onClick={() =>
merged.onItemClick?.(item, actualIndex)
}
>
{merged.renderItem(item, actualIndex)}
</div>
);
}}
</For>
</div>
</div>
);
}
実際の使用例:
typescriptfunction ListExample() {
const [users, setUsers] = createStore([
{
id: 1,
name: '田中太郎',
email: 'tanaka@example.com',
},
{ id: 2, name: '佐藤花子', email: 'sato@example.com' },
{
id: 3,
name: '鈴木一郎',
email: 'suzuki@example.com',
},
]);
const [isLoading, setIsLoading] = createSignal(false);
const handleUserClick = (
user: (typeof users)[0],
index: number
) => {
console.log(
'ユーザーがクリックされました:',
user,
'インデックス:',
index
);
};
return (
<div>
<List
items={users}
loading={isLoading()}
onItemClick={handleUserClick}
renderItem={(user, index) => (
<div class='user-card'>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)}
/>
</div>
);
}
これらの実践的なコンポーネント例では、SolidJS の特徴を活かしながら、再利用性とパフォーマンスを両立させる設計手法を示しています。各コンポーネントは型安全性を保ちながら、柔軟な拡張が可能な構造になっているのです。
まとめ
SolidJS のコンポーネント設計において、最も重要なのはシグナルベースの状態管理を理解し、効果的に活用することです。今回ご紹介した内容を振り返ってみましょう。
学習のポイント
項目 | 重要度 | 主なメリット |
---|---|---|
シグナルの適切な使用 | ★★★★★ | 細粒度な更新によるパフォーマンス向上 |
型安全な Props 設計 | ★★★★☆ | 開発時のエラー防止と保守性向上 |
コンポーネント分割戦略 | ★★★★☆ | 再利用性と可読性の向上 |
メモ化の活用 | ★★★☆☆ | 重い計算処理の最適化 |
実践で意識すべき点
SolidJS でのコンポーネント設計では、以下の点を常に意識することが重要です:
1. シグナルを関数として呼び出す
React とは異なり、SolidJS ではシグナルは必ず関数として呼び出す必要があります。これを忘れるとTypeError: count is not a function
のようなエラーが発生してしまいます。
2. 適切な粒度での状態管理 大きなオブジェクトを一つのストアで管理するよりも、関連する状態をシグナルで細かく分割することで、より効率的な更新が可能になります。
3. TypeScript との組み合わせ 型安全性を活かすことで、開発時のエラーを大幅に減らすことができます。特に Props の型定義は、チーム開発での重要な指針となります。
今後の学習方向
SolidJS のコンポーネント設計をさらに深く理解するために、以下の分野も学習されることをお勧めします:
- カスタムフックの作成: ロジックの再利用性を高める
- Context の活用: グローバルな状態管理
- Suspense との組み合わせ: 非同期処理の効率化
- テスト戦略: 品質の高いコンポーネントの作成
SolidJS は、まだ新しいフレームワークですが、その設計思想は非常に洗練されています。適切なコンポーネント設計を身につけることで、高性能で保守性の高いアプリケーションを構築できるようになるでしょう。
皆さんの開発がより効率的で楽しいものになることを願っています!
関連リンク
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
- review
人類はなぜ地球を支配できた?『サピエンス全史 上巻』ユヴァル・ノア・ハラリが解き明かす驚愕の真実