Agent skill to migrate React+Vite+Tailwind apps to Next.js 16
SaferSkills independently audited vite-2-next (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill helps AI agents:
proxy.ts)This migration preserves UI components while transforming the underlying architecture to leverage SSR, streaming, and Server Actions.
Confirm the project is a React application by checking:
react and react-dom in package.json dependenciesvite.config.ts/js, craco.config.js, or webpack.config.jssrc/ directory with components or root-level component filesIf none found, STOP — this skill does not apply.
Detect the package manager from the lockfile:
| Lockfile | Manager | Install | Uninstall | Execute |
|---|---|---|---|---|
pnpm-lock.yaml | pnpm | pnpm add | pnpm remove | pnpm dlx |
yarn.lock | yarn | yarn add | yarn remove | yarn dlx |
bun.lockb / bun.lock | bun | bun add | bun remove | bunx |
package-lock.json or none | npm | npm install | npm uninstall | npx |
Detect routing system:
react-router-dom in dependencies@tanstack/react-routerwouter| React/Vite Pattern | Next.js 16 App Router Equivalent |
|---|---|
Client-side routing (<BrowserRouter>) | File-system routing (app/ directory) |
useEffect data fetching | Server Components with async/await |
useState for server data | Server Actions, useActionState |
index.html with <div id="root"> | app/layout.tsx with RootLayout |
import.meta.env.VITE_* | process.env.NEXT_PUBLIC_* |
middleware.ts | proxy.ts (renamed in Next.js 16) |
| Vite plugins for CSS/assets | Built-in optimization + Turbopack |
| Client-only rendering | Hybrid rendering (Server + Client Components) |
Generally Compatible: Material-UI, Ant Design, Chakra UI, Radix UI, Headless UI, Zustand, Jotai, Valtio, React Hook Form, Formik, Framer Motion, date-fns, lodash, clsx
Requires Refactoring: Redux/Redux Toolkit (wrap in useRef provider), React Query/TanStack Query (configure for SSR), Apollo Client (SSR setup), SWR (configure for SSR)
Incompatible / Not Needed: react-router-dom, Vite plugins, react-helmet, dotenv
See references/compatibility.md for the full library compatibility matrix.
Categorize components by type:
CRITICAL: Do not delete or modify the existing React project yet. Create Next.js in a parallel directory for gradual migration and easy rollback.
npx create-next-app@latest [project-name]-nextjs --typescript --tailwind --app --src-dir --import-alias "@/*"Node.js 20.9.0 or later is required for Next.js 16.
[package-manager] add @radix-ui/react-slot class-variance-authority clsx tailwind-merge
[package-manager-exec] shadcn@latest init
[package-manager] add lucide-react zod react-hook-form @hookform/resolversSee references/tailwind-migration.md for detailed examples.
| React Router | Next.js App Router |
|---|---|
<Route path="/" element={<Home />} /> | app/page.tsx |
<Route path="/about" element={<About />} /> | app/about/page.tsx |
<Route path="/blog/:slug" element={<Post />} /> | app/blog/[slug]/page.tsx |
<Route path="/blog/*" element={<Blog />} /> | app/blog/[...slug]/page.tsx |
<Outlet /> in layout | {children} in layout.tsx |
<Navigate to="/" /> | redirect('/') from next/navigation |
Next.js 16:paramsandsearchParamsare async Promises — alwaysawaitthem.
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}See references/routing-patterns.md for complex routing scenarios.
Before (React):
// src/App.tsx
function App() {
return (
<ThemeProvider>
<BrowserRouter>
<Header />
<Routes>{/* routes */}</Routes>
<Footer />
</BrowserRouter>
</ThemeProvider>
);
}After (Next.js 16):
// src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/providers/theme-provider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "My App",
description: "App description",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}Conversion rules:
useState / useEffect / event handlers → Add 'use client''use client' ProviderReact.lazy → Replace with next/dynamic<a> for internal links → Replace with next/linkuseNavigate → Replace with useRouter from next/navigationSee references/component-patterns.md for detailed examples.
Before (React):
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}After (Next.js 16 — Server Component):
async function UserProfile({ userId }: { userId: string }) {
const user = await fetch(`https://api.example.com/users/${userId}`).then(
(res) => res.json(),
);
return <div>{user.name}</div>;
}Everything is dynamic by default. To opt into caching, use the "use cache" directive:
"use cache";
export default async function ProductsPage() {
const products = await fetch("https://api.example.com/products").then((r) =>
r.json(),
);
return <ProductList products={products} />;
}The oldfetch(url, { next: { revalidate: 60 } })andcache: 'force-cache'patterns are deprecated in Next.js 16. Use"use cache"instead.
// app/posts/create/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await fetch("https://api.example.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
revalidatePath("/posts");
redirect("/posts");
}
// app/posts/create/page.tsx
import { createPost } from "./actions";
export default function CreatePostPage() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Submit</button>
</form>
);
}See references/data-fetching.md for comprehensive patterns.
| Vite | Next.js 16 | Scope |
|---|---|---|
import.meta.env.VITE_API_URL | process.env.NEXT_PUBLIC_API_URL | Client + Server |
import.meta.env.VITE_* | process.env.NEXT_PUBLIC_* | Client + Server |
process.env.* (server) | process.env.* | Server only |
See references/environment.md for detailed configuration.
Move src/assets/ → public/. See references/assets.md for optimization.
Context providers must be 'use client'. All global stores (Zustand, Redux, Jotai) must use a useRef-based provider — never module-level singletons — to prevent state leaking between server requests.
See references/state-management.md for detailed patterns.
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET() {
const posts = await db.posts.findMany();
return NextResponse.json(posts);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await db.posts.create({ data: body });
return NextResponse.json(post, { status: 201 });
}Dynamic routeparamsare async in Next.js 16:{ params }: { params: Promise<{ id: string }> }— alwaysawait params.
// proxy.ts ← was middleware.ts in Next.js 13–15
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
return NextResponse.next();
}
export const config = { matcher: ["/dashboard/:path*"] };See references/api-routes.md for comprehensive examples.
import type { NextConfig } from "next";
const config: NextConfig = {
// React Compiler — stable in Next.js 16
reactCompiler: true,
experimental: {
// Turbopack persistent FS cache — faster cold starts
turbopackFileSystemCacheForDev: true,
},
images: {
remotePatterns: [{ protocol: "https", hostname: "**.example.com" }],
},
async redirects() {
return [{ source: "/old-path", destination: "/new-path", permanent: true }];
},
};
export default config;Turbopack is the default bundler in Next.js 16. Custom webpack configs will not work — rewrite for Turbopack or next.config.ts.See references/config-options.md for all available options.
See references/troubleshooting.md for solutions.
Vercel (Recommended):
npm install -g vercel && vercel login && vercelAlternatives: Netlify, Cloudflare Pages (@cloudflare/next-on-pages), AWS Amplify, Docker, Node.js server.
See references/deployment.md for platform-specific guides.
Convert Client Components to Server Components where possible. Use loading.tsx and <Suspense> for streaming. Add generateMetadata for SEO. Use parallel Promise.all() fetching.
See references/optimization.md for advanced techniques.
Do NOT:
useEffect for data fetching in Server Components'use client' to every file by defaultwindow, localStorage, document in Server Componentsnext: { revalidate } or cache: 'force-cache' — use "use cache" directive insteadcookies() or headers() synchronously — they are async in Next.js 16middleware.ts — it is now proxy.ts in Next.js 16useRef-based providersDO:
'use client' only when needednext/image, next/font, next/scriptawait params, await cookies(), await headers()layout, loading, error, not-foundComplexity estimate:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.