A cutting-edge React template that ships as a static SPA or an SSR server from the same codebase.
| Area | Tools |
|---|---|
| Framework | React 19 + React Compiler, TanStack Start 1.0 (Router + SSR) |
| Data | TanStack Query, wired into route loaders and SSR hydration |
| Build | Vite 8 (Rolldown), Nitro for the server build |
| Language | TypeScript 7 (native compiler) |
| UI | Tailwind CSS 4, shadcn/ui on Base UI, Lucide icons |
| Lint & format | Oxlint (type-aware) + Oxfmt, @shadcn/lint, React Doctor |
| Tests | Vitest 5 + Testing Library, Playwright browser tests for both builds |
| Tooling | pnpm, Knip, Husky + lint-staged, GitHub Actions, Dependabot |
This template is meant to stay on the latest versions of everything, which means:
- It may not suit every workflow, since tools get swapped as better ones appear.
- Some pieces are still young: the native React Compiler and Oxlint's JS plugins (used for
@shadcn/lintand the TanStack rules) are experimental, and Nitro v3 is in beta. - Every update is tested before landing here, but check the known issues section.
You need Node.js 24 (see .node-version) and pnpm (corepack enable picks the version pinned in package.json).
Use GitHub's Use this template button, or clone it and let the setup script rename the project and reset the git history:
git clone https://github.com/FerMPY/react-ts-template.git my-app
cd my-app
pnpm install
node scripts/setup.ts # optional: rename the package and start a fresh git history
pnpm dev # http://localhost:3000TanStack Start renders on the server by default. Run the same app as a pure client-side SPA with the spa mode:
| SSR server (default) | Static SPA | |
|---|---|---|
| Dev | pnpm dev |
pnpm dev:spa |
| Build | pnpm build → .output/ |
pnpm build:spa → dist/client/ |
| Run / deploy | pnpm start, or any Nitro preset |
Any static host (GitHub Pages, S3, Netlify…) |
| Route loaders | Server on first load, then client | Always in the browser |
| Server functions | ✅ | ❌ (no server) |
- Deploy the server anywhere Nitro supports:
NITRO_PRESET=vercel pnpm build(ornetlify,cloudflare,bun, …). The defaultnodepreset runs withpnpm start. - Deploy the SPA under a sub-path by setting
BASE_PATH, e.g.BASE_PATH=/my-repo/ pnpm build:spa. The build writesindex.htmlplus a404.htmlcopy so static hosts fall back to the app for client-side routes. - The included
deploy-pages.ymlworkflow publishes the SPA to GitHub Pages on every push tomain. Enable it under Settings → Pages → Source: GitHub Actions.
| Command | Description |
|---|---|
pnpm dev |
Start the dev server (SSR) |
pnpm dev:spa |
Start the dev server in SPA mode |
pnpm build |
Build the SSR server with Nitro |
pnpm build:spa |
Build the static SPA |
pnpm start |
Run the SSR build |
pnpm preview |
Preview the SSR build (pnpm preview:spa for the SPA) |
pnpm test |
Run tests once (pnpm test:watch to watch) |
pnpm test:e2e |
Build both targets and run the Playwright browser tests |
pnpm check |
Format check, lint, typecheck and unused-code check |
pnpm lint |
Lint with Oxlint (pnpm lint:fix to auto-fix) |
pnpm lint:react |
Scan for React issues with React Doctor |
pnpm lint:unused |
Find unused files, exports and dependencies with Knip |
pnpm format |
Format with Oxfmt (pnpm format:check to only check) |
pnpm typecheck |
Type-check with TypeScript 7 |
.
├── .github/ # CI, GitHub Pages deploy, React Doctor, Dependabot
├── docs/adr/ # Architecture decision records
├── e2e/ # Playwright browser tests for the SSR and SPA builds
├── public/ # Static files, served from BASE_PATH
├── scripts/setup.ts # Turns a clone into your own project
├── src/
│ ├── components/
│ │ └── ui/ # shadcn/ui components (yours to edit)
│ ├── lib/ # cn() class merging, isSpa flag
│ ├── queries/ # TanStack Query options
│ ├── routes/ # File-based routes (__root.tsx is the HTML shell)
│ ├── server/ # Server functions (SSR build only)
│ ├── test/ # Test setup and helpers
│ ├── router.tsx # Router + QueryClient setup
│ ├── routeTree.gen.ts # Generated by TanStack Router, don't edit
│ └── styles.css # Tailwind + shadcn theme tokens
├── AGENTS.md # Entry point for coding agents
├── CODING_STANDARDS.md # Conventions checked in review
├── components.json # shadcn/ui config
├── doctor.config.ts # React Doctor config
├── GLOSSARY.md # Project vocabulary
├── knip.json # Unused-code checker config
├── oxfmt.config.ts # Formatter config
├── oxlint.config.ts # Linter config
├── playwright.config.ts # Browser test config
├── vite.config.ts # Vite + Start + Nitro (SSR and SPA modes)
└── vitest.config.ts # Test config
Import app code with the #/ alias (import { Button } from "#/components/ui/button"). It's a standard package.json imports entry, so Vite, Vitest and Node resolve it without plugins.
The React Compiler memoizes components and hooks at build time, so you don't need useMemo, useCallback or memo for performance. It runs through oxc-transform-react, Oxc's native port of the compiler, enabled with react({ compiler: true }) in vite.config.ts. It's much faster than the Babel plugin, produces the same output (this template's bundles were byte-identical), and needs no Babel at all.
@vitejs/plugin-react still marks the native compiler as experimental. To switch back to the Babel version:
pnpm remove oxc-transform-react
pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler// vite.config.ts
import babel from "@rolldown/plugin-babel";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
// in plugins, replace react({ compiler: true }) with:
react(),
babel({ presets: [reactCompilerPreset()] }),Everything runs on the Oxc toolchain, which is much faster than ESLint + Prettier:
- Oxlint with type-aware rules (via
oxlint-tsgolint), React, React Compiler, jsx-a11y, import and Vitest rules. Seeoxlint.config.ts. - @shadcn/lint keeps the design system consistent: it stops raw palette colors, arbitrary values and restyling of shadcn components, and its errors suggest the right variant, size or token. That makes it especially useful when coding agents write UI.
- TanStack Query and Router ESLint plugins run inside Oxlint as JS plugins.
- Oxfmt formats code with Prettier-compatible output, and also sorts imports, Tailwind classes and
package.json. - React Doctor checks for React anti-patterns, accessibility and security issues (
pnpm lint:react), and comments on every PR through its GitHub Action. - Knip finds unused files, exports and dependencies (
pnpm lint:unused, part ofpnpm check), the kind of leftovers refactors and coding agents leave behind.
A pre-commit hook (Husky + lint-staged) fixes and formats staged files. In VS Code, install the recommended extensions (Oxc, TypeScript 7 and Tailwind CSS IntelliSense) to get format-on-save and inline lint errors.
Vitest runs with jsdom and Testing Library. Test files sit next to the code as *.test.tsx.
renderWithRouter renders a component inside an in-memory TanStack Router, so <Link> and router hooks work. Add withQueryClient for components that use TanStack Query:
import { screen } from "@testing-library/react";
import { renderWithRouter } from "#/test/render-with-router";
import { Counter } from "./counter";
it("increments", async () => {
const { user } = await renderWithRouter(Counter, { withQueryClient: true });
await user.click(screen.getByRole("button"));
expect(screen.getByRole("button")).toHaveTextContent("1");
});pnpm test:e2e builds both targets and runs Playwright against each: the SSR server, and the SPA hosted under a sub-path the way GitHub Pages serves it. Every test also fails on console errors, so hydration mismatches and broken deep links get caught. Install the browser once with pnpm exec playwright install chromium.
AGENTS.md is the entry point for agents (CLAUDE.md imports it). It points to three short docs, an idea borrowed from Matt Pocock:
GLOSSARY.md: the project's vocabulary, with synonyms to avoid. Add your domain's terms as the app grows.docs/adr/: why the choices that look surprising were made.CODING_STANDARDS.md: the judgement calls reviews check. Anything mechanical is a lint rule instead.
@shadcn/lint and React Doctor explain how to fix each problem they find, so agents can correct their own code by running pnpm check and pnpm lint:react.
- SPA hydration: in SPA mode, the root layout renders only after hydration to work around TanStack/router#8473. Remove the
ClientOnlywrapper insrc/routes/__root.tsxonce it's fixed. - GitHub Pages deep links are served from
404.html, so they work in the browser but return HTTP 404 (not ideal for SEO). Use the SSR build if that matters. - TypeScript 7 has no JavaScript API yet. Tools that need it (typescript-eslint, ts-morph…) won't run. If you add one, install TypeScript 6 next to it:
"typescript": "npm:@typescript/typescript6@^6.0.2".
- State: Zustand, Jotai, or TanStack Store
- Forms: TanStack Form
- Database: Drizzle or Prisma (SSR mode)
- Auth: Better Auth (SSR mode)
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Made with ❤️ by Fernando Mendoza