コンポーネントパターン
コンポーネントの分類
Shared UI (shared/ui/)
- 再利用可能な汎用UIコンポーネント
- プロジェクト全体で使用される基本的なコンポーネント
- ビジネスロジックを含まない
Features (features/)
- ユーザー視点で意味のある機能単位
- 特定のユースケースに特化したコンポーネント
- ビジネスロジックを含む
Widgets (widgets/)
- ページ内で使用される大きなUIブロック
- 複数のfeaturesやentitiesを組み合わせた複合コンポーネント
- 自己完結型のUIセクション
コンポーネントの実装パターン
関数コンポーネント
- 常に関数コンポーネントを使用
- アロー関数または関数宣言を使用可能
// ✅ Good: アロー関数export const BlogPreview = (props: BlogPreviewProps) => { // ...};
// ✅ Good: 関数宣言export function BlogPreview(props: BlogPreviewProps) { // ...}Props分割代入
- Propsは分割代入で受け取る
- デフォルト値を設定可能
// ✅ Goodexport const BlogPreview = (props: BlogPreviewProps) => { const { className, date, heading = "h3", slug, image, title } = props; // ...};
// ✅ Good: 直接分割代入export const BlogPreview = ({ className, date, heading = "h3", slug, image, title }: BlogPreviewProps) => { // ...};条件付きレンダリング
- 早期リターンを使用
- 三項演算子はシンプルな場合のみ
// ✅ Good: 早期リターンexport const FooterMobile = () => { const { pathname } = useLocation(); const isResume = pathname.startsWith("/resume");
if (isResume) return null;
return ( <footer> {/* ... */} </footer> );};
// ✅ Good: 三項演算子(シンプルな場合)const status = isLoading ? "loading" : "ready";クラス名の結合
classnamesライブラリを使用- 条件付きクラス名を簡潔に記述
import classnames from "classnames";
// ✅ Good<div className={classnames("base-class", className, { "conditional-class": isActive })}> {/* ... */}</div>画像最適化
loading="lazy"を設定alt属性を必須で設定- サイズを明示的に指定
// ✅ Good<img alt={title} className="transition-transform duration-300 ease-in-out hover:scale-105" height="auto" loading="lazy" src={image} width="auto"/>リンクとナビゲーション
- Remixの
Linkコンポーネントを使用 prefetch="intent"でパフォーマンス最適化
import { Link } from "@remix-run/react";
// ✅ Good<Link className="blog-preview" prefetch="intent" to={`/blog/${slug}`}> {title}</Link>フックの使用
カスタムフック
- 再利用可能なロジックはカスタムフックに抽出
shared/hooks/に配置useプレフィックスを使用
// ✅ Goodexport const usePageTracking = () => { // ...};
export const useIntro = () => { // ...};Remixフック
useLoaderData: ローダーからのデータ取得useFetcher: フォーム送信やデータフェッチuseNavigation: ナビゲーション状態の取得useLocation: 現在のロケーション情報
// ✅ Goodconst { theme } = useLoaderData<{ theme: string }>();const fetcher = useFetcher();const { state } = useNavigation();const { pathname } = useLocation();エクスポート
名前付きエクスポート
- デフォルトエクスポートより名前付きエクスポートを推奨
index.tsで再エクスポート
// ✅ Good: 名前付きエクスポートexport const BlogPreview = () => { // ...};
// index.tsexport { BlogPreview } from "./ui/BlogPreview";export type { BlogPreviewProps } from "./ui/BlogPreview";パフォーマンス最適化
React.memo
- 不要な再レンダリングを防ぐ
- Propsが頻繁に変更されないコンポーネントに使用
// ✅ Good: メモ化が必要な場合export const BlogPreview = React.memo((props: BlogPreviewProps) => { // ...});useMemo / useCallback
- 計算コストが高い処理や関数をメモ化
- 依存配列を正確に指定
// ✅ Good: 計算コストが高い場合const expensiveValue = useMemo(() => { return computeExpensiveValue(data);}, [data]);
// ✅ Good: コールバック関数のメモ化const handleClick = useCallback(() => { // ...}, [dependency]);