React Development · Lesson 1 of 15
Environment Setup
What You'll Have After This Guide
By the end of this guide you'll have:
- Node.js 20 LTS with a proper package manager
- A React + TypeScript + Vite project scaffolded
- VS Code configured with the right extensions
- ESLint + Prettier enforcing code quality automatically
- Path aliases so you never write
../../../componentsagain - Husky pre-commit hooks that block bad code from entering git
This is the same setup used on production React teams.
Step 1: Install Node.js (The Right Way)
Don't install Node.js directly from nodejs.org. Use a version manager — you'll need to switch Node versions between projects.
Option A: fnm (Recommended — Fast, cross-platform)
# macOS / Linux
curl -fsSL https://fnm.vercel.app/install | bash
# Windows (PowerShell)
winget install Schniz.fnm# After installing fnm:
fnm install 20 # Install Node 20 LTS
fnm use 20 # Use it in current shell
fnm default 20 # Make it the default
# Verify
node --version # v20.x.x
npm --version # 10.x.xOption B: nvm (Most popular, macOS/Linux only)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm install 20
nvm use 20
nvm alias default 20Check Node is working
node --version # Should print v20.x.x
npm --version # Should print 10.x.xStep 2: Choose a Package Manager
npm comes with Node. But you have better options:
| Package Manager | Install | Why Use It |
|---|---|---|
| npm | Built-in | Default, always works |
| pnpm | npm i -g pnpm | Fastest, disk-efficient (recommended) |
| yarn | npm i -g yarn | Stable, Workspaces support |
This guide uses pnpm — it's significantly faster and uses hard links to save disk space across projects.
npm install -g pnpm
pnpm --version # Should print 9.x.xStep 3: Scaffold Your React Project
Option A: Vite (Recommended for SPAs and libraries)
Vite is fast. Dev server starts in under 300ms. Hot Module Replacement is near-instant.
pnpm create vite@latest my-app -- --template react-ts
cd my-app
pnpm install
pnpm devYour browser opens at http://localhost:5173.
What Vite gives you:
my-app/
├── public/ # Static assets (favicon, robots.txt)
├── src/
│ ├── assets/ # Images, fonts imported by JS
│ ├── App.tsx # Root component
│ ├── App.css
│ ├── main.tsx # Entry point — renders into #root
│ └── vite-env.d.ts # Vite type definitions
├── index.html # Entry HTML (Vite serves this)
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.tsOption B: Next.js (Recommended for SSR, SSG, and full-stack)
Next.js adds server-side rendering, file-based routing, and API routes.
npx create-next-app@latest my-app \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*"
cd my-app
npm run devChoose Next.js if: You need SEO, server rendering, API routes, or full-stack capabilities.
Choose Vite if: You're building a SPA, dashboard, or component library.
Step 4: Configure VS Code
Essential Extensions
Install these — open VS Code, press Ctrl+P (or Cmd+P on Mac), paste each line:
ext install dbaeumer.vscode-eslint
ext install esbenp.prettier-vscode
ext install bradlc.vscode-tailwindcss
ext install dsznajder.es7-react-js-snippets
ext install ms-vscode.vscode-typescript-next
ext install streetsidesoftware.code-spell-checker
ext install usernamehw.errorlens
ext install christian-kohler.path-intellisenseOr install from the Extensions sidebar by searching these names:
- ESLint — in-editor linting
- Prettier — auto-format on save
- Tailwind CSS IntelliSense — autocomplete for Tailwind classes
- ES7+ React/Redux/React-Native snippets —
rfce→ full component template - TypeScript Nightly — latest TypeScript features
- Error Lens — shows errors inline (not just in Problems panel)
VS Code Workspace Settings
Create .vscode/settings.json in your project root:
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
},
"editor.tabSize": 2,
"editor.rulers": [100],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"files.exclude": {
"node_modules": true,
"dist": true,
".next": true
},
"search.exclude": {
"node_modules": true,
"dist": true,
"pnpm-lock.yaml": true
},
"tailwindCSS.experimental.classRegex": [
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}Recommended Extensions File
Create .vscode/extensions.json — VS Code will prompt teammates to install these:
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"dsznajder.es7-react-js-snippets",
"usernamehw.errorlens",
"christian-kohler.path-intellisense",
"streetsidesoftware.code-spell-checker"
]
}Step 5: ESLint Configuration
pnpm add -D eslint @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11yCreate eslint.config.ts (ESLint v9 flat config):
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactPlugin from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
plugins: {
react: reactPlugin,
"react-hooks": reactHooks,
"jsx-a11y": jsxA11y,
},
languageOptions: {
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: import.meta.dirname,
},
globals: {
window: "readonly",
document: "readonly",
},
},
settings: {
react: { version: "detect" },
},
rules: {
// React
"react/react-in-jsx-scope": "off", // Not needed in React 17+
"react/prop-types": "off", // We use TypeScript
"react/self-closing-comp": "warn",
"react/jsx-curly-brace-presence": ["warn", "never"],
// Hooks
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
// TypeScript
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/prefer-nullish-coalescing": "warn",
"@typescript-eslint/prefer-optional-chain": "warn",
// Accessibility
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-is-valid": "error",
},
},
{
// Test files — relax some rules
files: ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
{
ignores: ["dist/", ".next/", "node_modules/", "*.config.js"],
}
);Step 6: Prettier Configuration
pnpm add -D prettier eslint-config-prettierCreate .prettierrc:
{
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 100,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf"
}Create .prettierignore:
node_modules
dist
.next
build
pnpm-lock.yaml
package-lock.json
*.min.jsStep 7: TypeScript Configuration
Replace the default tsconfig.json with a stricter, production-grade config:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
/* Paths */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@hooks/*": ["./src/hooks/*"],
"@lib/*": ["./src/lib/*"],
"@types/*": ["./src/types/*"],
"@store/*": ["./src/store/*"]
},
/* Output */
"outDir": "./dist",
"skipLibCheck": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Configure path aliases in Vite
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@components": path.resolve(__dirname, "./src/components"),
"@hooks": path.resolve(__dirname, "./src/hooks"),
"@lib": path.resolve(__dirname, "./src/lib"),
"@store": path.resolve(__dirname, "./src/store"),
},
},
server: {
port: 3000,
open: true,
},
});Now you can write:
// Before: ❌ Brittle relative imports
import { Button } from "../../../components/ui/Button";
// After: ✅ Clean absolute imports
import { Button } from "@components/ui/Button";Step 8: Install Core Dependencies
# Routing
pnpm add react-router-dom
# Data fetching & server state
pnpm add @tanstack/react-query @tanstack/react-query-devtools
# Global state (pick one)
pnpm add @reduxjs/toolkit react-redux
# or
pnpm add zustand
# Forms & validation
pnpm add react-hook-form zod @hookform/resolvers
# Styling utilities
pnpm add clsx tailwind-merge
# Icons
pnpm add lucide-react
# HTTP client (optional — fetch works fine too)
pnpm add axiosStep 9: Project Folder Structure
Scale matters. Use a feature-based structure from the start:
src/
├── components/ # Shared UI components (buttons, inputs, modals)
│ ├── ui/ # Primitives (Button, Input, Badge, Card)
│ └── layout/ # Layout components (Header, Sidebar, Footer)
│
├── features/ # Feature modules — each self-contained
│ ├── auth/
│ │ ├── components/ # AuthForm, LoginPage, etc.
│ │ ├── hooks/ # useAuth, useLoginForm
│ │ ├── store/ # authSlice.ts (if Redux)
│ │ ├── api/ # authApi.ts (RTK Query or fetch)
│ │ └── types.ts
│ ├── products/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── ...
│ └── cart/
│
├── hooks/ # Global custom hooks (useDebounce, useLocalStorage)
├── lib/ # Utilities (cn, formatters, validators)
├── store/ # Redux store setup (if using Redux)
├── types/ # Global TypeScript types
├── router/ # Route definitions
│ └── index.tsx
├── App.tsx
└── main.tsxCreate the structure:
mkdir -p src/{components/{ui,layout},features/{auth,products}/components,hooks,lib,types,router}Step 10: Git Hooks with Husky
Block broken code from being committed:
pnpm add -D husky lint-staged
# Initialize husky
npx husky init
# Add pre-commit hook
echo 'npx lint-staged' > .husky/pre-commitCreate .lintstagedrc.json:
{
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
}Now every git commit runs ESLint + Prettier on staged files. Bad code can't enter the repository.
Step 11: Add Scripts to package.json
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --fix",
"format": "prettier --write src/",
"type-check": "tsc --noEmit",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}Step 12: Environment Variables
# .env (committed — public defaults)
VITE_APP_NAME=MyApp
VITE_API_URL=http://localhost:8000
# .env.local (never committed — secrets)
VITE_API_KEY=your-actual-key
# .env.production
VITE_API_URL=https://api.myapp.comImportant: In Vite, only variables prefixed with VITE_ are exposed to the browser. Never put secrets in VITE_ variables.
// Type-safe environment access
// src/lib/env.ts
const env = {
appName: import.meta.env.VITE_APP_NAME,
apiUrl: import.meta.env.VITE_API_URL,
} as const;
// Validate at startup
if (!env.apiUrl) throw new Error("VITE_API_URL is required");
export default env;Add .env.local to .gitignore:
.env.local
.env.*.localVerify Everything Works
# 1. Type checking — should print nothing (no errors)
pnpm type-check
# 2. Linting — should print nothing
pnpm lint
# 3. Tests — should pass
pnpm test
# 4. Build — should produce dist/ folder
pnpm build
# 5. Dev server
pnpm devCommon Setup Problems
Problem: Cannot find module '@/components/Button'
Fix: Make sure both tsconfig.json paths AND vite.config.ts aliases are configured. Both are required.
Problem: ESLint says "React must be in scope" on every file
Fix: Add "react/react-in-jsx-scope": "off" to ESLint config — this rule is for React 16 and below.
Problem: Prettier and ESLint conflict on formatting
Fix: Install eslint-config-prettier and add "prettier" as the last entry in your ESLint extends — it disables all ESLint formatting rules that Prettier handles.
Problem: Husky pre-commit hook not running
Fix: Run git init first if the repo isn't initialized yet. Husky requires an existing git repo.
Problem: pnpm dev works but pnpm build fails with type errors
Fix: Run pnpm type-check separately to see all TypeScript errors. Build runs the TS compiler strictly — fix the type errors it reports.