component-migration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Component Migration

组件迁移

Migrate components for RSC (React Server Component) compatibility. Determine which components need
'use client'
directives and which can remain server components.
为兼容RSC(React Server Component)进行组件迁移。确定哪些组件需要添加
'use client'
指令,哪些可以保留为服务端组件。

Toolkit Setup

工具包设置

This skill requires the
nextjs-migration-toolkit
skill to be installed. All migration skills depend on it for AST analysis.
bash
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
此技能需要安装
nextjs-migration-toolkit
技能。所有迁移技能都依赖它进行AST分析。
bash
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

Version-Specific Patterns

特定版本模式

Before applying any migration patterns, check the target Next.js version. Read
.migration/target-version.txt
if it exists, or ask the user.
Then 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:
    params
    and
    searchParams
    in page components are plain objects
  • Next.js 15+:
    params
    and
    searchParams
    are Promises — async pages must
    await
    them, client pages must use
    use()
    from React
在应用任何迁移模式之前,请检查目标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
    searchParams
    是Promise —— 异步页面必须
    await
    它们,客户端页面必须使用React的
    use()

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
    'use client';
    as the first line of the file
  • 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.tsx
(server component):
tsx
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.tsx
(client component):
tsx
'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)
  • useRouter
    import changes from
    next/router
    to
    next/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.tsx
(服务端组件):
tsx
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.tsx
(客户端组件):
tsx
'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/router
    改为
    next/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
    next/router
    usage (should be
    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:
It only works in a Client Component but none of its parents are marked with "use client"
Cause: A server component imports a component that uses hooks/events, but neither file has
'use client'
. Fix: Add
'use client'
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'
—— 不要在导入它的页面中添加。该指令会标记边界;客户端组件导入的所有内容也会被视为客户端代码。

Adding 'use client' too broadly

过度添加'use client'

Wrong: Adding
'use client'
to a page component just because it imports one interactive child. Right: Add
'use client'
only to the interactive component. Keep the page as a server component and import the client component into it. Rule of thumb: Push
'use client'
as deep as possible in the component tree.
错误做法:仅因为页面导入了一个交互式子组件,就给页面组件添加
'use client'
正确做法:仅给交互式组件添加
'use client'
。保持页面为服务端组件,并在其中导入客户端组件。 经验法则:将
'use client'
尽可能深地放在组件树中。

Passing non-serializable props across the boundary

跨边界传递不可序列化的props

Error:
Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server"
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 → Use server actions (
    'use server'
    ) or pass data and let the client component create its own handlers
  • Date objects → Pass as ISO string, parse in client
  • Class instances → Pass as plain object
错误信息
Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server"
原因:服务端组件将函数、Date对象或类实例作为props传递给客户端组件。 修复方案:转换为可序列化的替代方案:
  • 函数 → 使用服务端动作(
    '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
children
is a React node (serializable), not a component reference.
当客户端组件需要渲染服务端内容时,使用children模式:
tsx
// layout.tsx(服务端组件)
import { ClientShell } from './client-shell';
import { ServerContent } from './server-content';

export default function Layout() {
  return (
    <ClientShell>
      <ServerContent />   {/* 服务端组件作为children传递 */}
    </ClientShell>
  );
}
这种方式可行是因为
children
是React节点(可序列化),而非组件引用。

NEVER convert getServerSideProps data into useEffect + fetch

切勿将getServerSideProps数据转换为useEffect + fetch

Wrong: Page had
getServerSideProps
providing data as props. You make the whole page
'use client'
and fetch data in a
useEffect
+
useState
:
tsx
// 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
getServerSideProps
or
getStaticProps
, the data fetching MUST stay in a server component. NEVER move it into
useEffect
. The only code that goes into the client component is the interactive UI (useState, onClick, etc.), receiving data as props.
错误做法:原本页面使用
getServerSideProps
提供数据作为props。将整个页面设为
'use client'
,并在
useEffect
+
useState
中获取数据:
tsx
// 错误示例 —— 使用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>
  );
}
规则:如果原页面使用了
getServerSideProps
getStaticProps
,数据获取逻辑必须保留在服务端组件中。切勿将其移至
useEffect
中。只有交互式UI代码(useState、onClick等)才应放入客户端组件,并通过props接收数据。