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 ../../../components again
  • 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)

Bash
# macOS / Linux
curl -fsSL https://fnm.vercel.app/install | bash

# Windows (PowerShell)
winget install Schniz.fnm
Bash
# 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.x

Option B: nvm (Most popular, macOS/Linux only)

Bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

nvm install 20
nvm use 20
nvm alias default 20

Check Node is working

Bash
node --version    # Should print v20.x.x
npm --version     # Should print 10.x.x

Step 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.

Bash
npm install -g pnpm

pnpm --version   # Should print 9.x.x

Step 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.

Bash
pnpm create vite@latest my-app -- --template react-ts
cd my-app
pnpm install
pnpm dev

Your 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.ts

Option B: Next.js (Recommended for SSR, SSG, and full-stack)

Next.js adds server-side rendering, file-based routing, and API routes.

Bash
npx create-next-app@latest my-app \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir \
  --import-alias "@/*"

cd my-app
npm run dev

Choose 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-intellisense

Or 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 snippetsrfce → 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:

JSON
{
  "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:

JSON
{
  "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

Bash
pnpm add -D eslint @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y

Create eslint.config.ts (ESLint v9 flat config):

TYPESCRIPT
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

Bash
pnpm add -D prettier eslint-config-prettier

Create .prettierrc:

JSON
{
  "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.js

Step 7: TypeScript Configuration

Replace the default tsconfig.json with a stricter, production-grade config:

JSON
{
  "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

TYPESCRIPT
// 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:

TSX
// 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

Bash
# 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 axios

Step 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.tsx

Create the structure:

Bash
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:

Bash
pnpm add -D husky lint-staged

# Initialize husky
npx husky init

# Add pre-commit hook
echo 'npx lint-staged' > .husky/pre-commit

Create .lintstagedrc.json:

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

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

Bash
# .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.com

Important: In Vite, only variables prefixed with VITE_ are exposed to the browser. Never put secrets in VITE_ variables.

TSX
// 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.*.local

Verify Everything Works

Bash
# 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 dev

Common 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.