| name | react-component |
| description | Reactコンポーネントの設計と実装パターン。
関数コンポーネント、カスタムフック、状態管理、アクセシビリティ。
Reactコンポーネント作成、フロントエンド実装時に使用。
|
| version | 1.0.0 |
React Component Skill
Overview
保守性の高いReactコンポーネント設計パターンを提供します。
Component Structure
src/
├── components/
│ ├── ui/ # 汎用UIコンポーネント
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.test.tsx
│ │ │ └── index.ts
│ │ └── Input/
│ ├── features/ # 機能別コンポーネント
│ │ └── auth/
│ │ ├── LoginForm.tsx
│ │ └── hooks/
│ └── layouts/ # レイアウト
├── hooks/ # 共通カスタムフック
└── types/ # 型定義
Component Template
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
isLoading = false,
disabled,
className,
children,
...props
},
ref
) => {
return (
<button
ref={ref}
disabled={disabled || isLoading}
className={cn(
// ベーススタイル
'inline-flex items-center justify-center rounded-md font-medium',
'transition-colors focus-visible:outline-none focus-visible:ring-2',
// バリアント
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'bg-red-600 text-white hover:bg-red-700': variant === 'danger',
},
// サイズ
{
'h-8 px-3 text-sm': size === 'sm',
'h-10 px-4 text-base': size === 'md',
'h-12 px-6 text-lg': size === 'lg',
},
// 状態
'disabled:opacity-50 disabled:cursor-not-allowed',
className
)}
{...props}
>
{isLoading && <Spinner className="mr-2" />}
{children}
</button>
);
}
);
Button.displayName = 'Button';
Custom Hook Pattern
import { useState, useCallback } from 'react';
interface UseAsyncOptions<T> {
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
}
interface UseAsyncState<T> {
data: T | null;
error: Error | null;
isLoading: boolean;
}
export function useAsync<T, Args extends unknown[]>(
asyncFn: (...args: Args) => Promise<T>,
options: UseAsyncOptions<T> = {}
) {
const [state, setState] = useState<UseAsyncState<T>>({
data: null,
error: null,
isLoading: false,
});
const execute = useCallback(
async (...args: Args) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const data = await asyncFn(...args);
setState({ data, error: null, isLoading: false });
options.onSuccess?.(data);
return data;
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
setState({ data: null, error: err, isLoading: false });
options.onError?.(err);
throw err;
}
},
[asyncFn, options]
);
return { ...state, execute };
}
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error, execute: fetchUser } = useAsync(
() => api.getUser(userId),
{ onError: (e) => toast.error(e.message) }
);
useEffect(() => {
fetchUser();
}, [fetchUser]);
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
if (!user) return null;
return <div>{user.name}</div>;
}
Accessibility Checklist
<Dialog
open={isOpen}
onClose={onClose}
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<DialogTitle id="dialog-title">確認</DialogTitle>
<DialogDescription id="dialog-description">
この操作は取り消せません。
</DialogDescription>
<DialogActions>
<Button onClick={onClose}>キャンセル</Button>
<Button onClick={onConfirm} variant="danger">削除</Button>
</DialogActions>
</Dialog>
Performance Patterns
const MemoizedComponent = memo(ExpensiveComponent);
import { useVirtualizer } from '@tanstack/react-virtual';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const [formData, setFormData] = useGlobalState();
function Form() {
const [formData, setFormData] = useState();
return <FormFields data={formData} onChange={setFormData} />;
}