Communitygithub.com

Samueladewole/claude-skills

Maîtrisez Next.js 14+ avec App Router, Server Components, streaming, routes parallèles et récupération avancée de données. À utiliser lors de la création d'applications Next.js, de la mise en œuvre de SSR/SSG ou de l'optimisation de React Server Components.

Qu'est-ce que claude-skills ?

Cette compétence fournit des modèles complets pour l'architecture Next.js 14+ App Router, les Server Components et le développement full-stack moderne avec React. Elle couvre les modes de rendu, les conventions de fichiers et des modèles pratiques. Vous apprendrez à implémenter des Server Components, des Client Components, des Server Actions, des routes parallèles et interceptées, ainsi que le streaming avec Suspense. La compétence met l'accent sur les meilleures pratiques comme le colocalisation des requêtes de données, l'utilisation des limites Suspense et le recours aux Server Actions pour les mutations. Elle prend en charge les plateformes Next.js et est conçue pour être utilisée avec des agents IA comme Claude, Cursor ou Codex. La documentation inclut des snippets de code TypeScript détaillés, des stratégies de mise en cache et des directives pour éviter les pièges courants, ce qui la rend idéale pour les développeurs construisant ou migrant vers l'App Router.

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Samueladewole/claude-skills/tree/HEAD/nextjs-app-router-patterns

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Modèles Next.js App Router

Modèles complets pour l'architecture Next.js 14+ App Router, les Server Components et le développement full-stack moderne avec React.

Quand utiliser cette compétence

  • Construction de nouvelles applications Next.js avec App Router
  • Migration de Pages Router vers App Router
  • Implémentation de Server Components et de streaming
  • Mise en place de routes parallèles et interceptées
  • Optimisation de la récupération et de la mise en cache des données
  • Développement de fonctionnalités full-stack avec Server Actions

Concepts Fondamentaux

1. Modes de Rendu

ModeOùQuand l'utiliser
Server ComponentsServeur uniquementRécupération de données, calculs lourds, secrets
Client ComponentsNavigateurInteractivité, hooks, API navigateur
StatiqueTemps de buildContenu qui change rarement
DynamiqueTemps de requêteDonnées personnalisées ou en temps réel
StreamingProgressifPages volumineuses, sources de données lentes

2. Conventions de Fichiers

app/
├── layout.tsx       # Conteneur UI partagé
├── page.tsx         # UI de la route
├── loading.tsx      # UI de chargement (Suspense)
├── error.tsx        # Limite d'erreur
├── not-found.tsx    # UI pour 404
├── route.ts         # Point d'accès API
├── template.tsx     # Layout remonté
├── default.tsx      # Fallback pour route parallèle
└── opengraph-image.tsx  # Génération d'image OG

Démarrage Rapide

// app/layout.tsx
import { Inter } from 'next/font/google'
import { Providers } from './providers'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: { default: 'My App', template: '%s | My App' },
  description: 'Built with Next.js App Router',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={inter.className}>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

// app/page.tsx - Server Component par défaut
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // ISR : revalider toutes les heures
  })
  return res.json()
}

export default async function HomePage() {
  const products = await getProducts()

  return (
    <main>
      <h1>Products</h1>
      <ProductGrid products={products} />
    </main>
  )
}

Modèles

Modèle 1 : Server Components avec Récupération de Données

// app/products/page.tsx
import { Suspense } from 'react'
import { ProductList, ProductListSkeleton } from '@/components/products'
import { FilterSidebar } from '@/components/filters'

interface SearchParams {
  category?: string
  sort?: 'price' | 'name' | 'date'
  page?: string
}

export default async function ProductsPage({
  searchParams,
}: {
  searchParams: Promise<SearchParams>
}) {
  const params = await searchParams

  return (
    <div className="flex gap-8">
      <FilterSidebar />
      <Suspense
        key={JSON.stringify(params)}
        fallback={<ProductListSkeleton />}
      >
        <ProductList
          category={params.category}
          sort={params.sort}
          page={Number(params.page) || 1}
        />
      </Suspense>
    </div>
  )
}

