mcpField
R
react-nextjs-patterns

react-nextjs-patterns

github.com/duyet開発151.1k 回インストール1 週間前に更新
リポジトリ

react-nextjs-patterns について

react-nextjs-patternsは、保守しやすく高性能なReactとNext.jsのフロントエンドを構築するためのパターン集です。コンポーネント選択、データ取得、状態管理、性能、アクセシビリティを扱います。

主な機能

  1. 01サーバー/クライアントコンポーネントの指針
  2. 02Suspenseと並列データ取得
  3. 03コンポーネント分割とメモ化
  4. 04Context、Reducer、カスタムフック
  5. 05アクセシビリティとローディング状態の確認項目

活用シーン

  1. 01ページでサーバー/クライアントコンポーネントを選ぶ
  2. 02Next.jsで並列データ取得を構成する
  3. 03コンポーネントと状態管理を整理する
  4. 04フロントエンドの性能とアクセシビリティを確認する

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/duyet/codex-claude-plugins --skill react-nextjs-patterns

インストール後、Claude Code で/react-nextjs-patternsを入力して読み込まれたか確認します。

詳細

このスキルでは、パフォーマンスと保守性に優れたフロントエンドアプリケーションを構築するための、React および Next.js 固有のパターンを紹介します。

このスキルを呼び出すタイミング

次の場合に自動的に有効化されます:

  • React コンポーネントの実装
  • Next.js のページおよび API ルート
  • 状態管理のパターン
  • パフォーマンスの最適化
  • サーバーコンポーネントとクライアントコンポーネントの選択

Next.js App Router のパターン

サーバーコンポーネントとクライアントコンポーネント
// Server Component (default) - data fetching, no interactivity
// app/users/page.tsx
export default async function UsersPage() {
  const users = await getUsers(); // Runs on server

  return (
    <div>
      <h1>Users</h1>
      <UserList users={users} />
    </div>
  );
}

// Client Component - interactivity required
// components/user-search.tsx
'use client';

import { useState } from 'react';

export function UserSearch({ onSearch }: { onSearch: (q: string) => void }) {
  const [query, setQuery] = useState('');

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onKeyDown={(e) => e.key === 'Enter' && onSearch(query)}
    />
  );
}
Suspense によるストリーミング
// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* Fast data loads first */}
      <Suspense fallback={<StatsSkeleton />}>
        <StatsSection />
      </Suspense>

      {/* Slow data streams in */}
      <Suspense fallback={<ChartSkeleton />}>
        <AnalyticsChart />
      </Suspense>
    </div>
  );
}

// Async component that streams
async function StatsSection() {
  const stats = await getStats(); // Can be slow
  return <Stats data={stats} />;
}
データ取得のパターン
// Parallel data fetching
async function DashboardPage() {
  // Fetch in parallel, not sequentially
  const [users, orders, stats] = await Promise.all([
    getUsers(),
    getOrders(),
    getStats(),
  ]);

  return <Dashboard users={users} orders={orders} stats={stats} />;
}

// With error boundary
import { notFound } from 'next/navigation';

async function UserPage({ params }: { params: { id: string } }) {
  const user = await getUser(params.id);

  if (!user) {
    notFound(); // Renders not-found.tsx
  }

  return <UserProfile user={user} />;
}

React のパフォーマンスパターン

コンポーネントの分割
// BAD: Large component with all state
function BadUserList() {
  const [filter, setFilter] = useState('');
  const [users, setUsers] = useState<User[]>([]);
  const [selectedId, setSelectedId] = useState<string | null>(null);

  // All users re-render on any state change
  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {users.map(user => (
        <div
          key={user.id}
          onClick={() => setSelectedId(user.id)}
          className={selectedId === user.id ? 'selected' : ''}
        >
          {user.name}
        </div>
      ))}
    </div>
  );
}

// GOOD: Push state down to where it's needed
function GoodUserList() {
  const [users] = useState<User[]>([]);
  return <FilterableUserList users={users} />;
}

function FilterableUserList({ users }: { users: User[] }) {
  const [filter, setFilter] = useState('');
  const filtered = useMemo(
    () => users.filter(u => u.name.includes(filter)),
    [users, filter]
  );

  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      <SelectableList users={filtered} />
    </div>
  );
}

function SelectableList({ users }: { users: User[] }) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  return users.map(user => (
    <UserItem
      key={user.id}
      user={user}
      selected={selectedId === user.id}
      onSelect={() => setSelectedId(user.id)}
    />
  ));
}
メモ化の戦略
// Only memo when there's a measurable benefit
const UserItem = memo(function UserItem({
  user,
  selected,
  onSelect
}: {
  user: User;
  selected: boolean;
  onSelect: () => void;
}) {
  return (
    <div
      onClick={onSelect}
      className={selected ? 'selected' : ''}
    >
      {user.name}
    </div>
  );
});

