
React Impl Project Setup
- 11 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-impl-project-setup is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-impl-project-setup
- Frontend Development
- AI-coding skill
React Impl Project Setup by the numbers
- 11 all-time installs (skills.sh)
- Ranked #1,658 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-impl-project-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-impl-project-setup
Quick Reference
Project Creation (Step-by-Step)
React 18 (Stable)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run devReact 19
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install react@19 react-dom@19
npm install -D @types/react@19 @types/node
npm run devIn React 19, type definitions are included in thereactpackage itself. Install@types/react@19for the transition period until all tooling catches up.
Critical Warnings
NEVER use Create React App (CRA) -- it is deprecated and unmaintained. ALWAYS use Vite with react-ts template for new React projects.
NEVER commit .env.local or .env.*.local files -- these contain local secrets. ALWAYS add them to .gitignore.
NEVER expose sensitive keys in environment variables without the VITE_ prefix check -- only variables prefixed with VITE_ are exposed to the client bundle.
NEVER skip strict: true in tsconfig.json -- strict mode catches entire categories of bugs at compile time. ALWAYS enable it for new projects.
NEVER use relative imports like ../../../components/Button -- ALWAYS configure path aliases (@/) to keep imports clean and refactor-safe.
ALWAYS add type-check as a separate script in package.json -- Vite does NOT perform type checking during development or build by default.
---
Recommended Project Structure
my-app/
├── public/ # Static assets (copied as-is to dist/)
│ └── favicon.svg
├── src/
│ ├── components/ # Reusable UI components
│ │ ├── ui/ # Primitive/base components (Button, Input, Card)
│ │ └── features/ # Feature-specific components
│ ├── hooks/ # Custom React hooks
│ ├── utils/ # Pure utility functions (no React dependency)
│ ├── types/ # Shared TypeScript type definitions
│ ├── pages/ # Route-level page components
│ ├── layouts/ # Layout wrapper components
│ ├── services/ # API calls and external service integrations
│ ├── stores/ # State management (context, zustand, etc.)
│ ├── constants/ # Application constants and config
│ ├── assets/ # Images, fonts, SVGs imported in code
│ ├── App.tsx # Root component
│ ├── main.tsx # Application entry point
│ └── vite-env.d.ts # Vite client type declarations
├── .env # Default env vars (committed, no secrets)
├── .env.local # Local overrides (NEVER commit)
├── .env.production # Production env vars
├── .gitignore
├── .eslintrc.cjs # ESLint configuration
├── index.html # HTML entry point (Vite uses this as root)
├── package.json
├── tsconfig.json # TypeScript config (project references root)
├── tsconfig.app.json # App-specific TS config
├── tsconfig.node.json # Node/Vite config TS settings
└── vite.config.ts # Vite configurationStructure Rules
- ALWAYS co-locate component tests next to their source:
Button.tsx+Button.test.tsx - ALWAYS use barrel exports (
index.ts) per directory for clean imports - ALWAYS keep
utils/free of React imports -- pure functions only - ALWAYS place route-level components in
pages/, reusable components incomponents/
---
TypeScript Configuration
tsconfig.json (Project References Root)
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}tsconfig.app.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}Key Settings Explained
| Setting | Value | Why |
|---|---|---|
strict | true | Enables all strict type-checking options |
jsx | "react-jsx" | Uses React 17+ automatic JSX transform (no import React needed) |
moduleResolution | "bundler" | Matches Vite's module resolution strategy |
isolatedModules | true | Required for Vite -- ensures files can be transpiled independently |
noEmit | true | Vite handles transpilation; TypeScript only type-checks |
noUncheckedIndexedAccess | true | Prevents unsafe array/object index access |
paths | @/* -> ./src/* | Path alias for clean imports |
---
Vite Configuration
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
// Proxy API requests during development
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
build: {
sourcemap: true,
target: 'es2020',
},
});Path Alias Sync
ALWAYS configure path aliases in BOTH tsconfig.app.json AND vite.config.ts -- TypeScript needs them for type checking, Vite needs them for bundling.
---
ESLint Configuration
Install Dependencies
npm install -D eslint @eslint/js typescript-eslint \
eslint-plugin-react-hooks eslint-plugin-react-refresheslint.config.js (Flat Config)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
);ALWAYS use ESLint flat config (eslint.config.js) for new projects -- the.eslintrc.*format is deprecated.
---
Environment Variables
Rules
| Rule | Detail |
|---|---|
| Client-exposed vars | MUST be prefixed with VITE_ |
| Access pattern | import.meta.env.VITE_API_URL |
| Server-only vars | Use vars WITHOUT VITE_ prefix -- they are NOT bundled |
| Built-in vars | import.meta.env.MODE, import.meta.env.DEV, import.meta.env.PROD |
.env File Load Order
1. .env -- loaded in all cases 2. .env.local -- loaded in all cases, ignored by git 3. .env.[mode] -- loaded for specified mode (development/production) 4. .env.[mode].local -- loaded for specified mode, ignored by git
Type Declarations for Env Vars
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}ALWAYS declare custom VITE_ variables in a type definition file -- this provides autocompletion and type safety for import.meta.env.
---
Package.json Scripts
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"type-check": "tsc -b --noEmit"
}
}| Script | Purpose |
|---|---|
dev | Start Vite dev server with HMR |
build | Type-check then build for production |
preview | Locally preview production build |
lint | Run ESLint across the project |
type-check | Run TypeScript compiler for type checking only |
---
.gitignore
# Dependencies
node_modules/
# Build output
dist/
dist-ssr/
# Environment (local overrides with secrets)
.env.local
.env.*.local
# Editor
.vscode/*
!.vscode/extensions.json
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Debug logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*---
Decision Trees
New Project Setup
Need a new React project?
├── ALWAYS use: npm create vite@latest -- --template react-ts
├── Need React 19?
│ └── After scaffold: npm install react@19 react-dom@19
├── Need path aliases?
│ └── Configure BOTH tsconfig.app.json AND vite.config.ts
├── Need API proxy?
│ └── Add server.proxy to vite.config.ts
└── Need env variables in client code?
└── Prefix with VITE_ and declare types in env.d.tsTypeScript Strictness
Setting up tsconfig?
├── ALWAYS enable strict: true
├── ALWAYS enable isolatedModules: true (Vite requirement)
├── ALWAYS set jsx: "react-jsx" (automatic transform)
├── ALWAYS set noEmit: true (Vite handles transpilation)
└── Consider noUncheckedIndexedAccess: true (safer array access)---
Reference Links
- references/examples.md -- Complete project setup workflows and configuration examples
- references/patterns.md -- Project structure patterns and conventions
- references/anti-patterns.md -- Common setup mistakes and why they fail
Official Sources
- https://vite.dev/guide/
- https://react.dev/learn/start-a-new-react-project
- https://react.dev/learn/typescript
- https://www.typescriptlang.org/tsconfig
- https://eslint.org/docs/latest/use/configure/configuration-files
Anti-Patterns (React Project Setup)
1. Using Create React App
# WRONG: CRA is deprecated and unmaintained
npx create-react-app my-app --template typescript
# CORRECT: Use Vite with react-ts template
npm create vite@latest my-app -- --template react-tsWHY: Create React App (CRA) was officially deprecated by the React team. It uses Webpack with slow build times, has outdated dependencies with security vulnerabilities, and receives no updates. Vite provides faster dev startup, instant HMR, and active maintenance.
---
2. Disabling TypeScript Strict Mode
// WRONG: Disabling strict mode to "fix" type errors
{
"compilerOptions": {
"strict": false,
"noImplicitAny": false
}
}
// CORRECT: Enable strict mode and fix the actual type issues
{
"compilerOptions": {
"strict": true
}
}WHY: Disabling strict mode hides real bugs. noImplicitAny: false allows untyped values to slip through as any, defeating the purpose of TypeScript. Fix the type errors instead of suppressing them.
---
3. Forgetting isolatedModules
// WRONG: Missing isolatedModules breaks Vite transpilation
{
"compilerOptions": {
"isolatedModules": false
}
}
// CORRECT: Required for Vite
{
"compilerOptions": {
"isolatedModules": true
}
}WHY: Vite uses esbuild for transpilation, which processes each file independently. Without isolatedModules: true, TypeScript allows patterns (like const enum across files) that break when files are transpiled in isolation.
---
4. Path Aliases in Only One Config
// WRONG: Alias only in vite.config.ts -- TypeScript cannot resolve imports
// vite.config.ts
export default defineConfig({
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
});
// tsconfig.app.json -- missing paths! TypeScript shows red squiggles.// CORRECT: Configure in BOTH files
// tsconfig.app.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
}
}// vite.config.ts
export default defineConfig({
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
});WHY: TypeScript and Vite have separate module resolution. TypeScript uses paths for type checking and editor IntelliSense. Vite uses resolve.alias for bundling. Both must be configured for the full toolchain to work.
---
5. Exposing Secrets in Environment Variables
# WRONG: Sensitive key with VITE_ prefix -- bundled into client code!
VITE_DATABASE_URL=postgres://user:password@host/db
VITE_SECRET_KEY=sk-live-abc123
# CORRECT: Only expose public values with VITE_ prefix
VITE_API_URL=https://api.example.com
DATABASE_URL=postgres://user:password@host/db # NOT exposed to clientWHY: Any variable prefixed with VITE_ is statically replaced in the client bundle and visible to anyone who inspects the JavaScript. API keys, database URLs, and secrets MUST use unprefixed names so they remain server-side only.
---
6. Missing Type Declarations for Env Vars
// WRONG: Using import.meta.env without type declarations
const url = import.meta.env.VITE_API_URL; // type: any
// CORRECT: Declare types in env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
const url = import.meta.env.VITE_API_URL; // type: stringWHY: Without type declarations, all import.meta.env values are typed as any. You lose autocompletion, typo detection, and type safety. A misspelled variable name silently returns undefined at runtime.
---
7. Committing .env.local Files
# WRONG: .gitignore missing local env files
node_modules/
dist/
# CORRECT: Always ignore local env overrides
node_modules/
dist/
.env.local
.env.*.localWHY: .env.local and .env.*.local files contain developer-specific overrides and may include secrets. Committing them exposes credentials and causes conflicts between developers.
---
8. Using .eslintrc (Legacy Format)
// WRONG: Legacy .eslintrc.js format (deprecated in ESLint v9)
module.exports = {
extends: ['react-app'],
rules: { /* ... */ },
};
// CORRECT: Flat config format (eslint.config.js)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
},
);WHY: The .eslintrc.* configuration format is deprecated. ESLint 9+ uses the flat config format (eslint.config.js). The legacy format will be removed in a future major version.
---
9. Skipping type-check in CI/Build
// WRONG: Build without type checking
{
"scripts": {
"build": "vite build"
}
}
// CORRECT: Type-check before building
{
"scripts": {
"build": "tsc -b && vite build",
"type-check": "tsc -b --noEmit"
}
}WHY: Vite uses esbuild for transpilation, which strips types but does NOT check them. Without an explicit tsc step, type errors slip through to production. ALWAYS run tsc -b before vite build.
---
10. Deep Relative Import Paths
// WRONG: Fragile deep relative imports
import { Button } from '../../../components/ui/Button';
import { useAuth } from '../../../../hooks/useAuth';
import { formatDate } from '../../../utils/formatDate';
// CORRECT: Path aliases for clean, refactor-safe imports
import { Button } from '@/components/ui/Button';
import { useAuth } from '@/hooks/useAuth';
import { formatDate } from '@/utils/formatDate';WHY: Deep relative paths break when files move, are hard to read, and make it unclear where in the project tree a file lives. Path aliases (@/) provide stable, readable imports that survive refactoring.
---
11. Storing Tests in a Separate Directory Tree
# WRONG: Tests separated from source
src/
├── components/
│ └── Button.tsx
tests/
├── components/
│ └── Button.test.tsx
# CORRECT: Tests co-located with source
src/
├── components/
│ ├── Button.tsx
│ └── Button.test.tsxWHY: Separate test directories create a parallel file tree that must be manually kept in sync. When a component moves or is renamed, its test is easily forgotten. Co-location ensures tests move with their source and makes test coverage gaps immediately visible.
---
12. Using jsx: "react" Instead of "react-jsx"
// WRONG: Legacy JSX transform (requires "import React" in every file)
{
"compilerOptions": {
"jsx": "react"
}
}
// CORRECT: Automatic JSX transform (React 17+)
{
"compilerOptions": {
"jsx": "react-jsx"
}
}WHY: The "react" JSX mode uses the classic React.createElement transform, requiring import React from 'react' in every file that uses JSX. The "react-jsx" mode uses the automatic transform introduced in React 17, which inserts the import automatically. This reduces boilerplate and slightly improves bundle size.
Project Setup Workflows and Configuration Examples
Example 1: Minimal React 18 + Vite + TypeScript Setup
# Step 1: Scaffold the project
npm create vite@latest my-app -- --template react-ts
# Step 2: Install dependencies
cd my-app
npm install
# Step 3: Start development server
npm run devGenerated project includes:
src/App.tsx-- root componentsrc/main.tsx-- entry point withcreateRootindex.html-- Vite HTML entryvite.config.ts-- Vite config with@vitejs/plugin-reacttsconfig.json-- project references roottsconfig.app.json-- app TypeScript configtsconfig.node.json-- Node/Vite TypeScript config
---
Example 2: React 19 Project Setup
# Step 1: Scaffold with Vite
npm create vite@latest my-app-19 -- --template react-ts
cd my-app-19
# Step 2: Upgrade to React 19
npm install react@19 react-dom@19
# Step 3: Install React 19 types (transition period)
npm install -D @types/react@19 @types/node
# Step 4: Verify version
npm run dev// src/main.tsx -- React 19 entry point (same API as React 18)
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);---
Example 3: Path Aliases Configuration
Step 1: TypeScript (tsconfig.app.json)
{
"compilerOptions": {
// ... other options
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}Step 2: Vite (vite.config.ts)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});Step 3: Usage
// Before (fragile relative imports)
import { Button } from '../../../components/ui/Button';
import { useAuth } from '../../hooks/useAuth';
// After (clean alias imports)
import { Button } from '@/components/ui/Button';
import { useAuth } from '@/hooks/useAuth';---
Example 4: Environment Variables Setup
.env
# Committed to git -- no secrets here
VITE_APP_TITLE=My React App
VITE_API_URL=https://api.example.com.env.local
# NEVER committed -- local development overrides
VITE_API_URL=http://localhost:8080
SECRET_KEY=abc123 # NOT exposed to client (no VITE_ prefix).env.production
# Production-specific values
VITE_API_URL=https://api.production.com
VITE_APP_TITLE=My React App (Production)Type Declarations (src/env.d.ts)
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}Usage in Components
function ApiStatus(): React.ReactElement {
const apiUrl = import.meta.env.VITE_API_URL;
const isDev = import.meta.env.DEV;
return (
<div>
<p>API: {apiUrl}</p>
{isDev && <p>Running in development mode</p>}
</div>
);
}---
Example 5: Complete ESLint Flat Config
# Install all ESLint dependencies
npm install -D eslint @eslint/js typescript-eslint \
eslint-plugin-react-hooks eslint-plugin-react-refresh// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
// Additional recommended rules
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports' },
],
},
},
);---
Example 6: Vite Dev Server with API Proxy
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'node:path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
open: true,
proxy: {
// Proxy /api requests to backend
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
// Proxy WebSocket connections
'/ws': {
target: 'ws://localhost:8080',
ws: true,
},
},
},
build: {
sourcemap: true,
target: 'es2020',
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
});---
Example 7: Complete package.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",
"type-check": "tsc -b --noEmit",
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\""
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/node": "^22.0.0",
"@vitejs/plugin-react": "^4.3.0",
"eslint": "^9.0.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.0",
"prettier": "^3.3.0",
"typescript": "~5.6.0",
"typescript-eslint": "^8.0.0",
"vite": "^6.0.0"
}
}---
Example 8: Barrel Exports Pattern
// src/components/ui/Button.tsx
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ variant = 'primary', children, onClick }: ButtonProps): React.ReactElement {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{children}
</button>
);
}// src/components/ui/index.ts -- barrel export
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';// src/components/index.ts -- top-level barrel
export * from './ui';
export * from './features';// Usage in a page component
import { Button, Input, Card } from '@/components';---
Example 9: Prettier Configuration
npm install -D prettier// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always"
}// .prettierignore
dist
node_modules
*.min.jsProject Structure Patterns
Pattern 1: Feature-Based Organization
Organize code by feature/domain rather than by file type. Each feature directory contains its own components, hooks, types, and utilities.
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.tsx
│ │ │ ├── LoginForm.test.tsx
│ │ │ └── SignupForm.tsx
│ │ ├── hooks/
│ │ │ └── useAuth.ts
│ │ ├── types.ts
│ │ ├── api.ts
│ │ └── index.ts
│ ├── dashboard/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── types.ts
│ │ └── index.ts
│ └── settings/
│ ├── components/
│ ├── hooks/
│ └── index.ts
├── components/ # Shared/generic components only
│ └── ui/
├── hooks/ # Shared hooks only
├── utils/ # Shared utilities only
└── types/ # Global types onlyWhen to Use
- ALWAYS use feature-based organization for projects with 3+ distinct features
- ALWAYS keep shared/generic code in top-level directories (
components/,hooks/,utils/) - NEVER import from another feature's internal files -- use only the barrel export (
index.ts)
Benefits
- Features are self-contained and can be moved, deleted, or extracted
- Reduces cross-feature coupling
- Makes code ownership and review boundaries clear
---
Pattern 2: Flat Component Organization (Small Projects)
For small projects (< 15 components), a flat structure is simpler and avoids premature abstraction.
src/
├── components/
│ ├── Button.tsx
│ ├── Button.test.tsx
│ ├── Header.tsx
│ ├── Footer.tsx
│ ├── TodoList.tsx
│ └── TodoItem.tsx
├── hooks/
│ └── useTodos.ts
├── utils/
│ └── formatDate.ts
├── App.tsx
└── main.tsxWhen to Use
- Projects with fewer than 15 components
- Prototypes and MVPs
- ALWAYS migrate to feature-based when the flat structure exceeds ~20 files in
components/
---
Pattern 3: Component Co-location
ALWAYS keep related files next to their component. Tests, styles, and stories belong next to the component, not in separate top-level directories.
src/components/ui/Button/
├── Button.tsx # Component implementation
├── Button.test.tsx # Unit tests
├── Button.module.css # Scoped styles (if using CSS Modules)
├── Button.stories.tsx # Storybook stories (if using Storybook)
└── index.ts # Re-export// Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';Rules
- ALWAYS co-locate test files:
Component.test.tsxnext toComponent.tsx - ALWAYS co-locate styles:
Component.module.cssnext toComponent.tsx - NEVER create a separate
__tests__/orstyles/directory at the project root - ALWAYS provide an
index.tsbarrel export for component directories
---
Pattern 4: Layout Pattern
Layouts wrap page content with shared UI elements (navigation, sidebar, footer). Use layouts to avoid repeating wrapper markup in every page.
src/
├── layouts/
│ ├── RootLayout.tsx # App shell: nav + outlet
│ ├── DashboardLayout.tsx # Sidebar + content area
│ └── AuthLayout.tsx # Centered card for login/signup
├── pages/
│ ├── HomePage.tsx
│ ├── DashboardPage.tsx
│ └── LoginPage.tsx// layouts/RootLayout.tsx
import { Outlet } from 'react-router-dom';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
export function RootLayout(): React.ReactElement {
return (
<div className="app">
<Header />
<main>
<Outlet />
</main>
<Footer />
</div>
);
}Rules
- ALWAYS separate layouts from pages -- layouts handle structure, pages handle content
- ALWAYS use
<Outlet />(React Router) or{children}for content injection - NEVER put data fetching logic in layout components -- layouts are structural only
---
Pattern 5: Service Layer Pattern
Centralize API calls and external service interactions in a services/ directory. Components and hooks consume services, never call fetch directly.
// src/services/api.ts -- base API configuration
const BASE_URL = import.meta.env.VITE_API_URL;
async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${BASE_URL}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
return response.json() as Promise<T>;
}
export const api = { request };// src/services/todos.ts -- domain-specific service
import { api } from './api';
import type { Todo, CreateTodoInput } from '@/types';
export const todoService = {
getAll: () => api.request<Todo[]>('/todos'),
getById: (id: string) => api.request<Todo>(`/todos/${id}`),
create: (input: CreateTodoInput) =>
api.request<Todo>('/todos', {
method: 'POST',
body: JSON.stringify(input),
}),
delete: (id: string) =>
api.request<void>(`/todos/${id}`, { method: 'DELETE' }),
};Rules
- ALWAYS centralize API base URL and default headers in one file
- ALWAYS type all request and response payloads
- NEVER call
fetch()directly in components -- use the service layer - NEVER store auth tokens in service files -- inject them via interceptors or headers
---
Pattern 6: Type Organization
src/types/
├── index.ts # Re-exports all shared types
├── api.ts # API response/request types
├── models.ts # Domain model types
└── common.ts # Utility types (Nullable, Optional, etc.)// src/types/models.ts
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user' | 'viewer';
createdAt: string;
}
export interface Todo {
id: string;
title: string;
completed: boolean;
userId: string;
}// src/types/api.ts
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
}
export interface ApiError {
message: string;
code: string;
status: number;
}Rules
- ALWAYS use
interfacefor object shapes,typefor unions and intersections - ALWAYS export types from barrel
index.ts - NEVER put React component prop types in
types/-- keep prop types co-located with their component - ALWAYS use
typeimports:import type { User } from '@/types'
---
Pattern 7: Constants and Configuration
// src/constants/index.ts
export const APP_CONFIG = {
APP_NAME: 'My React App',
MAX_UPLOAD_SIZE_MB: 10,
PAGINATION_DEFAULT_SIZE: 20,
DEBOUNCE_MS: 300,
} as const;
export const ROUTES = {
HOME: '/',
LOGIN: '/login',
DASHBOARD: '/dashboard',
SETTINGS: '/settings',
} as const;
export const QUERY_KEYS = {
TODOS: 'todos',
USERS: 'users',
USER_PROFILE: 'user-profile',
} as const;Rules
- ALWAYS use
as constfor constant objects -- ensures literal types and readonly - ALWAYS centralize route paths in a constants file
- NEVER hardcode magic numbers or strings in components -- extract to constants