ワンクリックで
react-nextjs-patterns
Modern React 19 and Next.js 15 patterns for FrankX applications
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Modern React 19 and Next.js 15 patterns for FrankX applications
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Apply official FrankX brand standards to all artifacts, ensuring visual consistency across content, products, and communications. Use this skill for any FrankX-related content creation, design work, or communication.
Frank's personalized career and professional mastery coaching
Guided process to create your personalized Soulbook - a comprehensive life transformation system
Creates expert positioning content, social media posts, and marketing materials for Frank's personal brand and AI coaching business with soul-aligned messaging
Master-level visionary book writing system with research-driven methodology, author voice modeling, and iterative refinement across 9 quality dimensions
Expert Suno AI prompt engineering for cinematic, transformative music creation. Use this skill when creating Suno prompts for Vibe OS sessions, meditation tracks, or any AI-generated music that needs professional quality and emotional resonance.
| name | React & Next.js Patterns |
| description | Modern React 19 and Next.js 15 patterns for FrankX applications |
| version | 1.0.0 |
| source | Adapted from vercel-labs/react-best-practices |
Framework: Next.js 15 (App Router)
React: 19 (with Server Components)
Styling: Tailwind CSS 4 + shadcn/ui
State: React hooks + Server Actions
Validation: Zod
Testing: Vitest + Playwright
In Next.js App Router, components are Server Components by default.
// app/products/page.tsx - Server Component (default)
import { db } from '@/lib/db';
export default async function ProductsPage() {
// Direct database access - no API needed
const products = await db.product.findMany();
return (
<main>
<h1>Products</h1>
<ProductList products={products} />
</main>
);
}
When to use Server Components:
Add 'use client' when you need interactivity:
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
When to use Client Components:
// Server Component (page)
import { getUser } from '@/lib/auth';
import { UserProfile } from './UserProfile'; // Client
export default async function ProfilePage() {
const user = await getUser(); // Server-side fetch
return (
<main>
{/* Pass server data to client component */}
<UserProfile initialData={user} />
</main>
);
}
// Client Component
'use client';
export function UserProfile({ initialData }: { initialData: User }) {
const [user, setUser] = useState(initialData);
// Client-side interactivity with server-fetched data
}
Replace API routes for mutations:
// app/actions/subscribe.ts
'use server';
import { z } from 'zod';
import { db } from '@/lib/db';
const schema = z.object({
email: z.string().email(),
});
export async function subscribeAction(formData: FormData) {
const parsed = schema.safeParse({
email: formData.get('email'),
});
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors };
}
await db.subscriber.create({
data: { email: parsed.data.email },
});
return { success: true };
}
// app/components/SubscribeForm.tsx
'use client';
import { subscribeAction } from '@/app/actions/subscribe';
import { useActionState } from 'react';
export function SubscribeForm() {
const [state, action, pending] = useActionState(subscribeAction, null);
return (
<form action={action}>
<input name="email" type="email" disabled={pending} />
<button type="submit" disabled={pending}>
{pending ? 'Subscribing...' : 'Subscribe'}
</button>
{state?.error && <p className="text-red-500">{state.error.email}</p>}
{state?.success && <p className="text-green-500">Subscribed!</p>}
</form>
);
}
export default async function DashboardPage() {
// Fetch in parallel, not waterfall
const [user, posts, analytics] = await Promise.all([
getUser(),
getPosts(),
getAnalytics(),
]);
return <Dashboard user={user} posts={posts} analytics={analytics} />;
}
import { Suspense } from 'react';
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
{/* Fast content renders immediately */}
<WelcomeMessage />
{/* Slow content streams in */}
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</main>
);
}
// Fetch with cache control
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: {
revalidate: 3600, // Revalidate every hour
tags: ['products'], // For on-demand revalidation
},
});
return res.json();
}
// On-demand revalidation (in Server Action)
import { revalidateTag } from 'next/cache';
export async function updateProduct() {
await db.product.update(...);
revalidateTag('products'); // Invalidate cache
}
// app/products/error.tsx
'use client';
export default function ProductsError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="text-center py-10">
<h2>Something went wrong!</h2>
<p className="text-neutral-500">{error.message}</p>
<button onClick={reset} className="mt-4 btn">
Try again
</button>
</div>
);
}
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
export default async function ProductPage({ params }: Props) {
const product = await getProduct(params.id);
if (!product) {
notFound(); // Renders not-found.tsx
}
return <ProductDetails product={product} />;
}
import dynamic from 'next/dynamic';
// Load heavy component only when needed
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <ChartSkeleton />,
ssr: false, // Client-only
});
import Image from 'next/image';
export function ProductImage({ src, alt }: Props) {
return (
<Image
src={src}
alt={alt}
width={400}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
priority={false} // true for above-fold images
/>
);
}
// app/products/[id]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({ params }): Promise<Metadata> {
const product = await getProduct(params.id);
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.image],
},
};
}
// Server component with glassmorphic styling
export async function FeaturedProducts() {
const products = await getFeaturedProducts();
return (
<section className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-8">
<h2 className="text-2xl font-bold bg-gradient-to-r from-white to-aurora-1 bg-clip-text text-transparent">
Featured
</h2>
<div className="grid grid-cols-3 gap-6 mt-6">
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
</section>
);
}
// app/products/loading.tsx
export default function ProductsLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-white/10 rounded w-1/4 mb-6" />
<div className="grid grid-cols-3 gap-6">
{[1, 2, 3].map(i => (
<div key={i} className="h-64 bg-white/5 rounded-xl" />
))}
</div>
</div>
);
}
| Anti-Pattern | Better Approach |
|---|---|
'use client' at page level | Keep pages as Server Components |
| Fetching in useEffect | Fetch in Server Components |
| Props drilling through many levels | Use composition or context |
| Giant components | Extract into smaller pieces |
| Inline styles everywhere | Use Tailwind utilities |
| Ignoring loading/error states | Always handle both |
shadcn-ui-patterns - Component implementationtest-driven-development - Test componentswebapp-testing - E2E testing