Already have a Vite-powered React SPA? This guide walks you through migrating to FUNSTACK Static to unlock React Server Components and improved performance.
Migrating from a standard Vite React SPA to FUNSTACK Static involves:
The good news: your existing client components work as-is. You can migrate incrementally.
Add FUNSTACK Static to your existing project:
npm install @funstack/static
Or with pnpm:
pnpm add @funstack/static
Hint: at this point, you can add FUNSTACK Static knowledge to your AI agents. Run npx -p @funstack/static funstack-static-skill-installer (or npx skills add uhyo/funstack-static if you use skills CLI) to add the skill. Then you can ask your AI assistant for help with the migration!
Modify your vite.config.ts to add the FUNSTACK Static plugin:
import funstackStatic from "@funstack/static";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
funstackStatic({
root: "./src/Root.tsx",
app: "./src/App.tsx",
}),
react(),
],
});
Note that the existing React plugin remains; FUNSTACK Static works alongside it.
The Root component replaces your index.html file. Create src/Root.tsx:
// src/Root.tsx
import type React from "react";
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
{/* Add your existing <head> content here */}
</head>
<body>
<div id="root">{children}</div>
</body>
</html>
);
}
Move any content from your index.html <head> section into this component.
Your existing main.tsx or index.tsx likely looks like this:
// Before: src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
With FUNSTACK Static, you no longer need this file. Instead, your App.tsx becomes the entry point and is automatically rendered as a Server Component:
// After: src/App.tsx
import "./index.css";
import { HomePage } from "./pages/HomePage";
export default function App() {
return <HomePage />;
}
You can delete main.tsx - FUNSTACK Static handles the rendering.
In a standard Vite SPA, all components are client components. With FUNSTACK Static, components are Server Components by default.
As a starting point, it is possible to mark the App component as a client component by adding "use client" at the top:
// src/App.tsx
"use client";
// ...rest of the code
This makes the entire app a client component, similar to your previous SPA.
However, to take advantage of Server Components, you should incrementally convert components to Server Components. This can be done by removing "use client" from components that don't need it and instead adding it only to components that require client-side interactivity.
Note: you only need to add "use client" to components that are directly imported by Server Components. This marks the boundary between server and client code. Components imported by other client components don't need the directive.
// src/components/Counter.tsx
"use client";
import { useState } from "react";
import { Button } from "./Button"; // No "use client" needed in Button.tsx
export function Counter() {
const [count, setCount] = useState(0);
return <Button onClick={() => setCount(count + 1)}>Count: {count}</Button>;
}
In this example, Counter.tsx needs "use client" because it's imported by a Server Component. But Button.tsx doesn't need it since it's only imported by Counter, which is already a client component.
A component is a client component if it:
useState, useEffect, useContext, etc.)onClick, onChange, etc.)window, document, localStorage, etc.)If you're using React Router or another client-side router, you have two options:
You can continue using your existing router as a client component:
// src/App.tsx
import { ClientRouter } from "./ClientRouter";
export default function App() {
return <ClientRouter />;
}
// src/ClientRouter.tsx
"use client";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home } from "./pages/Home";
import { About } from "./pages/About";
export function ClientRouter() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}
For better performance, consider migrating to FUNSTACK Router which is a standalone router for SPAs but still integrates well with Server Components.
npm install @funstack/router
// src/App.tsx
import { Router } from "@funstack/router";
import { route } from "@funstack/router/server";
import { Home } from "./pages/Home";
import { About } from "./pages/About";
const routes = [
route({ path: "/", component: <Home /> }),
route({ path: "/about", component: <About /> }),
];
export default function App() {
return <Router routes={routes} />;
}
After migration, you can remove:
src/main.tsx (or src/index.tsx) - no longer neededindex.html - replaced by Root.tsxImport global CSS in your App.tsx:
// src/App.tsx
import "./index.css";
import "./global.css";
export default function App() {
// ...
}
Wrap client-side providers in a client component:
// src/Providers.tsx
"use client";
import { ThemeProvider } from "./ThemeContext";
import { AuthProvider } from "./AuthContext";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
);
}
// src/App.tsx
import { Providers } from "./Providers";
import { HomePage } from "./pages/HomePage";
export default function App() {
return (
<Providers>
<HomePage />
</Providers>
);
}
Client-side environment variables still work the same way with the VITE_ prefix:
// In client components
const apiUrl = import.meta.env.VITE_API_URL;
Run the development server:
npm run dev
Check that:
Build for production:
npm run build
Your static files will be generated in dist/public, ready for deployment.
Add "use client" at the top of components that use React hooks.
Components using browser APIs must be client components. Add "use client" directive.
Ensure CSS imports are in App.tsx or in client components that are actually rendered.