// useMemo for expensive computations
function ExpensiveList({ items }: { items: Item[] }) {
  const processed = useMemo(() => {
    return items
      .filter(complexFilter)
      .sort(complexSort)
      .map(complexTransform);
  }, [items]);

  return <List items={processed} />;
}

// useCallback for stable references passed to children
function Parent() {
  const [items, setItems] = useState<Item[]>([]);

  const handleDelete = useCallback((id: string) => {
    setItems(prev => prev.filter(item => item.id !== id));
  }, []);

  return <ItemList items={items} onDelete={handleDelete} />;
}

状態管理のパターン

Reducer を使用した Context
// types
interface State {
  user: User | null;
  isLoading: boolean;
  error: Error | null;
}

type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; user: User }
  | { type: 'FETCH_ERROR'; error: Error }
  | { type: 'LOGOUT' };

// reducer
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, isLoading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, isLoading: false, user: action.user };
    case 'FETCH_ERROR':
      return { ...state, isLoading: false, error: action.error };
    case 'LOGOUT':
      return { ...state, user: null };
  }
}

// context
const AuthContext = createContext<{
  state: State;
  dispatch: Dispatch<Action>;
} | null>(null);

// provider
function AuthProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(reducer, {
    user: null,
    isLoading: true,
    error: null,
  });

  return (
    <AuthContext.Provider value={{ state, dispatch }}>
      {children}
    </AuthContext.Provider>
  );
}

// hook
function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}
カスタムフック
// Data fetching hook
function useQuery<T>(
  key: string,
  fetcher: () => Promise<T>
): { data: T | null; isLoading: boolean; error: Error | null; refetch: () => void } {
  const [data, setData] = useState<T | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const fetchData = useCallback(async () => {
    setIsLoading(true);
    setError(null);
    try {
      const result = await fetcher();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Unknown error'));
    } finally {
      setIsLoading(false);
    }
  }, [fetcher]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, isLoading, error, refetch: fetchData };
}

// Debounced value hook
function useDebouncedValue<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

// Local storage hook
function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initialValue;
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback((value: T | ((prev: T) => T)) => {
    setStoredValue(prev => {
      const newValue = value instanceof Function ? value(prev) : value;
      localStorage.setItem(key, JSON.stringify(newValue));
      return newValue;
    });
  }, [key]);

  return [storedValue, setValue];
}

コンポーネントのパターン

複合コンポーネント
// Flexible API with compound components
const Tabs = ({ children, defaultValue }: { children: ReactNode; defaultValue: string }) => {
  const [activeTab, setActiveTab] = useState(defaultValue);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
};

Tabs.List = function TabsList({ children }: { children: ReactNode }) {
  return <div className="tabs-list">{children}</div>;
};

Tabs.Tab = function Tab({ value, children }: { value: string; children: ReactNode }) {
  const { activeTab, setActiveTab } = useTabsContext();
  return (
    <button
      className={activeTab === value ? 'active' : ''}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  );
};

Tabs.Panel = function TabsPanel({ value, children }: { value: string; children: ReactNode }) {
  const { activeTab } = useTabsContext();
  if (activeTab !== value) return null;
  return <div className="tabs-panel">{children}</div>;
};

// Usage
<Tabs defaultValue="tab1">
  <Tabs.List>
    <Tabs.Tab value="tab1">Tab 1</Tabs.Tab>
    <Tabs.Tab value="tab2">Tab 2</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="tab1">Content 1</Tabs.Panel>
  <Tabs.Panel value="tab2">Content 2</Tabs.Panel>
</Tabs>
Render Props と関数としての Children
// Data provider with render prop
function DataProvider<T>({
  fetcher,
  children,
}: {
  fetcher: () => Promise<T>;
  children: (data: T | null, isLoading: boolean) => ReactNode;
}) {
  const { data, isLoading } = useQuery('data', fetcher);
  return <>{children(data, isLoading)}</>;
}

// Usage
<DataProvider fetcher={getUsers}>
  {(users, isLoading) => (
    isLoading ? <Spinner /> : <UserList users={users} />
  )}
</DataProvider>

ベストプラクティスのチェックリスト

  • デフォルトではサーバーコンポーネントを使用し、必要な場合にのみクライアントコンポーネントを使用する
  • 状態は、それを必要とする最も下位のコンポーネントに持たせる
  • 大きなコンポーネントを、より小さく役割を絞ったコンポーネントに分割する
  • 非同期処理には Suspense 境界を使用する
  • プロファイリングで効果が確認された場合にのみメモ化する
  • 再利用可能な状態付きロジックにはカスタムフックを作成する
  • コンポーネントの状態には判別可能なユニオン型を使用する
  • 適切なエラー境界を実装する
  • アクセシビリティ(ARIA、キーボード操作)を確保する
  • 適切な読み込み状態とエラー状態を使用する

コメント