A modern single-page application built with React 19 and TanStack Router, designed for building type-safe, performant user interfaces with an exceptional developer experience.
We're running React 19 with TanStack Router for type-safe routing, Jotai for atomic state management, shadcn/ui components styled with Tailwind CSS v4, and Better Auth for authentication. The whole thing is bundled with Vite because life's too short for slow builds.
- React 19: The latest and greatest, with concurrent features that make your app feel snappy
- TanStack Router: Type-safe routing that catches broken links at compile time, not user complaint time
- Jotai: Bottom-up state management that scales from simple counters to complex application state
- shadcn/ui: Copy-paste components that you actually own, built on Radix UI primitives
- Tailwind CSS v4: Utility-first CSS with lightning-fast builds and zero runtime
- Better Auth: Modern authentication that doesn't make you want to cry
- Vite: Fast development server and optimized production builds
- TypeScript: Because runtime errors are so 2019
app/
├── components/ # Reusable UI components
│ ├── error.tsx # Error boundary component
│ ├── layout.tsx # Layout wrapper component
│ └── index.ts # Component exports
├── lib/ # Utility functions and configurations
│ ├── auth.ts # Authentication setup
│ ├── routeTree.gen.ts # Generated route tree (auto-generated)
│ ├── store.ts # Jotai atoms and global state
│ └── utils.ts # Utility functions
├── public/ # Static assets
│ ├── favicon.ico # App favicon
│ └── site.manifest # PWA manifest
├── routes/ # Page components and route definitions
│ ├── __root.tsx # Root layout component
│ ├── index.tsx # Home page
│ └── about.tsx # About page
├── styles/ # Global styles
│ └── globals.css # Global CSS and theme variables
├── index.html # HTML template
├── index.tsx # Application entry point
├── global.d.ts # TypeScript global declarations
├── tailwind.config.css # Tailwind CSS v4 configuration
├── vite.config.ts # Vite configuration
└── components.json # shadcn/ui configuration
# Install dependencies (from monorepo root)
bun install
# Start development server
bun app:dev
# or
bun --filter @repo/app dev
# Build for production
bun app:build
# or
bun --filter @repo/app buildThe app will be available at http://localhost:5173 (Vite's default port).
# Development
bun dev # Start dev server with hot reload
bun build # Build for production to dist/
bun preview # Preview production build locally
bun typecheck # Run TypeScript type checking
bun lint # Lint code with ESLint
# Testing
bun test # Run tests
bun test:watch # Run tests in watch mode
bun test:coverage # Generate coverage reportRoutes are defined in the routes/ directory using TanStack Router's file-based routing:
// routes/dashboard.$orgId.tsx
export const Route = createFileRoute("/dashboard/$orgId")({
component: Dashboard,
loader: async ({ params }) => {
// Pre-load data before component renders
return await fetchOrganization(params.orgId);
},
validateSearch: z.object({
tab: z.enum(["overview", "members", "settings"]).optional(),
}),
});routes/__root.tsx- Root layout with navigation and error boundariesroutes/index.tsx- Landing pageroutes/about.tsx- About pageroutes/(auth)/- Authentication group (login, signup, forgot password)routes/(dashboard)/- Dashboard group (protected routes)
import { Link, useNavigate } from "@tanstack/react-router";
// Declarative navigation
<Link to="/dashboard/$orgId" params={{ orgId: "123" }}>
Dashboard
</Link>;
// Programmatic navigation
const navigate = useNavigate();
navigate({ to: "/dashboard/$orgId", params: { orgId: "123" } });// lib/store.ts
import { atom } from "jotai";
// Simple atom
export const countAtom = atom(0);
// Derived atom
export const doubleCountAtom = atom((get) => get(countAtom) * 2);
// Async atom
export const userAtom = atom(async () => {
const response = await fetch("/api/user");
return response.json();
});import { useAtom } from "jotai";
import { countAtom } from "@/lib/store";
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}We use the shared @repo/ui package for UI components:
import { Button, Card, Dialog } from "@repo/ui";
function MyComponent() {
return (
<Card>
<Button variant="primary" size="lg">
Click me
</Button>
</Card>
);
}# Add a new shadcn/ui component to the shared package
bun ui:add dialog
# List available components
bun ui:list
# Install essential components
bun ui:essentialsThe tailwind.config.css file uses the new CSS-based configuration:
@import "tailwindcss";
/* Content paths */
@source "./routes/**/*.{ts,tsx}";
@source "./components/**/*.{ts,tsx}";
@source "../../packages/ui/components/**/*.{ts,tsx}";
/* Custom dark mode variant */
@custom-variant dark (&:is(.dark *));
/* Theme configuration */
@theme inline {
--color-primary: var(--primary);
--color-background: var(--background);
/* ... more theme tokens */
}Our design system uses CSS custom properties:
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
/* Light mode colors */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
/* Dark mode colors */
}Authentication is configured in lib/auth.ts:
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL,
});
export const { useSession, signIn, signOut, signUp } = authClient;// routes/(dashboard)/_layout.tsx
export const Route = createFileRoute("/(dashboard)")({
beforeLoad: async ({ context }) => {
const session = await authClient.getSession();
if (!session) {
throw redirect({ to: "/login" });
}
return { session };
},
});We use tRPC for end-to-end type safety:
// lib/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@repo/api";
export const api = createTRPCReact<AppRouter>();function UserProfile() {
const { data: user, isLoading } = api.user.me.useQuery();
if (isLoading) return <Skeleton />;
if (!user) return <div>User not found</div>;
return <div>Welcome, {user.name}!</div>;
}Routes are automatically code-split by TanStack Router:
// Lazy load heavy components
const Dashboard = lazy(() => import("./Dashboard"));
export const Route = createFileRoute("/dashboard")({
component: Dashboard,
});// Use optimized image loading
import { Image } from "@repo/ui";
<Image
src="/hero.jpg"
alt="Hero"
loading="lazy"
sizes="(max-width: 768px) 100vw, 50vw"
/>;// components/Button.test.tsx
import { render, screen } from "@testing-library/react";
import { Button } from "./Button";
test("renders button with text", () => {
render(<Button>Click me</Button>);
expect(screen.getByText("Click me")).toBeInTheDocument();
});// routes/Dashboard.test.tsx
import { render } from "@testing-library/react";
import { createMemoryRouter } from "@tanstack/react-router";
test("dashboard loads organization data", async () => {
const router = createMemoryRouter({
routes: routeTree,
initialLocation: "/dashboard/123",
});
render(<RouterProvider router={router} />);
// Test implementation
});bun buildThis creates an optimized build in dist/ with:
- Minified code for smaller bundle sizes
- Asset hashing for optimal caching
- Tree shaking to remove unused code
- Code splitting for faster loading
# .env.local
VITE_API_URL=http://localhost:3000
VITE_APP_NAME="My App"
VITE_PUBLIC_URL=http://localhost:5173Note: Vite requires VITE_ prefix for client-side variables.
Build fails with TypeScript errors
# Check types without building
bun typecheckRoute not found
- Verify file naming follows TanStack Router conventions
- Check that route exports are correct
- Ensure route tree is properly generated
Styles not applying
- Check that the class is included in Tailwind's content paths
- Verify CSS variables are defined
- Clear browser cache
API calls failing
- Ensure the API server is running
- Check environment variables
- Verify tRPC types are up to date
Slow initial load
- Check bundle size with
bun analyze - Implement route-based code splitting
- Lazy load heavy components
Excessive re-renders
- Use React DevTools Profiler
- Implement proper memoization
- Check Jotai atom dependencies
- Keep components focused - single responsibility
- Use TypeScript for all components
- Implement proper error boundaries
- Add loading states for async operations
- Consider accessibility from the start
- Keep atoms small and focused
- Use derived atoms for computed values
- Avoid prop drilling with proper atom scope
- Clean up subscriptions in useEffect
- Lazy load routes and heavy components
- Optimize bundle size with tree shaking
- Use proper caching strategies
- Implement virtual scrolling for long lists
When adding new features:
- Follow existing patterns for consistency
- Add proper TypeScript types
- Include unit tests
- Update documentation
- Test accessibility
- Verify mobile responsiveness
Remember: The best code is the code that doesn't need to be written. The second best is code that's easy to delete. 🎯
- React 19 Documentation
- TanStack Router Documentation
- Jotai Documentation
- shadcn/ui Documentation
- Tailwind CSS v4 Documentation
- Better Auth Documentation
- Vite Documentation
- tRPC Documentation
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." 💡
— Martin Fowler