AgentSkillsCN

legacy-migration

引导从旧版 Next.js、React 以及前端系统向项目现代化的功能驱动架构迁移。适用于从 Pages Router 迁移到 App Router、将 Redux 迁移至 Zustand/React Query、将手动类型转换为 Zod Schema、将单体式代码库重构为功能模块,或升级类组件与旧有模式时使用。

SKILL.md
--- frontmatter
name: legacy-migration
description: "Guides migration of legacy Next.js, React, and frontend systems to the project's modern feature-based architecture. Activates when refactoring from Pages Router to App Router, migrating Redux to Zustand/React Query, converting manual types to Zod schemas, restructuring monolithic codebases into feature modules, or upgrading class components and legacy patterns."

Legacy System Migration

This skill guides the migration of legacy frontend systems to this project's modern architecture: Next.js 16 App Router, feature-based modules, Zod schemas, SSR-first data fetching, and Tailwind CSS.

IMPORTANT: Migration is incremental, not big-bang. Migrate one feature at a time. Old and new code can coexist during transition. Always start by analyzing the legacy code thoroughly before touching anything.

Migration Philosophy

  1. Incremental, not big-bang — Migrate one feature at a time. The old and new can coexist.
  2. Schema-first — Start every migration by defining Zod schemas for the data. This forces clarity about the data model before touching UI.
  3. Test before and after — Write tests for legacy behavior before migrating. Verify they pass with the new code.
  4. Feature boundaries first — Group related code into a feature module before refactoring internals.
  5. Delete aggressively — Once migrated, remove the old code. Don't leave commented-out legacy code.
  6. Preserve behavior — Functional parity is mandatory. UX improvements come AFTER migration.
  7. Preserve URLs — Routes should keep the same URLs after migration.

Migration Map

Legacy PatternTarget PatternGuide
Pages Router (pages/)App Router (src/app/)examples/pages-router-to-app-router.md
getServerSideProps / getStaticPropsServer Components + prefetch + <Hydrate>examples/pages-router-to-app-router.md
_app.tsx / _document.tsxlayout.tsx + provider hierarchyexamples/pages-router-to-app-router.md
next/routernext/navigationexamples/pages-router-to-app-router.md
router.queryuseParams() + useSearchParams()examples/pages-router-to-app-router.md
router.eventsusePathname() in useEffectexamples/pages-router-to-app-router.md
Redux / Context API (server data)React Query (useQuery, useMutation)examples/state-management-migration.md
Redux / Context API (UI state)Zustand in stores/[name]-store.tsexamples/state-management-migration.md
Axios / manual fetchapiClient() + serverApiClient()examples/state-management-migration.md
Manual loading/error statesTanStack Query + feedback componentsexamples/state-management-migration.md
Hand-written interfacesZod schemas + z.inferexamples/types-to-zod-schemas.ts
PropTypesZod schemas + z.inferexamples/types-to-zod-schemas.ts
any / untypedZod schema with strict validationexamples/types-to-zod-schemas.ts
Monolithic src/components/Feature-based src/features/examples/monolithic-to-feature-based.md
CSS Modules / Styled Components / SCSSTailwind CSS v4 via cn()See styling section below
Inline stylesTailwind CSS v4 via cn()See styling section below
Class componentsFunctional components + hooksSee component section below
HOCs (Higher-Order Components)Custom hooks or compositionSee component section below
Render propsCustom hooksSee component section below
return null on empty dataEmptyState from @/features/feedbackSee component section below
Manual loading/error UILoadingState / ErrorState from @/features/feedbackSee component section below
Hardcoded test dataMock factories (createMock[Entity])See testing section below
Global state for everythingReact Query (server) + Zustand (client)examples/state-management-migration.md
No API envelopeApiResponse<T> envelopeSee API section below

Migration Decision Tree: State Management

code
What kind of state is this?
├── Server/API data (tasks, users, etc.)
│   ├── Was in Redux → Migrate to React Query (useQuery/useMutation)
│   ├── Was in Context → Migrate to React Query
│   ├── Was in local state (useState + useEffect + fetch) → Migrate to React Query
│   └── Was fetched with Axios/SWR → Replace with apiClient() + React Query
├── Client-only UI state (sidebar open, modal visible, filters)
│   ├── Shared across components → Zustand store in stores/[name]-store.ts
│   └── Local to one component → useState (keep as-is)
└── Unsure?
    └── If it comes from an API → React Query. Otherwise → Zustand or useState.

Migration Sequence (recommended)

For a complete feature migration, follow this order:

code
1. Analyze           — Read ALL legacy code, identify patterns
2. Schemas & Types   — Define Zod schemas, infer types
3. API Layer         — Migrate endpoints to route handlers + ApiResponse<T>
4. Data Fetching     — Replace manual fetch with apiClient/serverApiClient
5. State Management  — Replace Redux/Context with React Query + Zustand
6. Feature Structure — Move files into src/features/[name]/ structure
7. Components        — Migrate to functional, Tailwind, shadcn/ui
8. Pages             — Convert to thin App Router pages + SSR-first
9. Tests             — Create mock factories, write unit + E2E tests
10. Barrel Export    — Create index.ts with full public API
11. Navigation       — Add nav item to shell/constants/navigation.ts
12. Cleanup          — Delete legacy files, unused deps, dead code
13. Verify           — tsc --noEmit, vitest run, next build

Styling Migration

CSS Modules → Tailwind

tsx
// ❌ Before: CSS Modules
import styles from './Card.module.css';
<div className={styles.card}>
  <h2 className={styles.title}>{title}</h2>
