Episode 2 — React Frontend Architecture NextJS / 2.1 — Introduction to React
2.1.f — Project Structure Breakdown
In one sentence: A well-organized React project structure separates concerns by feature or type, making code discoverable, maintainable, and scalable as the application grows from a handful of components to hundreds.
Navigation: ← Setting Up React With Vite · Next → JSX Syntax Rules
1. Default Vite + React Project Structure
When you create a project with npm create vite@latest my-app -- --template react, you get this structure:
my-app/
├── node_modules/ ← Installed dependencies (git-ignored)
├── public/ ← Static assets (served as-is)
│ └── vite.svg
├── src/ ← Application source code
│ ├── assets/ ← Importable assets (processed by Vite)
│ │ └── react.svg
│ ├── App.css ← Styles for App component
│ ├── App.jsx ← Root application component
│ ├── index.css ← Global styles
│ └── main.jsx ← Application entry point
├── .eslintrc.cjs ← ESLint configuration
├── .gitignore ← Git ignore rules
├── index.html ← The single HTML page
├── package.json ← Project metadata and dependencies
├── package-lock.json ← Exact dependency versions
└── vite.config.js ← Vite configuration
For the TypeScript template (react-ts), add:
├── tsconfig.json ← TypeScript configuration
├── tsconfig.node.json ← TypeScript config for Node.js files
└── src/
└── vite-env.d.ts ← Vite type declarations
Every file has a purpose. No file is "just boilerplate."
2. package.json: The Project Manifest
The package.json is the single most important configuration file. It defines what your project is, what it depends on, and how to run it.
{
"name": "my-react-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"vite": "^6.0.0"
}
}
Section-by-Section Breakdown
Metadata:
{
"name": "my-react-app", // Project name (used by npm if published)
"private": true, // Prevents accidental npm publish
"version": "0.0.0", // Semantic version (major.minor.patch)
"type": "module" // Use ES modules (import/export) by default
}
Scripts:
{
"scripts": {
"dev": "vite", // Start dev server (http://localhost:5173)
"build": "vite build", // Create production build in dist/
"lint": "eslint .", // Run ESLint on all files
"preview": "vite preview" // Preview production build locally
}
}
Run scripts with npm run <name>:
npm run dev # Start development server
npm run build # Build for production
npm run lint # Check code quality
npm run preview # Preview production build
Dependencies vs devDependencies:
dependencies:
Packages your app NEEDS at runtime.
Included in the production bundle.
react — The React library (component model, hooks)
react-dom — React's DOM renderer (renders to browser DOM)
devDependencies:
Packages only needed during DEVELOPMENT.
NOT included in the production bundle.
vite — Build tool
eslint — Code linting
@types/react — TypeScript type definitions
@vitejs/plugin-react — Vite's React plugin (JSX transformation)
Version Ranges
"react": "^18.3.1"
^ (caret) = Compatible updates
^18.3.1 means: >=18.3.1 AND <19.0.0
Allows minor and patch updates, not major
~ (tilde) = Patch updates only
~18.3.1 means: >=18.3.1 AND <18.4.0
Exact:
18.3.1 means: exactly 18.3.1
* (star):
* means: any version (dangerous!)
3. node_modules/: The Dependency Folder
node_modules/
├── react/ ← The React library
├── react-dom/ ← React DOM renderer
├── vite/ ← Vite build tool
├── rollup/ ← Bundler (used by Vite for production)
├── esbuild/ ← Fast transformer (used by Vite)
├── eslint/ ← Code linter
├── ... (hundreds more) ← Transitive dependencies
└── .vite/ ← Vite's dependency cache
└── deps/ ← Pre-bundled dependencies
Why node_modules Is Huge
You install 6 packages.
Those 6 packages have their own dependencies.
Those dependencies have dependencies.
And so on.
Your package.json: 6 packages
Actual node_modules: 200-500 packages
Disk size: 100MB - 500MB
This is normal. Don't worry about it.
Rules for node_modules
- Never edit files inside node_modules — changes are overwritten on
npm install - Never commit node_modules to git —
.gitignoreexcludes it - Delete and reinstall if corrupted —
rm -rf node_modules && npm install - The
package-lock.jsonensures reproducibility — exact versions are locked
4. public/: Static Assets
The public/ folder contains files that are served as-is, without any processing by Vite.
public/
├── vite.svg ← Default Vite logo
├── favicon.ico ← Browser tab icon
├── robots.txt ← Search engine crawling rules
├── sitemap.xml ← Sitemap for SEO
├── manifest.json ← PWA manifest
└── images/ ← Images referenced by absolute URL
└── og-image.png ← Social media preview image
How to Reference public/ Files
// Files in public/ are referenced by absolute path from the root
// No import needed
function App() {
return (
<div>
{/* Direct reference to public/vite.svg */}
<img src="/vite.svg" alt="Vite logo" />
{/* Direct reference to public/images/og-image.png */}
<img src="/images/og-image.png" alt="Preview" />
</div>
);
}
<!-- In index.html -->
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
public/ vs src/assets/: When to Use Which
public/ folder:
- Files are NOT processed by Vite
- No hashing, no optimization
- Referenced by absolute URL ("/image.png")
- Good for: favicon, robots.txt, manifest.json,
files referenced by third-party scripts, files that
need a stable URL
src/assets/ folder:
- Files ARE processed by Vite
- Content-hashed filenames (cache busting)
- Can be imported in JavaScript
- Optimized during build (images, SVGs)
- Good for: images used in components, SVGs,
fonts, any asset that's part of the app
// src/assets/ — import the file, Vite processes it
import reactLogo from './assets/react.svg';
function App() {
// reactLogo is a URL string like "/assets/react-B3KkXd.svg"
// The hash changes when the file changes (cache busting)
return <img src={reactLogo} alt="React" />;
}
5. index.html: The Single HTML Page
In a Vite project, index.html is at the project root (not inside public/ like in CRA). This is the entry point for your entire application.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Line-by-Line
<!DOCTYPE html>
<!-- Declares HTML5 document type -->
<html lang="en">
<!-- Root element with language declaration (accessibility + SEO) -->
<meta charset="UTF-8" />
<!-- Character encoding: supports all Unicode characters -->
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<!-- Favicon: the icon shown in the browser tab -->
<!-- References public/vite.svg (absolute path = public folder) -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Responsive design: tells mobile browsers to use device width -->
<!-- Without this, mobile browsers zoom out to fit desktop layout -->
<title>Vite + React</title>
<!-- Page title: shown in browser tab and search results -->
<div id="root"></div>
<!-- THE mount point. React renders the entire app inside this div. -->
<!-- The "root" ID is convention — could be any ID. -->
<!-- This div is empty in the HTML. JavaScript fills it. -->
<script type="module" src="/src/main.jsx"></script>
<!-- type="module" enables ES module imports in the browser -->
<!-- src points to the application entry point -->
<!-- In dev: Vite serves the file directly -->
<!-- In build: Vite replaces this with the bundled script -->
Why index.html Is at the Root (Not in public/)
In CRA, index.html was inside public/. Vite places it at the root because Vite treats index.html as a source file, not a static asset. Vite processes it:
- Resolves
<script>and<link>tags - Injects HMR client code during development
- Replaces references with hashed filenames during build
- Supports HTML transforms via plugins
6. main.jsx: The Application Entry Point
This file is where React takes over the browser.
// main.jsx (JavaScript template)
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
Line-by-Line
import { StrictMode } from 'react';
// StrictMode is a development tool that:
// - Warns about legacy API usage
// - Double-invokes render functions to detect side effects
// - Double-invokes effects to detect missing cleanup
// - Only active in development (stripped in production)
import { createRoot } from 'react-dom/client';
// createRoot is the React 18+ API for rendering
// It replaces the old ReactDOM.render() from React 17
// createRoot enables Concurrent Features (batching, transitions)
import './index.css';
// Import global CSS styles
// Vite injects this CSS into the page
import App from './App.jsx';
// Import the root component of your application
// Everything in your app is a descendant of <App />
createRoot(document.getElementById('root'))
// 1. Find the <div id="root"> in index.html
// 2. Create a React root (the bridge between React and the DOM)
.render(
<StrictMode>
<App />
</StrictMode>
);
// 3. Render the <App /> component inside <StrictMode>
// This is where React takes over. From this point on,
// React manages everything inside <div id="root">.
StrictMode Behavior in Development
StrictMode effects (development only):
1. Double rendering:
React renders every component TWICE to detect impure renders.
If your component has side effects during render, the double
render makes them visible (e.g., console.log fires twice).
2. Double effect invocation:
useEffect runs → cleanup runs → useEffect runs again.
This detects missing cleanup functions.
3. Deprecated API warnings:
Warns if you use legacy lifecycle methods, string refs,
findDOMNode, or legacy context API.
4. Production behavior:
StrictMode does NOTHING in production.
No double renders. No double effects. Zero overhead.
It is purely a development tool.
7. App.jsx: The Root Component
// App.jsx (default template)
import { useState } from 'react';
import reactLogo from './assets/react.svg';
import viteLogo from '/vite.svg';
import './App.css';
function App() {
const [count, setCount] = useState(0);
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
);
}
export default App;
This is a working demo component. In a real project, you'd replace it with your application's structure:
// App.jsx (realistic starting point)
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import NotFoundPage from './pages/NotFoundPage';
function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
8. CSS Files: index.css and App.css
index.css: Global Styles
/* index.css — global styles applied to the entire app */
/* CSS Reset / Normalization */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Root-level styles */
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color: #213547;
background-color: #ffffff;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
min-height: 100vh;
}
index.css is imported in main.jsx and applies to the entire application. It's the right place for:
- CSS resets/normalization
- CSS custom properties (variables)
- Base typography
- Global layout rules
App.css: Component-Scoped Styles
/* App.css — styles specific to the App component */
/* Note: These are NOT truly scoped — they're global CSS class names */
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.card {
padding: 2em;
}
In a real project, you'd typically use one of these approaches for component styling:
Styling approaches:
1. CSS Modules (built into Vite):
Button.module.css → import styles from './Button.module.css'
Automatically scoped to the component
2. Tailwind CSS:
className="bg-blue-500 text-white px-4 py-2 rounded"
Utility classes, no separate CSS files
3. styled-components / Emotion:
const Button = styled.button`background: blue;`
CSS-in-JS, fully scoped
4. Plain CSS files:
import './Button.css'
Global scope (risk of name collisions)
9. assets/: Importable Assets
src/assets/
├── react.svg ← SVG logo
├── images/ ← Component images
│ ├── hero.jpg
│ └── avatar.png
├── fonts/ ← Custom fonts
│ ├── Inter-Regular.woff2
│ └── Inter-Bold.woff2
└── icons/ ← SVG icons
├── arrow.svg
└── check.svg
How Asset Imports Work
// Importing an image returns a URL string
import heroImage from './assets/images/hero.jpg';
// heroImage = "/assets/hero-Bk3x8fj.jpg" (hashed in production)
function Hero() {
return <img src={heroImage} alt="Hero" />;
}
// Importing CSS
import './assets/fonts/fonts.css';
// Vite injects this CSS into the page
// Importing SVG as URL
import arrowIcon from './assets/icons/arrow.svg';
// arrowIcon = "/assets/arrow-Df3k2.svg"
// Importing SVG as React component (with vite-plugin-svgr)
import { ReactComponent as ArrowIcon } from './assets/icons/arrow.svg';
// <ArrowIcon width={24} height={24} />
Asset Size Thresholds
Vite's default behavior for assets:
Small files (< 4KB):
Inlined as base64 data URLs
No separate HTTP request
<img src="data:image/svg+xml;base64,PHN2Zy..." />
Large files (>= 4KB):
Output as separate files with content hash
<img src="/assets/hero-Bk3x8fj.jpg" />
Configure the threshold:
// vite.config.js
export default defineConfig({
build: {
assetsInlineLimit: 4096, // bytes (default: 4096 = 4KB)
},
});
10. Recommended Folder Structures
The default Vite template is minimal. As your project grows, you need structure. Here are three approaches:
Approach 1: Flat (Small Projects, <20 Components)
src/
├── components/
│ ├── Button.jsx
│ ├── Button.module.css
│ ├── Card.jsx
│ ├── Card.module.css
│ ├── Header.jsx
│ ├── Footer.jsx
│ ├── Modal.jsx
│ └── Sidebar.jsx
├── pages/
│ ├── HomePage.jsx
│ ├── AboutPage.jsx
│ └── ContactPage.jsx
├── hooks/
│ ├── useAuth.js
│ └── useLocalStorage.js
├── utils/
│ ├── formatDate.js
│ └── api.js
├── App.jsx
├── App.css
├── main.jsx
└── index.css
Pros: Simple, easy to navigate, low overhead. Cons: Breaks down when you have 50+ components.
Approach 2: Grouped by Type (Medium Projects, 20-100 Components)
src/
├── components/
│ ├── ui/ ← Generic, reusable UI elements
│ │ ├── Button/
│ │ │ ├── Button.jsx
│ │ │ ├── Button.module.css
│ │ │ ├── Button.test.jsx
│ │ │ └── index.js
│ │ ├── Input/
│ │ ├── Modal/
│ │ ├── Card/
│ │ └── Dropdown/
│ ├── layout/ ← Layout components
│ │ ├── Header/
│ │ ├── Footer/
│ │ ├── Sidebar/
│ │ └── Layout/
│ └── common/ ← Shared domain components
│ ├── UserAvatar/
│ ├── SearchBar/
│ └── ErrorBoundary/
├── pages/
│ ├── Home/
│ │ ├── HomePage.jsx
│ │ ├── HomePage.module.css
│ │ └── index.js
│ ├── Dashboard/
│ └── Settings/
├── hooks/
│ ├── useAuth.js
│ ├── useDebounce.js
│ ├── useMediaQuery.js
│ └── useLocalStorage.js
├── context/
│ ├── AuthContext.jsx
│ └── ThemeContext.jsx
├── services/
│ ├── api.js
│ ├── auth.js
│ └── storage.js
├── utils/
│ ├── formatDate.js
│ ├── validation.js
│ └── constants.js
├── styles/
│ ├── globals.css
│ ├── variables.css
│ └── reset.css
├── assets/
│ ├── images/
│ ├── icons/
│ └── fonts/
├── types/ ← TypeScript types (if using TS)
│ ├── user.ts
│ └── api.ts
├── App.jsx
└── main.jsx
Pros: Clear separation by responsibility, scalable to medium size. Cons: Finding all files for a feature requires jumping between folders.
Approach 3: Feature-Based (Large Projects, 100+ Components)
src/
├── features/
│ ├── auth/ ← Everything for authentication
│ │ ├── components/
│ │ │ ├── LoginForm.jsx
│ │ │ ├── SignupForm.jsx
│ │ │ ├── ForgotPassword.jsx
│ │ │ └── ProtectedRoute.jsx
│ │ ├── hooks/
│ │ │ ├── useAuth.js
│ │ │ └── usePermissions.js
│ │ ├── services/
│ │ │ └── authApi.js
│ │ ├── context/
│ │ │ └── AuthContext.jsx
│ │ ├── utils/
│ │ │ └── tokenStorage.js
│ │ ├── types/
│ │ │ └── auth.ts
│ │ └── index.js ← Public API for this feature
│ │
│ ├── dashboard/ ← Everything for the dashboard
│ │ ├── components/
│ │ │ ├── DashboardPage.jsx
│ │ │ ├── StatsCard.jsx
│ │ │ ├── RecentActivity.jsx
│ │ │ └── ChartWidget.jsx
│ │ ├── hooks/
│ │ │ └── useDashboardData.js
│ │ ├── services/
│ │ │ └── dashboardApi.js
│ │ └── index.js
│ │
│ ├── products/ ← Everything for products
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── index.js
│ │
│ └── cart/ ← Everything for shopping cart
│ ├── components/
│ ├── hooks/
│ ├── services/
│ └── index.js
│
├── shared/ ← Shared across features
│ ├── components/
│ │ ├── ui/ ← Design system components
│ │ │ ├── Button/
│ │ │ ├── Input/
│ │ │ └── Modal/
│ │ └── layout/
│ │ ├── Header/
│ │ ├── Footer/
│ │ └── Layout/
│ ├── hooks/
│ │ ├── useDebounce.js
│ │ └── useMediaQuery.js
│ ├── utils/
│ │ ├── formatDate.js
│ │ └── validation.js
│ ├── types/
│ │ └── common.ts
│ └── styles/
│ ├── globals.css
│ └── variables.css
│
├── App.jsx
└── main.jsx
Pros:
- Colocation: Everything for a feature is in one folder
- Team scalability: Different teams can own different features
- Clear boundaries: Features communicate through their
index.jspublic API - Easy deletion: Remove a feature = delete its folder
Cons:
- More initial setup
- Must discipline cross-feature imports (use
index.jsexports) - Shared code decisions can be tricky
11. Feature-Based Structure: Deep Dive
The feature-based approach is the most scalable. Here's how to implement it properly.
The index.js Public API Pattern
Each feature exposes a public API through its index.js:
// src/features/auth/index.js
// Components: what other features can import
export { LoginForm } from './components/LoginForm';
export { SignupForm } from './components/SignupForm';
export { ProtectedRoute } from './components/ProtectedRoute';
// Hooks: what other features can use
export { useAuth } from './hooks/useAuth';
export { usePermissions } from './hooks/usePermissions';
// Context: if needed by other features
export { AuthProvider } from './context/AuthContext';
// Types: shared type definitions
export type { User, AuthState } from './types/auth';
// DO NOT export:
// - Internal components (PasswordStrengthMeter, etc.)
// - Internal hooks (useAuthForm, etc.)
// - Services (authApi — only used internally)
// - Utils (tokenStorage — implementation detail)
Import Rules
// GOOD: Import from the feature's public API
import { useAuth, LoginForm } from '@/features/auth';
import { ProductCard } from '@/features/products';
// GOOD: Import shared utilities
import { Button } from '@/shared/components/ui/Button';
import { useDebounce } from '@/shared/hooks/useDebounce';
// BAD: Reach into a feature's internal structure
import { authApi } from '@/features/auth/services/authApi';
import { PasswordStrengthMeter } from '@/features/auth/components/PasswordStrengthMeter';
// These are internal details — they might change without notice
Feature Communication
Feature communication rules:
auth ←→ dashboard:
dashboard imports from auth's public API (useAuth)
auth does NOT import from dashboard
products ←→ cart:
cart imports from products' public API (Product type, ProductCard)
products does NOT import from cart
Shared ←→ any feature:
Any feature can import from shared/
shared/ NEVER imports from features/
Feature dependency direction:
shared/ (zero dependencies on features)
↑
features/auth (depends on shared/)
↑
features/dashboard (depends on shared/ and auth)
↑
features/products (depends on shared/)
↑
features/cart (depends on shared/ and products)
12. Index Files for Clean Imports (Barrel Exports)
An index file re-exports everything from a directory, enabling cleaner imports:
src/components/ui/
├── Button/
│ ├── Button.jsx
│ ├── Button.module.css
│ └── index.js ← Re-exports Button
├── Input/
│ ├── Input.jsx
│ └── index.js
├── Modal/
│ ├── Modal.jsx
│ └── index.js
└── index.js ← Barrel: re-exports all UI components
// src/components/ui/Button/index.js
export { Button } from './Button';
export { default as Button } from './Button'; // if default export
// src/components/ui/index.js (barrel file)
export { Button } from './Button';
export { Input } from './Input';
export { Modal } from './Modal';
export { Dropdown } from './Dropdown';
export { Card } from './Card';
// Without barrel exports:
import { Button } from '@/components/ui/Button/Button';
import { Input } from '@/components/ui/Input/Input';
import { Modal } from '@/components/ui/Modal/Modal';
// With barrel exports:
import { Button, Input, Modal } from '@/components/ui';
Barrel Export Caveat: Tree Shaking
Barrel files can hurt tree shaking if not used carefully:
// If you import ONE thing from a barrel:
import { Button } from '@/components/ui';
// The bundler needs to evaluate the entire barrel to find Button.
// If the barrel re-exports components with side effects
// (like CSS imports), all of them might be included.
// Mitigation: Use named re-exports (not default) and ensure
// components are side-effect free.
// In package.json (for library authors):
{
"sideEffects": false // Tells bundler all files are side-effect free
}
13. Environment Files
Project root:
├── .env ← All environments
├── .env.local ← All environments (git-ignored)
├── .env.development ← npm run dev only
├── .env.development.local ← npm run dev only (git-ignored)
├── .env.production ← npm run build only
└── .env.production.local ← npm run build only (git-ignored)
# .env
VITE_APP_NAME=My React App
VITE_APP_VERSION=1.0.0
# .env.development
VITE_API_URL=http://localhost:8080/api
VITE_DEBUG_MODE=true
# .env.production
VITE_API_URL=https://api.myapp.com
VITE_DEBUG_MODE=false
# .env.local (git-ignored — secrets go here)
VITE_GOOGLE_MAPS_KEY=AIzaSyB...
VITE_STRIPE_PUBLIC_KEY=pk_test_...
Which Files to Commit
COMMIT to git:
.env ← Default values (no secrets)
.env.development ← Dev-specific values (no secrets)
.env.production ← Prod-specific values (no secrets)
DO NOT COMMIT:
.env.local ← Contains secrets
.env.development.local ← Contains secrets
.env.production.local ← Contains secrets
.gitignore already excludes *.local files.
14. .gitignore Patterns for React Projects
# .gitignore for a Vite + React project
# Dependencies
node_modules/
# Build output
dist/
dist-ssr/
build/
# Environment files with secrets
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Debug logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# TypeScript cache
*.tsbuildinfo
# Vite cache
node_modules/.vite/
# Test coverage
coverage/
# Storybook build
storybook-static/
15. Configuration Files
.eslintrc.cjs (or eslint.config.js)
ESLint enforces code quality rules:
// eslint.config.js (ESLint 9+ flat config)
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
];
Key rules:
react-hooks/rules-of-hooks— Ensures hooks are called correctlyreact-hooks/exhaustive-deps— Ensures effect dependencies are completereact-refresh/only-export-components— Ensures HMR works correctly
Prettier Configuration
Prettier formats code automatically:
// .prettierrc
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"arrowParens": "always",
"bracketSpacing": true,
"jsxSingleQuote": false
}
# Install
npm install --save-dev prettier
# Add script to package.json
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,json}\""
tsconfig.json (TypeScript)
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "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,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
16. Key Takeaways
-
Every file in the default template has a purpose.
index.htmlis the entry point,main.jsxbootstraps React,App.jsxis the root component, and configuration files control build behavior. -
public/is for static files,src/assets/is for imported files. Files inpublic/are served as-is. Files insrc/assets/are processed, hashed, and optimized by Vite. -
main.jsxbridges HTML and React.createRoot(document.getElementById('root')).render(<App />)is where React takes control of the DOM. -
StrictMode is a development-only safety net. It double-renders components and effects to surface bugs. It has zero production overhead.
-
dependencies are for runtime; devDependencies are for build time. React goes in
dependencies. Vite, ESLint, and TypeScript go indevDependencies. -
Feature-based folder structure scales best. Colocating all files for a feature (components, hooks, services, types) in one folder makes code discoverable and teams autonomous.
-
Barrel exports (
index.js) clean up imports but must be used carefully to avoid tree-shaking issues. -
Environment variables must be prefixed with
VITE_to be accessible in client code. Never put secrets in committed.envfiles — use.env.local(git-ignored). -
Configuration files (ESLint, Prettier, tsconfig) enforce consistency. Set them up once and they prevent entire categories of bugs and style inconsistencies across the team.
-
The
node_modules/folder is generated, huge, and must never be committed.package-lock.jsonensures every developer gets the exact same dependencies.
Explain-It Challenge
-
A new team member asks: "Why is
reactindependenciesbutviteindevDependencies? They both seem important." Explain the difference and what would happen if you swapped them. -
Your team is scaling from 10 components to 100. You currently use a flat
src/components/folder. Propose a migration plan to a feature-based structure. Which files move where? What import patterns change? -
Explain why Vite places
index.htmlat the project root instead of insidepublic/, and what Vite does toindex.htmlduring the build process.
Navigation: ← Setting Up React With Vite · Next → JSX Syntax Rules