Minimal React Interop Shell Strategy
Context: Supporting a full modern meta-framework (like TanStack Start or Next.js App Router) entirely through Vox compiler code generation poses a high maintenance burden. Frameworks frequently change their routing shapes, SSR boundaries, and file conventions.
This document explores a 90-95% maintainable shell approach. The goal is to provide Vox users with the full power of the React ecosystem (specifically v0 component generation) without the Vox codebase having to carry the weight of being a full Next.js or TanStack Start compiler.
1. The Core Philosophy: Vox as a Component Engine, Not an App Bundler
The central realization is that Vox does not need to own the frontend build process or route tree generation.
To support the best features of modern React, Vox should compile its UI declarations down to primitive, framework-agnostic React components, and expose data fetching as standard HTTP/RPC clients. The target framework (whether Next.js, TanStack, or Vite SPA) simply imports and mounts these primitives.
Why this is highly maintainable:
- React components are stable: The way to write a functional React component hasn't fundamentally changed in years.
- Routing is volatile: File-based routing conventions (Next.js
page.tsxvs TanStack.route.tsx) change rapidly. - v0 Dependencies: v0.dev generates pure React + Tailwind (typically shadcn/ui). This relies on standard components, not specific routing layers.
2. The "90% Shell" Architecture
Instead of Vox generating __root.tsx, routes.ts, and full TanStack configurations, we define a strict boundary:
A. The Presentation Layer (Vox Path C → Pure React)
When a user writes a Path C component:
// vox:skip
component Sidebar() {
view: <div class="sidebar">...</div>
}
Vox compiles this into a pure .tsx file exporting a React functional component. It has zero knowledge of whether it will be rendered by Next.js or TanStack Start.
B. The Interop Layer (Islands & v0)
The @island and @v0 declarations tell Vox: "I am importing an external React component."
Vox simply treats these as standard ES module imports in the generated TypeScript. This allows 100% compatibility with v0.dev because a v0 component is just a React island.
C. The Data Layer (Server Functions → Typed RPC)
Instead of hardcoding @query to TanStack's createServerFn or Next.js's "use server" actions, Vox compiles @query and @mutation into two halves:
- Backend: An Axum JSON HTTP endpoint.
- Frontend: A generated, framework-agnostic typed fetch client (e.g.,
voxClient.fetchPosts()).
If a user is using TanStack Query, they wrap it: useQuery({ queryFn: () => voxClient.fetchPosts() }). If they are using Next.js Server Components, they await it directly.
D. The Routing Layer (Abstract Route Maps)
Instead of generating a complex TanStack Route Tree or Next.js App directory, the routes { } block in Vox generates a simple, abstract JSON / TypeScript Route Manifest.
// Generated by Vox
export const routes = [
{ path: "/", component: Home, loader: voxClient.getHomeData },
{ path: "/posts/:id", component: PostDetail, loader: voxClient.getPostData }
];
The Framework Adapter (The 10% the user/template owns): We provide official, tiny "glue" templates for Next.js or TanStack.
- A TanStack template consumes this JSON map and feeds it to
createRouter. - A Next.js template uses a catch-all route
app/[[...slug]]/page.tsxthat consumes this map to render the right component.
3. Comparing the Deep Integration (Previous Plan) vs. the Shell Approach
| Feature | Deep Integration (TanStack Specific) | Minimal Shell (Framework Agnostic) |
|---|---|---|
routes { } output | Highly specific virtual file routes (__root.tsx, index.route.tsx) | Abstract Route Manifest (routes.manifest.ts) |
@query output | @tanstack/react-start createServerFn() | Framework-agnostic typed fetch client |
| Scaffold Files | Compiler generates vite.config.ts, package.json, etc. | Compiler just generates dist/ components. User uses standard CLI (e.g., pnpm create next-app) |
| v0 Support | Fully supported | Fully supported |
| Maintenance Burden | Very High (Must track TanStack API changes, Vite plugin changes) | Very Low (React functional components and fetch are incredibly stable) |
| Flexibility | Locked to TanStack Start | User can drop Vox output into Next.js, Remix, or TanStack |
4. Conclusion & Recommendation
The previous implementation plan describes a Deep Integration. It is powerful but brittle. If TanStack Start changes its file routing conventions (which it does frequently), the Vox compiler breaks.
The Minimal Shell Strategy is exactly the 90-95% solution. It isolates the heavy lifting (React rendering, TypeScript types, v0 layout) from the volatile framework mechanics (routing, bundlers, SSR context).
To achieve this:
- Keep the Path C → React generation.
- Keep the
@islandinterop for v0.dev. - Pivot routing: Change the
routesblock codegen to output an abstract array of route objects instead of a rigid framework-specific tree. - Pivot server functions: Change
@queryto generate a standard typed fetch SDK rather than tying directly tocreateServerFn.
This allows Vox to remain maintainable while giving developers the full power of the modern frontend ecosystem.