mcpField
R
react-patterns

react-patterns

github.com/rohitg00開発2.8k212 回インストール25 週間前に更新
リポジトリ

react-patterns について

React開発者向けのパターン集です。React 19、Server Components、Server Actions、コンポーネント構成、パフォーマンスに関する実践を扱います。

主な機能

  1. 01React 19のuse()と状態フック
  2. 02Server ComponentsとServer Actions
  3. 03Suspenseとエラーバウンダリ
  4. 04カスタムフックと複合コンポーネント
  5. 05パフォーマンスと状態整理の指針

活用シーン

  1. 01非同期データを読むUIの実装
  2. 02サーバー側フォーム操作の構築
  3. 03データ読み込み境界の分割
  4. 04コンポーネントロジックの再利用と状態整理

インストール

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

$npx skills add https://github.com/rohitg00/awesome-claude-code-toolkit --skill react-patterns

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

詳細

React パターン

use() フック(React 19)

use() は、レンダー中に Promise や Context の値を直接読み取ります。他のフックとは異なり、条件分岐やループ内でも呼び出せます。

import { use } from 'react';

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

function ThemeButton() {
  const theme = use(ThemeContext);
  return <button style={{ background: theme.primary }}>Click</button>;
}

Promise とともに use() を使用するコンポーネントは、<Suspense> 境界でラップしてください。

サーバーコンポーネント

// app/users/page.tsx - Server Component (default, no directive needed)
import { UserList } from './UserList';

export default async function UsersPage() {
  const users = await fetch('https://api.example.com/users', {
    next: { revalidate: 60 },
  }).then(r => r.json());

  return <UserList users={users} />;
}

// app/users/UserList.tsx - Still a Server Component
export function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map(u => (
        <li key={u.id}>
          {u.name}
          <DeleteButton userId={u.id} />
        </li>
      ))}
    </ul>
  );
}

'use client' はできるだけ深い階層に置いてください。インタラクティブ性が必要な末端のコンポーネントだけをクライアントコンポーネントにします。

サーバーアクション

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const body = formData.get('body') as string;

  await db.insert(posts).values({ title, body });

  revalidatePath('/posts');
  redirect('/posts');
}
// app/posts/new/page.tsx
import { createPost } from '../actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="body" required />
      <button type="submit">Create</button>
    </form>
  );
}

useActionState(React 19)

'use client';

import { useActionState } from 'react';
import { createUser } from './actions';

function SignupForm() {
  const [state, formAction, isPending] = useActionState(createUser, {
    errors: {},
    message: '',
  });

  return (
    <form action={formAction}>
      <input name="email" />
      {state.errors.email && <p>{state.errors.email}</p>}
      <button disabled={isPending}>
        {isPending ? 'Creating...' : 'Sign Up'}
      </button>
      {state.message && <p>{state.message}</p>}
    </form>
  );
}

useOptimistic(React 19)

'use client';

import { useOptimistic } from 'react';
import { likePost } from './actions';

function LikeButton({ count, postId }: { count: number; postId: string }) {
  const [optimisticCount, addOptimistic] = useOptimistic(count);

  async function handleLike() {
    addOptimistic(prev => prev + 1);
    await likePost(postId);
  }

  return (
    <form action={handleLike}>
      <button type="submit">{optimisticCount} Likes</button>
    </form>
  );
}

Suspense 境界

import { Suspense } from 'react';

function Dashboard() {
  return (
    <div>
      <Suspense fallback={<StatsSkeleton />}>
        <StatsPanel />
      </Suspense>
      <div className="grid grid-cols-2">
        <Suspense fallback={<ChartSkeleton />}>
          <RevenueChart />
        </Suspense>
        <Suspense fallback={<ListSkeleton />}>
          <RecentActivity />
        </Suspense>
      </div>
    </div>
  );
}

独立したデータ取得単位を Suspense 境界で囲んでください。ページ全体を単一の境界で囲むのは避けてください(ストリーミングの利点が失われます)。

エラーバウンダリー

'use client';

import { Component, type ReactNode } from 'react';

class ErrorBoundary extends Component<
  { fallback: ReactNode; children: ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    reportError(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

または、ルートレベルのエラー処理には Next.js の error.tsx 規約を使用してください。

カスタムフック

function useDebounce<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;
}

function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initial;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

カスタムフックのルール:

  • use を先頭に付ける
  • 2 つ以上のコンポーネントでロジックを共有する場合に切り出す
  • フックは単一の関心事に集中させる
  • タプル [value, setter] またはオブジェクト { data, error, loading } を返す

複合コンポーネント

function Tabs({ children }: { children: ReactNode }) {
  const [active, setActive] = useState(0);
  return (
    <TabsContext value={{ active, setActive }}>
      <div role="tablist">{children}</div>
    </TabsContext>
  );
}

Tabs.Tab = function Tab({ index, children }: { index: number; children: ReactNode }) {
  const { active, setActive } = use(TabsContext);
  return (
    <button
      role="tab"
      aria-selected={active === index}
      onClick={() => setActive(index)}
    >
      {children}
    </button>
  );
};

Tabs.Panel = function Panel({ index, children }: { index: number; children: ReactNode }) {
  const { active } = use(TabsContext);
  if (active !== index) return null;
  return <div role="tabpanel">{children}</div>;
};

// Usage
<Tabs>
  <Tabs.Tab index={0}>Profile</Tabs.Tab>
  <Tabs.Tab index={1}>Settings</Tabs.Tab>
  <Tabs.Panel index={0}><ProfileForm /></Tabs.Panel>
  <Tabs.Panel index={1}><SettingsForm /></Tabs.Panel>
</Tabs>

パフォーマンスのルール

  • JSX の props 内でオブジェクトや配列を作成しないでください(再レンダーの原因になります)
  • プロファイリングで不要な再レンダーが確認された場合にのみ、React.memo を使用してください
  • 計算コストの高い処理や、メモ化された子コンポーネントに渡す安定した参照には、useMemo/useCallback を優先してください
  • コンポーネントの状態を意図的にリセットするには、key を使用してください
  • 状態は近くに配置してください。状態は、使用する場所のできるだけ近くに保ちます
  • 2 階層を超える props のバケツリレーは避け、代わりに Context またはコンポジションを使用してください

コメント