// components/products/ProductList.tsx - Server Component
async function getProducts(filters: ProductFilters) {
  const res = await fetch(
    `${process.env.API_URL}/products?${new URLSearchParams(filters)}`,
    { next: { tags: ['products'] } }
  )
  if (!res.ok) throw new Error('Failed to fetch products')
  return res.json()
}

export async function ProductList({ category, sort, page }: ProductFilters) {
  const { products, totalPages } = await getProducts({ category, sort, page })

  return (
    <div>
      <div className="grid grid-cols-3 gap-4">
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
      <Pagination currentPage={page} totalPages={totalPages} />
    </div>
  )
}

Modèle 2 : Client Components avec 'use client'

// components/products/AddToCartButton.tsx
'use client'

import { useState, useTransition } from 'react'
import { addToCart } from '@/app/actions/cart'

export function AddToCartButton({ productId }: { productId: string }) {
  const [isPending, startTransition] = useTransition()
  const [error, setError] = useState<string | null>(null)

  const handleClick = () => {
    setError(null)
    startTransition(async () => {
      const result = await addToCart(productId)
      if (result.error) {
        setError(result.error)
      }
    })
  }

  return (
    <div>
      <button
        onClick={handleClick}
        disabled={isPending}
        className="btn-primary"
      >
        {isPending ? 'Ajout...' : 'Ajouter au panier'}
      </button>
      {error && <p className="text-red-500 text-sm">{error}</p>}
    </div>
  )
}

Modèle 3 : Server Actions

// app/actions/cart.ts
"use server";

import { revalidateTag } from "next/cache";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function addToCart(productId: string) {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("session")?.value;

  if (!sessionId) {
    redirect("/login");
  }

  try {
    await db.cart.upsert({
      where: { sessionId_productId: { sessionId, productId } },
      update: { quantity: { increment: 1 } },
      create: { sessionId, productId, quantity: 1 },
    });

    revalidateTag("cart");
    return { success: true };
  } catch (error) {
    return { error: "Impossible d'ajouter l'article au panier" };
  }
}

export async function checkout(formData: FormData) {
  const address = formData.get("address") as string;
  const payment = formData.get("payment") as string;

  // Valider
  if (!address || !payment) {
    return { error: "Champs obligatoires manquants" };
  }

  // Traiter la commande
  const order = await processOrder({ address, payment });

  // Rediriger vers la confirmation
  redirect(`/orders/${order.id}/confirmation`);
}

Modèle 4 : Routes Parallèles

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <div className="dashboard-grid">
      <main>{children}</main>
      <aside className="analytics-panel">{analytics}</aside>
      <aside className="team-panel">{team}</aside>
    </div>
  )
}

// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
  const stats = await getAnalytics()
  return <AnalyticsChart data={stats} />
}

// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
  return <ChartSkeleton />
}

// app/dashboard/@team/page.tsx
export default async function TeamSlot() {
  const members = await getTeamMembers()
  return <TeamList members={members} />
}

Modèle 5 : Routes Interceptées (Modèle Modal)

// Structure de fichiers pour le modal photo
// app/
// ├── @modal/
// │   ├── (.)photos/[id]/page.tsx  # Interception
// │   └── default.tsx
// ├── photos/
// │   └── [id]/page.tsx            # Page complète
// └── layout.tsx

// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal'
import { PhotoDetail } from '@/components/PhotoDetail'

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const photo = await getPhoto(id)

  return (
    <Modal>
      <PhotoDetail photo={photo} />
    </Modal>
  )
}

// app/photos/[id]/page.tsx - Version page complète
export default async function PhotoPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const photo = await getPhoto(id)

  return (
    <div className="photo-page">
      <PhotoDetail photo={photo} />
      <RelatedPhotos photoId={id} />
    </div>
  )
}

