component-migration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseComponent Migration
组件迁移
Migrate components for RSC (React Server Component) compatibility. Determine which components need directives and which can remain server components.
'use client'为兼容RSC(React Server Component)进行组件迁移。确定哪些组件需要添加指令,哪些可以保留为服务端组件。
'use client'Toolkit Setup
工具包设置
This skill requires the skill to be installed. All migration skills depend on it for AST analysis.
nextjs-migration-toolkitbash
TOOLKIT_DIR="$(cd "$(dirname "$SKILL_PATH")/../nextjs-migration-toolkit" && pwd)"
if [ ! -f "$TOOLKIT_DIR/package.json" ]; then
echo "ERROR: nextjs-migration-toolkit is not installed." >&2
echo "Run: npx skills add blazity/next-migration-skills -s nextjs-migration-toolkit" >&2
echo "Then retry this skill." >&2
exit 1
fi
bash "$TOOLKIT_DIR/scripts/setup.sh" >/dev/null此技能需要安装技能。所有迁移技能都依赖它进行AST分析。
nextjs-migration-toolkitbash
TOOLKIT_DIR="$(cd "$(dirname "$SKILL_PATH")/../nextjs-migration-toolkit" && pwd)"
if [ ! -f "$TOOLKIT_DIR/package.json" ]; then
echo "ERROR: nextjs-migration-toolkit is not installed." >&2
echo "Run: npx skills add blazity/next-migration-skills -s nextjs-migration-toolkit" >&2
echo "Then retry this skill." >&2
exit 1
fi
bash "$TOOLKIT_DIR/scripts/setup.sh" >/dev/nullVersion-Specific Patterns
特定版本模式
Before applying any migration patterns, check the target Next.js version. Read if it exists, or ask the user.
.migration/target-version.txtThen read the corresponding version patterns file:
bash
SKILL_DIR="$(cd "$(dirname "$SKILL_PATH")" && pwd)"
cat "$SKILL_DIR/../version-patterns/nextjs-<version>.md"Critical version differences that affect component migration:
- Next.js 14: and
paramsin page components are plain objectssearchParams - Next.js 15+: and
paramsare Promises — async pages mustsearchParamsthem, client pages must useawaitfrom Reactuse()
在应用任何迁移模式之前,请检查目标Next.js版本。如果存在文件,请读取该文件,或询问用户。
.migration/target-version.txt然后读取对应的版本模式文件:
bash
SKILL_DIR="$(cd "$(dirname "$SKILL_PATH")" && pwd)"
cat "$SKILL_DIR/../version-patterns/nextjs-<version>.md"影响组件迁移的关键版本差异:
- Next.js 14:页面组件中的和
params为普通对象searchParams - Next.js 15+:和
params是Promise —— 异步页面必须searchParams它们,客户端页面必须使用React的awaituse()
Steps
步骤
1. Inventory All Components
1. 盘点所有组件
bash
npx tsx "$TOOLKIT_DIR/src/bin/ast-tool.ts" analyze components <srcDir>bash
npx tsx "$TOOLKIT_DIR/src/bin/ast-tool.ts" analyze components <srcDir>2. Review Classification
2. 审查分类结果
For each component classified as "client":
- Review the client indicators (hooks, events, browser APIs)
- Determine if the entire component needs to be client, or if it can be split
对于每个被分类为“客户端”的组件:
- 查看客户端标识(钩子、事件、浏览器API)
- 判断是否需要将整个组件设为客户端组件,或者是否可以拆分
3. Apply Client Directives
3. 应用客户端指令
For components that must be client components:
- Add as the first line of the file
'use client'; - Ensure all imports used by the client component are also client-compatible
对于必须设为客户端组件的文件:
- 在文件第一行添加
'use client'; - 确保客户端组件使用的所有导入也兼容客户端环境
4. Split Components Where Possible
4. 尽可能拆分组件
If a component has both server and client parts:
- Extract the interactive part into a separate client component
- Keep the data-fetching and static rendering in the server component
- Import the client component into the server component
Before — mixed component (Pages Router):
tsx
import { useState } from 'react';
import { useRouter } from 'next/router';
export default function ProductPage({ product }) {
const router = useRouter();
const [qty, setQty] = useState(1);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<input type="number" value={qty} onChange={(e) => setQty(Number(e.target.value))} />
<button onClick={() => alert(`Added ${qty}`)}>Add to Cart</button>
<button onClick={() => router.back()}>Back</button>
</div>
);
}After — split into server + client (App Router):
app/products/[id]/page.tsxtsx
import { Metadata } from 'next';
import { ProductActions } from './product-actions';
export async function generateMetadata({ params }): Promise<Metadata> {
const product = await fetch(`https://api.example.com/products/${params.id}`,
{ cache: 'no-store' }).then(r => r.json());
return { title: product.name };
}
export default async function ProductPage({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`,
{ cache: 'no-store' }).then(r => r.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ProductActions />
</div>
);
}app/products/[id]/product-actions.tsxtsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function ProductActions() {
const router = useRouter();
const [qty, setQty] = useState(1);
return (
<>
<input type="number" value={qty} onChange={(e) => setQty(Number(e.target.value))} />
<button onClick={() => alert(`Added ${qty}`)}>Add to Cart</button>
<button onClick={() => router.back()}>Back</button>
</>
);
}Key points in the split:
- Server component does the data fetching (async, no hooks)
- Client component handles all interactivity (useState, onClick, useRouter)
- import changes from
useRoutertonext/routernext/navigation - Props passed across the boundary must be serializable (no functions or Date objects)
如果组件同时包含服务端和客户端部分:
- 将交互式部分提取为独立的客户端组件
- 将数据获取和静态渲染逻辑保留在服务端组件中
- 在服务端组件中导入客户端组件
拆分前 —— 混合组件(Pages Router):
tsx
import { useState } from 'react';
import { useRouter } from 'next/router';
export default function ProductPage({ product }) {
const router = useRouter();
const [qty, setQty] = useState(1);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<input type="number" value={qty} onChange={(e) => setQty(Number(e.target.value))} />
<button onClick={() => alert(`Added ${qty}`)}>Add to Cart</button>
<button onClick={() => router.back()}>Back</button>
</div>
);
}拆分后 —— 拆分为服务端+客户端(App Router):
app/products/[id]/page.tsxtsx
import { Metadata } from 'next';
import { ProductActions } from './product-actions';
export async function generateMetadata({ params }): Promise<Metadata> {
const product = await fetch(`https://api.example.com/products/${params.id}`,
{ cache: 'no-store' }).then(r => r.json());
return { title: product.name };
}
export default async function ProductPage({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`,
{ cache: 'no-store' }).then(r => r.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ProductActions />
</div>
);
}app/products/[id]/product-actions.tsxtsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function ProductActions() {
const router = useRouter();
const [qty, setQty] = useState(1);
return (
<>
<input type="number" value={qty} onChange={(e) => setQty(Number(e.target.value))} />
<button onClick={() => alert(`Added ${qty}`)}>Add to Cart</button>
<button onClick={() => router.back()}>Back</button>
</>
);
}拆分的关键点:
- 服务端组件负责数据获取(异步,无钩子)
- 客户端组件处理所有交互逻辑(useState、onClick、useRouter)
- 的导入从
useRouter改为next/routernext/navigation - 跨边界传递的props必须可序列化(不能是函数或Date对象)
5. Update Props
5. 更新Props
bash
npx tsx "$TOOLKIT_DIR/src/bin/ast-tool.ts" analyze props <componentFile>- Ensure props passed from server to client components are serializable
- No functions, classes, or Date objects as props across the boundary
- Convert non-serializable props to serializable alternatives
bash
npx tsx "$TOOLKIT_DIR/src/bin/ast-tool.ts" analyze props <componentFile>- 确保从服务端传递到客户端组件的props是可序列化的
- 禁止跨边界传递函数、类或Date对象作为props
- 将不可序列化的props转换为可序列化的替代方案
6. Validate Components
6. 验证组件
bash
npx tsx "$TOOLKIT_DIR/src/bin/ast-tool.ts" validate <appDir>Check that:
- All files using client features have
'use client' - No server-only imports in client components
- No usage (should be
next/router)next/navigation
bash
npx tsx "$TOOLKIT_DIR/src/bin/ast-tool.ts" validate <appDir>检查以下内容:
- 所有使用客户端特性的文件都已添加
'use client' - 客户端组件中没有服务端专属的导入
- 没有使用(应改为
next/router)next/navigation
Common Pitfalls
常见陷阱
"You're importing a component that needs useState"
“你正在导入一个需要useState的组件”
Error:
Cause: A server component imports a component that uses hooks/events, but neither file has .
Fix: Add to the component that uses the hook — NOT to the page that imports it. The directive marks the boundary; everything imported by a client component is also client.
It only works in a Client Component but none of its parents are marked with "use client"'use client''use client'错误信息:
原因:服务端组件导入了一个使用钩子/事件的组件,但两个文件都没有添加。
修复方案:在使用钩子的组件中添加 —— 不要在导入它的页面中添加。该指令会标记边界;客户端组件导入的所有内容也会被视为客户端代码。
It only works in a Client Component but none of its parents are marked with "use client"'use client''use client'Adding 'use client' too broadly
过度添加'use client'
Wrong: Adding to a page component just because it imports one interactive child.
Right: Add only to the interactive component. Keep the page as a server component and import the client component into it.
Rule of thumb: Push as deep as possible in the component tree.
'use client''use client''use client'错误做法:仅因为页面导入了一个交互式子组件,就给页面组件添加。
正确做法:仅给交互式组件添加。保持页面为服务端组件,并在其中导入客户端组件。
经验法则:将尽可能深地放在组件树中。
'use client''use client''use client'Passing non-serializable props across the boundary
跨边界传递不可序列化的props
Error:
Cause: A server component passes a function, Date object, or class instance as a prop to a client component.
Fix: Convert to serializable alternatives:
Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server"- Functions → Use server actions () or pass data and let the client component create its own handlers
'use server' - Date objects → Pass as ISO string, parse in client
- Class instances → Pass as plain object
错误信息:
原因:服务端组件将函数、Date对象或类实例作为props传递给客户端组件。
修复方案:转换为可序列化的替代方案:
Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server"- 函数 → 使用服务端动作()或传递数据,让客户端组件创建自己的处理程序
'use server' - Date对象 → 以ISO字符串形式传递,在客户端解析
- 类实例 → 转换为普通对象传递
The "children" pattern for mixing server + client
混合服务端+客户端的“children”模式
When a client component needs to render server content, use the children pattern:
tsx
// layout.tsx (server component)
import { ClientShell } from './client-shell';
import { ServerContent } from './server-content';
export default function Layout() {
return (
<ClientShell>
<ServerContent /> {/* Server component passed as children */}
</ClientShell>
);
}This works because is a React node (serializable), not a component reference.
children当客户端组件需要渲染服务端内容时,使用children模式:
tsx
// layout.tsx(服务端组件)
import { ClientShell } from './client-shell';
import { ServerContent } from './server-content';
export default function Layout() {
return (
<ClientShell>
<ServerContent /> {/* 服务端组件作为children传递 */}
</ClientShell>
);
}这种方式可行是因为是React节点(可序列化),而非组件引用。
childrenNEVER convert getServerSideProps data into useEffect + fetch
切勿将getServerSideProps数据转换为useEffect + fetch
Wrong: Page had providing data as props. You make the whole page and fetch data in a + :
getServerSideProps'use client'useEffectuseStatetsx
// WRONG — "use client" page with useEffect
'use client';
import { useState, useEffect } from 'react';
export default function AnalyticsPage() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/analytics').then(r => r.json()).then(setData);
}, []);
if (!data) return <p>Loading...</p>;
return <div>{data.pageViews}</div>;
}Why this is broken: The HTTP response contains only "Loading..." — real data appears only after JavaScript executes on the client. This destroys SSR, breaks SEO, and fails any test that checks the server-rendered HTML for data content.
Right: Split into an async server component (fetches data) and a client component (handles interactivity). Data appears in the initial HTML response.
tsx
// app/analytics/page.tsx (server component — NO 'use client')
import { getAnalyticsData } from '@/lib/analytics';
import { AnalyticsView } from './analytics-view';
export default async function AnalyticsPage() {
const data = getAnalyticsData();
return <AnalyticsView data={data} />;
}tsx
// app/analytics/analytics-view.tsx (client component)
'use client';
import { useState } from 'react';
export function AnalyticsView({ data }: { data: AnalyticsData }) {
const [view, setView] = useState<'overview' | 'pages'>('overview');
return (
<div>
<button onClick={() => setView('overview')}>Overview</button>
<button onClick={() => setView('pages')}>Top Pages</button>
{view === 'overview' ? <p>{data.pageViews}</p> : <p>...</p>}
</div>
);
}Rule: If the original page had or , the data fetching MUST stay in a server component. NEVER move it into . The only code that goes into the client component is the interactive UI (useState, onClick, etc.), receiving data as props.
getServerSidePropsgetStaticPropsuseEffect错误做法:原本页面使用提供数据作为props。将整个页面设为,并在 + 中获取数据:
getServerSideProps'use client'useEffectuseStatetsx
// 错误示例 —— 使用useEffect的'use client'页面
'use client';
import { useState, useEffect } from 'react';
export default function AnalyticsPage() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/analytics').then(r => r.json()).then(setData);
}, []);
if (!data) return <p>Loading...</p>;
return <div>{data.pageViews}</div>;
}问题所在:HTTP响应仅包含“Loading...”,实际数据仅在客户端执行JavaScript后才会显示。这会破坏SSR、影响SEO,并且任何检查服务端渲染HTML数据内容的测试都会失败。
正确做法:拆分为异步服务端组件(负责获取数据)和客户端组件(负责处理交互)。数据会出现在初始HTML响应中。
tsx
// app/analytics/page.tsx(服务端组件 —— 无需'use client')
import { getAnalyticsData } from '@/lib/analytics';
import { AnalyticsView } from './analytics-view';
export default async function AnalyticsPage() {
const data = getAnalyticsData();
return <AnalyticsView data={data} />;
}tsx
// app/analytics/analytics-view.tsx(客户端组件)
'use client';
import { useState } from 'react';
export function AnalyticsView({ data }: { data: AnalyticsData }) {
const [view, setView] = useState<'overview' | 'pages'>('overview');
return (
<div>
<button onClick={() => setView('overview')}>Overview</button>
<button onClick={() => setView('pages')}>Top Pages</button>
{view === 'overview' ? <p>{data.pageViews}</p> : <p>...</p>}
</div>
);
}规则:如果原页面使用了或,数据获取逻辑必须保留在服务端组件中。切勿将其移至中。只有交互式UI代码(useState、onClick等)才应放入客户端组件,并通过props接收数据。
getServerSidePropsgetStaticPropsuseEffect