react-patterns について
React開発者向けのパターン集です。React 19、Server Components、Server Actions、コンポーネント構成、パフォーマンスに関する実践を扱います。
主な機能
- 01React 19のuse()と状態フック
- 02Server ComponentsとServer Actions
- 03Suspenseとエラーバウンダリ
- 04カスタムフックと複合コンポーネント
- 05パフォーマンスと状態整理の指針
活用シーン
- 01非同期データを読むUIの実装
- 02サーバー側フォーム操作の構築
- 03データ読み込み境界の分割
- 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 またはコンポジションを使用してください
コメント
Markdown 対応