// app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <html>
      <body>
        {children}
        {modal}
      </body>
    </html>
  )
}

Modèle 6 : Streaming avec Suspense

// app/product/[id]/page.tsx
import { Suspense } from 'react'

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  // Ces données se chargent en premier (bloquant)
  const product = await getProduct(id)

  return (
    <div>
      {/* Rendu immédiat */}
      <ProductHeader product={product} />

      {/* Diffusion des avis */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={id} />
      </Suspense>

      {/* Diffusion des recommandations */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={id} />
      </Suspense>
    </div>
  )
}

// Ces composants récupèrent leurs propres données
async function Reviews({ productId }: { productId: string }) {
  const reviews = await getReviews(productId) // API lente
  return <ReviewList reviews={reviews} />
}

async function Recommendations({ productId }: { productId: string }) {
  const products = await getRecommendations(productId) // Basé sur ML, lent
  return <ProductCarousel products={products} />
}

Modèle 7 : Gestionnaires de Route (Routes API)

// app/api/products/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const category = searchParams.get("category");

  const products = await db.product.findMany({
    where: category ? { category } : undefined,
    take: 20,
  });

  return NextResponse.json(products);
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  const product = await db.product.create({
    data: body,
  });

  return NextResponse.json(product, { status: 201 });
}

// app/api/products/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const product = await db.product.findUnique({ where: { id } });

  if (!product) {
    return NextResponse.json({ error: "Produit non trouvé" }, { status: 404 });
  }

  return NextResponse.json(product);
}

Modèle 8 : Métadonnées et SEO

// app/products/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'

type Props = {
  params: Promise<{ slug: string }>
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const product = await getProduct(slug)

  if (!product) return {}

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      images: [{ url: product.image, width: 1200, height: 630 }],
    },
    twitter: {
      card: 'summary_large_image',
      title: product.name,
      description: product.description,
      images: [product.image],
    },
  }
}

export async function generateStaticParams() {
  const products = await db.product.findMany({ select: { slug: true } })
  return products.map((p) => ({ slug: p.slug }))
}

export default async function ProductPage({ params }: Props) {
  const { slug } = await params
  const product = await getProduct(slug)

  if (!product) notFound()

  return <ProductDetail product={product} />
}

Stratégies de Mise en Cache

Cache de Données

// Pas de cache (toujours frais)
fetch(url, { cache: "no-store" });

// Cache permanent (statique)
fetch(url, { cache: "force-cache" });

// ISR - revalider après 60 secondes
fetch(url, { next: { revalidate: 60 } });

// Invalidation basée sur les tags
fetch(url, { next: { tags: ["products"] } });

// Invalidation via Server Action
("use server");
import { revalidateTag, revalidatePath } from "next/cache";

export async function updateProduct(id: string, data: ProductData) {
  await db.product.update({ where: { id }, data });
  revalidateTag("products");
  revalidatePath("/products");
}

Bonnes Pratiques

À faire

  • Commencez par Server Components - Ajoutez 'use client' seulement quand nécessaire
  • Colocalisez les requêtes de données - Récupérez les données là où elles sont utilisées
  • Utilisez des limites Suspense - Activez le streaming pour les données lentes
  • Tirez parti des routes parallèles - États de chargement indépendants
  • Utilisez Server Actions - Pour les mutations avec amélioration progressive

À éviter

  • Ne passez pas de données non sérialisables - Limitations de la frontière Serveur → Client
  • N'utilisez pas de hooks dans Server Components - Pas de useState, useEffect
  • Ne faites pas de requêtes dans Client Components - Utilisez Server Components ou React Query
  • N'imbriquez pas trop de layouts - Chaque layout s'ajoute à l'arbre de composants
  • N'ignorez pas les états de chargement - Fournissez toujours loading.tsx ou Suspense

Ressources

Individual skills in this repo

This repo contains 5 individual skills — each has its own dedicated page.

Skills associés