Back to skills
SKILL.md
React Vite Project Architect
ASecurityProduction-ready React project structure validation and scaffolding with Vite, featuring modern patterns, TypeScript, and comprehensive tooling. Use when scaffolding, structuring, or architecting react vite projects.
- 9 stars
- 0 votes
- 0 copies
- 2 views
- Added September 8, 2026
Works with
Security analysis
96/100- Installs packages at runtime which could introduce malicious dependencies
npx -y skills add anubhavg-icpl/vibe --skill react-vite-project-architect --agent claude-codeAre you the author of React Vite Project Architect?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-react-vite-project-architect)---
name: react-vite-project-architect
description: Production-ready React project structure validation and scaffolding with Vite, featuring modern patterns, TypeScript, and comprehensive tooling. Use when scaffolding, structuring, or architecting react vite projects.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: project-structure
tags: [react, vite, typescript, tailwindcss, tanstack, zustand]
---
# React Vite Project Architect Mode
You are a React project structure expert. Your role is to validate, scaffold, and improve React application architectures following modern best practices with Vite, TypeScript, and production-ready patterns.
## Core Competencies
### React & Build Tools
- **React 18.x/19.x** - Concurrent rendering, Server Components (with frameworks)
- **Vite 6.x** - Lightning-fast HMR, native ESM, Rollup bundling
- **TypeScript 5.x** - Strict mode, satisfies operator, const type params
- **SWC** - Fast Rust-based transpilation
### State Management
- **Zustand** - Lightweight, hook-based state
- **TanStack Query** - Server state, caching, mutations
- **Jotai** - Atomic state management
- **Redux Toolkit** - Full-featured state (when needed)
### Styling
- **Tailwind CSS 4.x** - Utility-first, JIT compilation
- **CSS Modules** - Scoped styles
- **Styled Components / Emotion** - CSS-in-JS
- **Vanilla Extract** - Zero-runtime CSS-in-TS
### Routing & Data
- **TanStack Router** - Type-safe file-based routing
- **React Router 7** - Traditional routing
- **TanStack Query** - Data fetching & caching
- **Axios / ky** - HTTP clients
### Testing
- **Vitest** - Vite-native test runner
- **Testing Library** - Component testing
- **Playwright** - E2E testing
- **MSW** - API mocking
## Project Structure Patterns
### Feature-Based Structure (Recommended)
```text
my-react-app/
├── public/
│ ├── favicon.ico
│ └── robots.txt
├── src/
│ ├── main.tsx # Entry point
│ ├── App.tsx # Root component
│ ├── vite-env.d.ts # Vite types
│ │
│ ├── app/ # App-level concerns
│ │ ├── providers/ # Context providers
│ │ │ ├── index.tsx # Combined providers
│ │ │ ├── QueryProvider.tsx
│ │ │ └── ThemeProvider.tsx
│ │ ├── router/ # Routing configuration
│ │ │ ├── index.tsx
│ │ │ ├── routes.tsx
│ │ │ └── guards/
│ │ │ └── AuthGuard.tsx
│ │ └── store/ # Global state
│ │ ├── index.ts
│ │ └── slices/
│ │ └── userSlice.ts
│ │
│ ├── features/ # Feature modules
│ │ ├── auth/
│ │ │ ├── index.ts # Public exports
│ │ │ ├── components/
│ │ │ │ ├── LoginForm.tsx
│ │ │ │ ├── RegisterForm.tsx
│ │ │ │ └── LoginForm.test.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useAuth.ts
│ │ │ │ └── useLogin.ts
│ │ │ ├── api/
│ │ │ │ └── authApi.ts
│ │ │ ├── types/
│ │ │ │ └── index.ts
│ │ │ └── utils/
│ │ │ └── validators.ts
│ │ │
│ │ ├── dashboard/
│ │ │ ├── index.ts
│ │ │ ├── components/
│ │ │ ├── hooks/
│ │ │ └── api/
│ │ │
│ │ └── users/
│ │ ├── index.ts
│ │ ├── components/
│ │ │ ├── UserList.tsx
│ │ │ ├── UserCard.tsx
│ │ │ └── UserForm.tsx
│ │ ├── hooks/
│ │ │ └── useUsers.ts
│ │ ├── api/
│ │ │ └── usersApi.ts
│ │ └── types/
│ │ └── index.ts
│ │
│ ├── components/ # Shared UI components
│ │ ├── ui/ # Base components
│ │ │ ├── Button/
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── Button.test.tsx
│ │ │ │ └── index.ts
│ │ │ ├── Input/
│ │ │ ├── Modal/
│ │ │ ├── Card/
│ │ │ └── index.ts
│ │ │
│ │ ├── layout/ # Layout components
│ │ │ ├── Header/
│ │ │ ├── Sidebar/
│ │ │ ├── Footer/
│ │ │ └── PageLayout.tsx
│ │ │
│ │ └── common/ # Reusable patterns
│ │ ├── ErrorBoundary.tsx
│ │ ├── LoadingSpinner.tsx
│ │ └── Suspense.tsx
│ │
│ ├── hooks/ # Shared hooks
│ │ ├── useDebounce.ts
│ │ ├── useLocalStorage.ts
│ │ ├── useMediaQuery.ts
│ │ └── index.ts
│ │
│ ├── lib/ # Third-party config
│ │ ├── axios.ts # API client setup
│ │ ├── queryClient.ts # TanStack Query config
│ │ └── i18n.ts # i18n config
│ │
│ ├── utils/ # Utility functions
│ │ ├── cn.ts # className helper
│ │ ├── formatters.ts
│ │ ├── validators.ts
│ │ └── constants.ts
│ │
│ ├── types/ # Global types
│ │ ├── api.ts
│ │ ├── common.ts
│ │ └── index.ts
│ │
│ ├── assets/ # Static assets
│ │ ├── images/
│ │ ├── fonts/
│ │ └── icons/
│ │
│ └── styles/ # Global styles
│ ├── globals.css
│ └── tailwind.css
│
├── tests/ # E2E tests
│ ├── e2e/
│ │ ├── auth.spec.ts
│ │ └── dashboard.spec.ts
│ └── setup.ts
│
├── .github/
│ └── workflows/
│ └── ci.yml
│
├── index.html # HTML entry
├── vite.config.ts
├── tsconfig.json
├── tsconfig.node.json
├── tailwind.config.ts
├── postcss.config.js
├── eslint.config.js # ESLint flat config
├── prettier.config.js
├── vitest.config.ts
├── playwright.config.ts
├── .env.example
├── .gitignore
├── package.json
└── README.md
```
### Atomic Design Structure (Alternative)
```text
src/
├── components/
│ ├── atoms/ # Basic elements (Button, Input, Text)
│ │ ├── Button/
│ │ ├── Input/
│ │ └── Text/
│ ├── molecules/ # Combinations (SearchInput, Card)
│ │ ├── SearchBar/
│ │ └── FormField/
│ ├── organisms/ # Complex sections (Header, UserTable)
│ │ ├── Header/
│ │ └── UserTable/
│ ├── templates/ # Page layouts
│ │ ├── DashboardLayout/
│ │ └── AuthLayout/
│ └── pages/ # Route components
│ ├── HomePage/
│ └── LoginPage/
```
## Configuration Templates
### package.json
```json
{
"name": "my-react-app",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"prepare": "husky"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@tanstack/react-query": "^5.62.0",
"@tanstack/react-router": "^1.93.0",
"zustand": "^5.0.2",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"tailwind-merge": "^2.6.0",
"date-fns": "^4.1.0",
"zod": "^3.24.1",
"react-hook-form": "^7.54.2",
"@hookform/resolvers": "^3.9.1"
},
"devDependencies": {
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react-swc": "^3.7.2",
"vite": "^6.0.5",
"typescript": "^5.7.2",
"tailwindcss": "^4.0.0",
"@tailwindcss/vite": "^4.0.0",
"postcss": "^8.4.49",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"@eslint/js": "^9.17.0",
"typescript-eslint": "^8.18.2",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.1.0",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"vitest": "^2.1.8",
"@vitest/ui": "^2.1.8",
"@vitest/coverage-v8": "^2.1.8",
"@testing-library/react": "^16.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.5.2",
"jsdom": "^25.0.1",
"msw": "^2.7.0",
"@playwright/test": "^1.49.1",
"husky": "^9.1.7",
"lint-staged": "^15.2.11"
},
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,css}": ["prettier --write"]
}
}
```
### vite.config.ts
```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import tailwindcss from "@tailwindcss/vite";
import { resolve } from "path";
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
"@components": resolve(__dirname, "./src/components"),
"@features": resolve(__dirname, "./src/features"),
"@hooks": resolve(__dirname, "./src/hooks"),
"@lib": resolve(__dirname, "./src/lib"),
"@utils": resolve(__dirname, "./src/utils"),
"@types": resolve(__dirname, "./src/types"),
"@assets": resolve(__dirname, "./src/assets"),
},
},
server: {
port: 3000,
open: true,
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
build: {
outDir: "dist",
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"],
router: ["@tanstack/react-router"],
query: ["@tanstack/react-query"],
},
},
},
},
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./tests/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "tests/"],
},
},
});
```
### tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@features/*": ["./src/features/*"],
"@hooks/*": ["./src/hooks/*"],
"@lib/*": ["./src/lib/*"],
"@utils/*": ["./src/utils/*"],
"@types/*": ["./src/types/*"],
"@assets/*": ["./src/assets/*"]
}
},
"include": ["src", "tests"],
"references": [{ "path": "./tsconfig.node.json" }]
}
```
### tsconfig.node.json
```json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts", "vitest.config.ts", "tailwind.config.ts"]
}
```
### eslint.config.js (Flat Config)
```javascript
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import globals from "globals";
export default tseslint.config(
{ ignores: ["dist", "node_modules", "coverage"] },
{
extends: [js.configs.recommended, ...tseslint.configs.strictTypeChecked],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2022,
globals: globals.browser,
parserOptions: {
project: ["./tsconfig.json", "./tsconfig.node.json"],
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
react,
"react-hooks": reactHooks,
},
settings: {
react: {
version: "detect",
},
},
rules: {
...react.configs.recommended.rules,
...react.configs["jsx-runtime"].rules,
...reactHooks.configs.recommended.rules,
// React rules
"react/prop-types": "off",
"react/display-name": "off",
// TypeScript rules
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
"@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: { attributes: false } }],
},
},
);
```
### prettier.config.js
```javascript
/** @type {import("prettier").Config} */
export default {
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: "es5",
printWidth: 80,
plugins: ["prettier-plugin-tailwindcss"],
tailwindFunctions: ["clsx", "cn"],
};
```
### tailwind.config.ts (Tailwind v4)
```typescript
// Note: Tailwind v4 uses CSS-based configuration
// This file is for editor tooling support
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
primary: {
50: "var(--color-primary-50)",
100: "var(--color-primary-100)",
// ... etc
900: "var(--color-primary-900)",
},
},
fontFamily: {
sans: ["Inter", "sans-serif"],
},
},
},
};
```
### src/styles/tailwind.css (Tailwind v4)
```css
@import "tailwindcss";
/* Theme configuration */
@theme {
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-900: #1e3a8a;
--font-family-sans: "Inter", sans-serif;
--breakpoint-xs: 475px;
}
/* Global styles */
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/* Component classes */
@layer components {
.btn {
@apply inline-flex items-center justify-center rounded-md px-4 py-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50;
}
.btn-primary {
@apply btn bg-primary-600 text-white hover:bg-primary-700;
}
.input {
@apply flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2;
}
}
```
### vitest.config.ts
```typescript
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
import { resolve } from "path";
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./tests/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
exclude: ["node_modules", "dist", "tests/e2e"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "tests/", "**/*.d.ts", "**/*.config.*", "**/types/"],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
},
});
```
### tests/setup.ts
```typescript
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach, beforeAll, afterAll } from "vitest";
import { setupServer } from "msw/node";
import { handlers } from "./mocks/handlers";
// MSW server setup
export const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => {
cleanup();
server.resetHandlers();
});
afterAll(() => server.close());
// Mock window.matchMedia
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
```
### playwright.config.ts
```typescript
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
{
name: "Mobile Chrome",
use: { ...devices["Pixel 5"] },
},
],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});
```
## Code Templates
### Main Entry (src/main.tsx)
```tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { AppProviders } from "@/app/providers";
import { AppRouter } from "@/app/router";
import "@/styles/tailwind.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AppProviders>
<AppRouter />
</AppProviders>
</StrictMode>,
);
```
### App Providers
```tsx
// src/app/providers/index.tsx
import type { ReactNode } from "react";
import { QueryProvider } from "./QueryProvider";
import { ThemeProvider } from "./ThemeProvider";
interface AppProvidersProps {
children: ReactNode;
}
export function AppProviders({ children }: AppProvidersProps) {
return (
<QueryProvider>
<ThemeProvider>{children}</ThemeProvider>
</QueryProvider>
);
}
// src/app/providers/QueryProvider.tsx
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { queryClient } from "@/lib/queryClient";
export function QueryProvider({ children }: { children: ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
```
### TanStack Query Setup
```typescript
// src/lib/queryClient.ts
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes
retry: 1,
refetchOnWindowFocus: false,
},
mutations: {
retry: 0,
},
},
});
```
### Zustand Store
```typescript
// src/app/store/index.ts
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
interface User {
id: string;
name: string;
email: string;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
devtools(
persist(
immer((set) => ({
user: null,
isAuthenticated: false,
login: (user) =>
set((state) => {
state.user = user;
state.isAuthenticated = true;
}),
logout: () =>
set((state) => {
state.user = null;
state.isAuthenticated = false;
}),
})),
{
name: "auth-storage",
partialize: (state) => ({ user: state.user }),
},
),
{ name: "AuthStore" },
),
);
```
### Feature Module Example
```typescript
// src/features/users/index.ts
// Public API for the users feature
export { UserList } from "./components/UserList";
export { UserCard } from "./components/UserCard";
export { useUsers, useUser } from "./hooks/useUsers";
export type { User, CreateUserDto } from "./types";
// src/features/users/types/index.ts
export interface User {
id: string;
name: string;
email: string;
role: "admin" | "user";
createdAt: Date;
}
export interface CreateUserDto {
name: string;
email: string;
role: "admin" | "user";
}
// src/features/users/api/usersApi.ts
import { api } from "@/lib/axios";
import type { User, CreateUserDto } from "../types";
export const usersApi = {
getAll: async (): Promise<User[]> => {
const { data } = await api.get<User[]>("/users");
return data;
},
getById: async (id: string): Promise<User> => {
const { data } = await api.get<User>(`/users/${id}`);
return data;
},
create: async (dto: CreateUserDto): Promise<User> => {
const { data } = await api.post<User>("/users", dto);
return data;
},
delete: async (id: string): Promise<void> => {
await api.delete(`/users/${id}`);
},
};
// src/features/users/hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { usersApi } from "../api/usersApi";
import type { CreateUserDto } from "../types";
const USERS_KEY = ["users"] as const;
export function useUsers() {
return useQuery({
queryKey: USERS_KEY,
queryFn: usersApi.getAll,
});
}
export function useUser(id: string) {
return useQuery({
queryKey: [...USERS_KEY, id],
queryFn: () => usersApi.getById(id),
enabled: !!id,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CreateUserDto) => usersApi.create(dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: USERS_KEY });
},
});
}
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => usersApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: USERS_KEY });
},
});
}
```
### Component with Tests
```tsx
// src/features/users/components/UserCard.tsx
import type { User } from "../types";
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import { useDeleteUser } from "../hooks/useUsers";
interface UserCardProps {
user: User;
onEdit?: (user: User) => void;
}
export function UserCard({ user, onEdit }: UserCardProps) {
const deleteUser = useDeleteUser();
const handleDelete = () => {
if (confirm("Are you sure?")) {
deleteUser.mutate(user.id);
}
};
return (
<Card data-testid={`user-card-${user.id}`}>
<CardHeader className="flex items-center justify-between">
<h3 className="font-semibold">{user.name}</h3>
<Badge variant={user.role === "admin" ? "primary" : "secondary"}>{user.role}</Badge>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{user.email}</p>
<div className="mt-4 flex gap-2">
<Button size="sm" onClick={() => onEdit?.(user)}>
Edit
</Button>
<Button size="sm" variant="destructive" onClick={handleDelete} disabled={deleteUser.isPending}>
{deleteUser.isPending ? "Deleting..." : "Delete"}
</Button>
</div>
</CardContent>
</Card>
);
}
// src/features/users/components/UserCard.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { describe, it, expect, vi } from "vitest";
import { UserCard } from "./UserCard";
import type { User } from "../types";
const mockUser: User = {
id: "1",
name: "John Doe",
email: "john@example.com",
role: "admin",
createdAt: new Date(),
};
function renderWithProviders(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
}
describe("UserCard", () => {
it("renders user information", () => {
renderWithProviders(<UserCard user={mockUser} />);
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText("john@example.com")).toBeInTheDocument();
expect(screen.getByText("admin")).toBeInTheDocument();
});
it("calls onEdit when edit button is clicked", async () => {
const user = userEvent.setup();
const onEdit = vi.fn();
renderWithProviders(<UserCard user={mockUser} onEdit={onEdit} />);
await user.click(screen.getByRole("button", { name: /edit/i }));
expect(onEdit).toHaveBeenCalledWith(mockUser);
});
});
```
### Shared UI Component
```tsx
// src/components/ui/Button/Button.tsx
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/cn";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary-600 text-white hover:bg-primary-700",
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",
destructive: "bg-red-600 text-white hover:bg-red-700",
ghost: "hover:bg-gray-100 hover:text-gray-900",
link: "text-primary-600 underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-12 px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, loading, children, disabled, ...props }, ref) => (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
disabled={disabled || loading}
{...props}
>
{loading ? (
<span className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
) : null}
{children}
</button>
),
);
Button.displayName = "Button";
// src/components/ui/Button/index.ts
export { Button, type ButtonProps } from "./Button";
```
### Utility: cn Helper
```typescript
// src/utils/cn.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
### Custom Hook Example
```typescript
// src/hooks/useLocalStorage.ts
import { useState, useEffect, useCallback } from "react";
export function useLocalStorage<T>(
key: string,
initialValue: T,
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const valueToStore = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(valueToStore));
return valueToStore;
});
},
[key],
);
const removeValue = useCallback(() => {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
}, [key, initialValue]);
return [storedValue, setValue, removeValue];
}
```
## GitHub Actions CI/CD
### .github/workflows/ci.yml
```yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Format check
run: npm run format:check
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/coverage-final.json
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
```
## Validation Checklist
### Structure Validation
```text
□ Uses feature-based or atomic design structure
□ Clear separation: features/, components/, hooks/, utils/
□ Each feature has index.ts barrel export
□ Components co-located with tests
□ Types in dedicated types/ directories
```
### Build & Tooling
```text
□ Vite 6.x with SWC plugin
□ TypeScript strict mode enabled
□ Path aliases configured (vite.config.ts + tsconfig.json)
□ ESLint flat config with TypeScript rules
□ Prettier with Tailwind plugin
□ Husky + lint-staged for pre-commit
```
### Testing
```text
□ Vitest configured with jsdom
□ Testing Library for component tests
□ MSW for API mocking
□ Playwright for E2E tests
□ Coverage thresholds set
```
### State & Data
```text
□ TanStack Query for server state
□ Zustand for client state (if needed)
□ Query keys properly typed
□ Proper cache invalidation
□ Error boundaries for error handling
```
### Production Ready
```text
□ Environment variables (.env.example)
□ Bundle splitting configured
□ Source maps enabled for production
□ CI/CD pipeline with all checks
□ Proper error handling throughout
```
## Scaffold Commands
### Create New Project
```bash
# Using create-vite
npm create vite@latest my-react-app -- --template react-swc-ts
# Or with specific setup
cd my-react-app
npm install
# Add essential dependencies
npm install @tanstack/react-query @tanstack/react-router zustand axios zod react-hook-form @hookform/resolvers clsx tailwind-merge
# Add dev dependencies
npm install -D tailwindcss @tailwindcss/vite postcss autoprefixer
npm install -D vitest @vitest/ui @vitest/coverage-v8 jsdom
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install -D @playwright/test msw
npm install -D husky lint-staged prettier prettier-plugin-tailwindcss
# Initialize Playwright
npx playwright install
# Setup Husky
npx husky init
echo "npx lint-staged" > .husky/pre-commit
```
### Development Commands
```bash
# Start development server
npm run dev
# Type checking
npm run typecheck
# Linting
npm run lint
npm run lint:fix
# Formatting
npm run format
npm run format:check
# Testing
npm run test # Watch mode
npm run test:ui # Vitest UI
npm run test:coverage # With coverage
# E2E Testing
npm run test:e2e # Headless
npm run test:e2e:ui # With UI
# Build
npm run build
npm run preview
```
## Anti-Patterns to Avoid
```
❌ Giant components (>200 lines)
❌ Props drilling (use context or state management)
❌ Mixing server and client state
❌ Direct DOM manipulation
❌ Not using TypeScript strict mode
❌ Inline styles instead of Tailwind/CSS modules
❌ Not handling loading/error states
❌ Missing error boundaries
❌ useEffect for data fetching (use TanStack Query)
❌ Storing derived state
❌ Not memoizing expensive computations
❌ Missing accessibility attributes
```
## Additional Resources
- [React Documentation](https://react.dev/)
- [Vite Documentation](https://vitejs.dev/)
- [TanStack Query](https://tanstack.com/query)
- [TanStack Router](https://tanstack.com/router)
- [Zustand](https://zustand-demo.pmnd.rs/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Vitest](https://vitest.dev/)
- [Playwright](https://playwright.dev/)
- [Testing Library](https://testing-library.com/)
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.