</div>

// ✅ After: Tailwind via cn()
import { cn } from "@/lib/utils";
<div className={cn("rounded-lg border bg-card p-6 shadow-sm")}>
  <h2 className={cn("text-lg font-semibold")}>{title}</h2>
</div>

Styled Components → Tailwind

tsx
// ❌ Before: Styled Components
const StyledButton = styled.button`
  background: blue;
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
`;

// ✅ After: shadcn/ui Button
import { Button } from "@/components/ui/button";
<Button variant="default">Click me</Button>

Inline Styles → Tailwind

tsx
// ❌ Before: inline styles
<div style={{ display: 'flex', gap: '8px', padding: '16px' }}>

// ✅ After: Tailwind
<div className="flex gap-2 p-4">

Component Migration

Class Component → Functional

tsx
// ❌ Before: Class component
class TaskList extends React.Component {
  state = { tasks: [], loading: true };
  componentDidMount() { this.fetchTasks(); }
  render() { ... }
}

// ✅ After: Functional with React Query
"use client";
import { useTasks } from "@/features/tasks";
import { ErrorState, LoadingState, EmptyState } from "@/features/feedback";

function TaskList() {
  const { data: tasks, isLoading, error } = useTasks();
  if (error) return <ErrorState error={error} />;
  if (isLoading) return <LoadingState />;
  if (!tasks?.length) return <EmptyState title="No tasks" />;
  return <ul>...</ul>;
}

HOC → Custom Hook

tsx
// ❌ Before: HOC
const withAuth = (Component) => (props) => {
  const user = useContext(AuthContext);
  if (!user) return <Redirect to="/login" />;
  return <Component {...props} user={user} />;
};

// ✅ After: Hook + middleware protection
// Middleware handles auth redirect — no need for HOC
// Component just uses the session:
import { useSession } from "next-auth/react";

function Dashboard() {
  const { data: session } = useSession();
  // session is guaranteed by middleware
}

next/router → next/navigation

tsx
// ❌ Before: Pages Router
import { useRouter } from "next/router";
const router = useRouter();
const { id } = router.query;
router.push("/tasks");

// ✅ After: App Router
import { useRouter, useParams } from "next/navigation";
const router = useRouter();
const { id } = useParams();
router.push("/tasks");

API Migration

Manual API → ApiResponse envelope

typescript
// ❌ Before: inconsistent response shapes
app.get('/api/tasks', (req, res) => {
  res.json(tasks);  // raw array
});
app.get('/api/tasks/:id', (req, res) => {
  res.json({ task });  // wrapped differently
});

// ✅ After: consistent ApiResponse<T> envelope
export async function GET() {
  try {
    const tasks = await db.tasks.findMany();
    return Response.json({
      data: tasks,
      error: null,
      meta: { total: tasks.length },
    });
  } catch {
    return Response.json(
      { data: null, error: { message: "Failed to fetch tasks", code: "FETCH_ERROR" } },
      { status: 500 }
    );
  }
}

getServerSideProps → Server Component + Prefetch

tsx
// ❌ Before: getServerSideProps
export async function getServerSideProps(context) {
  const tasks = await fetchTasks(context.query.filter);
  return { props: { tasks } };
}
export default function TasksPage({ tasks }) {
  return <TaskList tasks={tasks} />;
}

// ✅ After: Server Component + prefetch + Hydrate
import { Hydrate } from "@/lib/hydrate";
import { TaskList, prefetchTasks } from "@/features/tasks";

export default async function TasksPage() {
  await prefetchTasks();
  return (
    <Hydrate>
      <TaskList />
    </Hydrate>
  );
}

Testing Migration

Hardcoded data → Mock factories

typescript
// ❌ Before: hardcoded inline
const task = { id: '1', title: 'Test', status: 'pending' };

// ✅ After: mock factory with overrides
import { createMockTask } from "../mocks/task.mock";
const task = createMockTask({ status: "done" });

Critical Checks After Migration

  • No pages/ directory remnants (fully on App Router)
  • No getServerSideProps / getStaticProps / getInitialProps
  • No _app.tsx / _document.tsx
  • No next/router imports (use next/navigation)
  • No Redux / Context for server state (use React Query)
  • No manual TypeScript interfaces for data (use Zod z.infer)
  • No CSS Modules / Styled Components / SCSS (use Tailwind)
  • No class components
  • No HOCs for auth/data (use hooks + middleware)
  • No return null for empty data (use EmptyState)
  • No hardcoded test data (use mock factories)
  • All features have barrel exports (index.ts)
  • All pages are thin (prefetch + Hydrate + render)
  • All data-fetching components handle empty/error/loading states
  • Navigation added to shell if needed
  • tsc --noEmit passes
  • vitest run passes
  • next build succeeds

DO NOT

  • DO NOT do a big-bang rewrite — migrate one feature at a time.
  • DO NOT change behavior during migration — preserve functional parity first.
  • DO NOT leave commented-out legacy code — delete it completely.
  • DO NOT hand-write TypeScript interfaces — always use Zod schemas + z.infer.
  • DO NOT put server data in Zustand — use React Query for API/server data.
  • DO NOT forget to delete old files after migration — clean up completely.
  • DO NOT skip verification after each feature migration — run tsc, vitest, and next build.
  • DO NOT skip the analysis step — read ALL legacy code before touching anything.
  • DO NOT migrate without creating mock factories — tests need them.
  • DO NOT return null for empty data — use EmptyState from @/features/feedback.