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.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill react-vite-project-architect --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of React Vite Project Architect?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-react-vite-project-architect)More formats (shields.io, HTML) on the badges page.
---
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/)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!