技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
react-nextjs【数据与研究】Use this skill when the user says 'Next.js', 'App Router', 'Server Component', 'Client Component', 'Next.js structure', 'SSR Next.js', 'SSG Next.js', 'Next.js data fetching', 'use server', 'use client', or when building a Next.js application with App Router. This skill enforces: default to Server Components, add 'use client' only when necessary (even…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build Next.js apps with App Router. Server Components by default. 'use client' only when required. Data fetching in Server Components with async/await.
Exact user phrases: "Next.js", "App Router", "Server Component", "Client Component", "Next.js structure", "SSR Next.js", "SSG Next.js", "Next.js data fetching", "use server", "use client".
Before activating, verify:
No file output. Produces page file structure and component code as text.
File structure:
app/
layout.tsx
page.tsx
loading.tsx
error.tsx
users/[id]/page.tsx
Code: show component and data fetching. No import statements.
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output.
2560 tokens.
| Approach | Trade-off | When to Use |
|---|---|---|
| Server Component | Zero client JS, direct DB access | Data fetching, static content |
| Client Component | Interactivity, hooks, browser APIs | Forms, animations, state |
| Route Handler (route.ts) | Standard API endpoint | Webhooks, third-party integration |
| Server Action | Form mutation, revalidation | All form submissions |
| Middleware | Edge, runs before request | Auth redirects, A/B testing |
| ISR (revalidate) | Stale-while-revalidate | Content pages, blogs |
Does the component need:
├── Event listeners (onClick, onChange) -> Client Component
├── Hooks (useState, useEffect, useContext) -> Client Component
├── Browser APIs (localStorage, window) -> Client Component
├── Direct DB access / file system -> Server Component
└── Fetch data from API -> Server Component
How often does the data change?
├── Never (blog post, docs) -> static (default, no revalidate)
├── Occasionally (product catalog) -> ISR with revalidate: 3600
├── On demand (CMS content) -> revalidatePath / revalidateTag
└── Every request (auth user) -> dynamic (no cache)
Is the fetch result user-specific?
├── Yes -> no cache
│ ├── User in cookie -> `cache: 'no-store'`
│ └── User in header -> `cache: 'no-store'`
└── No -> cache (default)
├── Can be stale 60s -> `next: { revalidate: 60 }`
└── Never stale -> default fetch caching
// app/users/page.tsx
async function UsersPage() {
const users = await db.user.findMany({ orderBy: { createdAt: 'desc' }, take: 20 })
return (
<div>
<h1>Users</h1>
{users.map(user => <UserCard key={user.id} user={user} />)}
</div>
)
}
// app/users/UserForm.tsx
'use client'
import { useActionState } from 'react'
import { createUser } from './actions'
export function UserForm() {
const [state, formAction, pending] = useActionState(createUser, null)
return (
<form action={formAction}>
<input name="name" required />
{state?.error && <p>{state.error}</p>}
<button disabled={pending}>{pending ? 'Saving...' : 'Create'}</button>
</form>
)
}
// app/users/actions.ts
'use server'
import { z } from 'zod'
const schema = z.object({ name: z.string().min(2), email: z.string().email() })
export async function createUser(_: any, formData: FormData) {
const data = Object.fromEntries(formData)
const result = schema.safeParse(data)
if (!result.success) return { error: 'Validation failed', issues: result.error.flatten().fieldErrors }
await db.user.create({ data: result.data })
revalidatePath('/users')
return { success: true }
}
// app/layout.tsx
export default function Layout({ children, feed, notifications }: {
children: React.ReactNode
feed: React.ReactNode
notifications: React.ReactNode
}) {
return (
<div>
<main>{children}</main>
<aside>
{feed}
{notifications}
</aside>
</div>
)
}
// app/(.)photo/[id]/page.tsx
export default function PhotoModal({ params }: { params: { id: string } }) {
return <div className="modal"><Photo id={params.id} /></div>
}
// app/api/users/route.ts
export async function GET() {
const users = await db.user.findMany()
return Response.json(users)
}
export async function POST(request: Request) {
const body = await request.json()
const user = await db.user.create({ data: body })
return Response.json(user, { status: 201 })
}
async function ProfilePage() {
const user = await db.user.findUnique({ where: { id } })
return <ProfileCard user={user} />
}
function ProductsPage({ searchParams }: { searchParams: { q?: string; page?: string } }) {
const results = await db.product.search(searchParams.q || '', { page: Number(searchParams.page) || 1 })
return <ProductList items={results} />
}
// app/Providers.tsx
'use client'
import { createContext, useContext } from 'react'
const ThemeContext = createContext<'light' | 'dark'>('light')
export const useTheme = () => useContext(ThemeContext)
// store/cart.ts
import { create } from 'zustand'
export const useCart = create<{ items: CartItem[]; add(item: CartItem): void }>((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
}))
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: { formats: ['image/avif', 'image/webp'] },
experimental: { ppr: true }, // Partial Prerendering
logging: { fetches: { fullUrl: true } },
serverExternalPackages: ['bcrypt'],
}
export default nextConfig
ANALYZE=true npm run build # generates bundle report
next build --debug # verbose build output
import { describe, it, expect } from 'vitest'
import { createUser } from '../app/users/actions'
describe('createUser', () => {
it('validates input', async () => {
const formData = new FormData()
formData.set('name', 'A')
formData.set('email', 'invalid')
const result = await createUser(null, formData)
expect(result).toHaveProperty('issues')
expect(result.issues.name).toBeDefined()
})
})
import { render, screen } from '@testing-library/react'
import { UserCard } from '../app/users/UserCard'
it('renders user name', () => {
render(<UserCard user={{ id: '1', name: 'Alice' }} />)
expect(screen.getByText('Alice')).toBeDefined()
})
import { test, expect } from '@playwright/test'
test('creates user', async ({ page }) => {
await page.goto('/users/new')
await page.fill('[name="name"]', 'Alice')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/users')
await expect(page.getByText('Alice')).toBeVisible()
})
// Before: Pages Router
pages/users/[id].tsx
getServerSideProps -> props
component receives props
// After: App Router
app/users/[id]/page.tsx
async function page({ params })
data fetching inline with await
// Layout migration:
pages/_app.tsx + pages/_document.tsx -> app/layout.tsx (single file)
// Before
export const getServerSideProps = async () => {
const data = await fetchData()
return { props: { data } }
}
// After
async function Page() {
const data = await fetchData()
return <div>{data}</div>
}
// Before
export const getStaticProps = async () => {
const data = await fetchData()
return { props: { data }, revalidate: 60 }
}
// After
async function Page() {
const data = await fetch('https://api.com/data', { next: { revalidate: 60 } }).then(r => r.json())
return <div>{data}</div>
}
// Anti-pattern
'use client'
function Page() { /* entire page client-rendered */ }
// Correct: only the interactive widget
function Page() { return <div><ClientWidget /></div> }
// Anti-pattern
useEffect(() => { fetch('/api/data').then(setData) }, [])
// Correct: Server Component
async function Page() { const data = await fetchData(); return <div>{data}</div> }
If a layout.tsx has 'use client', all children become client components. Keep layouts as Server Components.
Functions, Date objects, and undefined values crash between Server and Client Components.
| Aspect | App Router | Pages Router |
|---|---|---|
| Component model | Server + Client | All client |
| Data fetching | async component + fetch | getServerSideProps/getStaticProps |
| Layouts | Nested, persist | Manual per-page |
| Streaming | Native via Suspense | Not supported |
| Loading states | loading.tsx auto | Manual |
| Metadata | Built-in Metadata API | next/head |
| Mutations | Server Actions | API routes + client fetch |
next build --debug — build analysis@next/bundle-analyzer — visualize bundlesnext lint — ESLint with Next.js rulesnext dev --turbo — Turbopack@next/codemod — migration upgradesnext-sitemap — sitemaps@sentry/nextjs — error trackingDefault: Server Component. Add 'use client' only for:
onClick/onChange/onSubmit -> Client
useState/useEffect/useReducer -> Client
Browser APIs -> Client
app/
layout.tsx -- Root (html, body, providers)
page.tsx -- Home
loading.tsx -- Suspense fallback
error.tsx -- Error boundary (client)
not-found.tsx -- 404
users/[id]/page.tsx -- Dynamic route
(auth)/login/page.tsx -- Route group
async function Page() {
const data = await db.query(...)
return <div>{data}</div>
}
'use server'
export async function action(formData: FormData) {
await db.mutate(...)
revalidatePath('/')
}
export const metadata: Metadata = { title: 'Page Title' }
No artifact produced. Next skill: frontend-testing. Carry forward: App Router structure, data fetching pattern, Server/Client boundary decisions.