The overengineering behind modern web dev
React is probably the most used js library out there and I've always felt guilty not using it, writing my websites in pure vanilla html + css + javascript.
The fact is that React is HARD and COMPLEX and I always wondered if the game was worth the candle.
Why do we actually need React for?
One big deal with React is the use of JSX.
JSX (sometimes referred to as JavaScript XML) is an XML-like extension to the JavaScript language syntax.
So JSX isn’t a separate language. It’s a syntax extension that lets you write something that looks different from normal JavaScript, but gets compiled (by a compiler like Babel) into regular JavaScript function calls.
Example:
const element = <h1>Hello, world!</h1>;
This is not valid plain JavaScript, but Babel will turn it into:
const element = React.createElement("h1", null, "Hello, world!");
Along with this React allows you to work with components which are JavaScript functions that return markup:
function MyButton() {
return (
<button>I'm a button</button>
);
}
So you can just write our function once and reuse the component wherever you want!
There's a catch!
As I said JSX isn't nativaly supported by web browsers, it needs to be compiled to regular javascript, that's where a compiler like Babel comes in handy.
But as complexity grows you may also want the following things:
-
A dev server with hot reload, instant updates when you save files.
-
Optimized builds: Minification, code splitting etc.
-
Bundling: Basically resolving all the
importsin one unique file to make it faster (one of the most famous is Rollup)
So it comes Vite, a local development server that does all of this for you: it has a Hot Module Replacement (HMR) system, which reduces wait times during development, has server-side rendering (SSR), code-splitting, and asynchronous loading.
Along with these it may be convenient to use a package manager like npm that handles files and third party packages.
When creating a vite app for istance you'd run:
npm create vite@latest (this downloads a sample app )
If you want to start from scratch:
npm install -D vite (The -D flag stands for Dev dependency)
We get these files:
|-- node_modules/
|-- package-lock.json
|-- package.json
in the package.json we have all the packages that we've installed along with other configuration data:
{
"name": "test",
"version": "0.0.0",
// Packages used for developing, ex. minification, bundling, hmr...
"devDependencies": {
"vite": "^7.1.3"
},
// Packages used for running
"dependencies": {
"react": "^19.1.1"
},
// Running "npm run dev" will actually run "npx vite" for starting local server
"scripts": {
"dev": "vite",
"build": "vite build", // Building production (bundled and minified) build, outputs to /dist folder
"preview": "vite preview" // Same as build but starts local server as well to test it
},
}
The package-lock.json contains just the same packages but with resolved urls and all specific packages, for instance vite in its turn requires other packages, like the bundler "Rollup", and it automatically adds them for you:
"node_modules/rollup": {
"version": "4.48.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.1.tgz",
"integrity": "sha512-jVG20NvbhTYDkGAty2/Yh7HK6/q3DGSRH4o8ALKGArmMuaauM9kLfoMZ+WliPwA5+JHr2lTn3g557FxBV87ifg==",
...
In the node_modules folder we have the actual downloaded packages.
| node_modules
| @react
| @vite
How projects are built
So how does a real-world project actually look like, and how does it turn into a working website?
First, our source files are organized into a standard directory layout. A typical Vite + React project usually looks like this:
my-app/
├── node_modules/ # Installed packages
├── public/ # Static assets (favicons, images served directly)
├── src/ # Actual code lives here
│ ├── components/ # Reusable JSX components (Header.jsx, Button.jsx)
│ ├── App.jsx # Main application component
│ ├── main.jsx # Entry point: renders App into the DOM
│ └── index.css # Global styles
├── index.html # Main HTML template
└── package.json # Project settings & dependencies
To understand how React boots up, we have to trace the entry chain.
It starts in the root index.html and immediately points to main.jsx.
Unlike traditional web projects with hundreds of lines of markup, a React index.html is minimal.
It only does two main jobs:
-
Provides an empty
<div>where React will render our app. -
Loads
main.jsxusing a native ES module script tag (type="module").
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My React App</title>
</head>
<body>
<!-- 1. The placeholder div where React mounts the UI -->
<div id="root"></div>
<!-- 2. Vite reads this entry script tag to start compiling -->
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
When Vite compiles our site for production, it swaps <script type="module" src="/src/main.jsx"> in index.html with a link to our bundled, minified JS file.
main.jsx is the bridge between plain JavaScript and React.
It grabs that <div id="root"> from the DOM, initializes React inside it, and wraps our App component with routing capabilities so multi-page links work.
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App.jsx';
import './index.css';
// 1. Grab the <div id="root"> element from index.html
const rootElement = document.getElementById('root');
// 2. Create the React Root and mount the application
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
{/* Enables URL listening and client-side routing across our app */}
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
App.jsx is the core component of our application.
// src/App.jsx
import MyButton from './components/MyButton';
export default function App() {
return (
<main>
<h1>Welcome to My App</h1>
<p>This is rendered inside the #root container!</p>
<MyButton />
</main>
);
}
With Vite we will compile and bundle all our .jsx files ending up with only plain JS.
Running npm run build triggers Vite to execute the following steps:
- Crawling the Tree: Vite opens
index.htmland builds a complete map of every file our project imports. - Transpilation: Babel (or ESBuild inside Vite) translates all our custom JSX and modern JavaScript into plain JS (
React.createElementcalls) that any browser can run. - Bundling & Minification: Rollup takes dozens of separate component files and stitches them together into just a handful of optimized JavaScript and CSS files. It strips out whitespace, renames long variable names to single letters, and deletes unused code ("tree-shaking").
- Output (
/dist): Everything is saved into a fresh/dist(distribution) folder.
dist/
├── assets/
│ ├── index-B3x9kL12.js # All our JS + React bundled & minified
│ └── index-C9m1aZ45.css # All our CSS combined
└── index.html # Clean HTML linking to the assets
That tiny /dist folder is our final product.
We upload those static files to a web server (like Vercel), and we're live!
How about more pages?
In traditional web development, having multiple pages (like Home, About, and Contact) meant having actual separate HTML files: index.html, about.html, and contact.html. Whenever a user clicked a link, the browser sent a request to the server, downloaded the new HTML file, and completely refreshed the screen.
With modern React apps, things work differently. You usually have a Single Page Application (SPA). There is still only one index.html file, but a client-side routing library (like react-router-dom) swaps components on the fly when the URL changes, without refreshing the browser.
Here is how a multi-page app project structure actually looks:
my-app/
├── src/
│ ├── components/ # Shared UI pieces across pages
│ │ ├── Navbar.jsx # Stays visible on every page
│ │ └── Footer.jsx
│ │
│ ├── pages/ # Individual page views
│ │ ├── Home.jsx # Home view content
│ │ ├── About.jsx # About view content
│ │ └── Contact.jsx # Contact view content
│ │
│ ├── App.jsx # Sets up routes connecting URLs to Pages
│ └── main.jsx # Entry point
└── index.html # Still just ONE single HTML file!
Instead of linking directly to about.html, App.jsx maps URL paths to our page components:
// App.jsx
import { Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";
import Contact from "./pages/Contact";
import Navbar from "./components/Navbar";
function App() {
return (
<>
<Navbar /> {/* Stays pinned on top */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</>
);
}
If our app gets huge with dozens of pages, bundling everything into a single index.js file would make the initial load painfully slow.
Vite handles this automatically using a feature called Code Splitting:
- Route-based Chunks: Rollup splits our code so that page components (
About.jsx,Contact.jsx) are compiled into separatejschunk files. - Lazy Loading: The user downloads only the JavaScript needed for the
Homepage first. When they click on/about, Vite dynamically fetchesabout-D8a19K.jsin the background.
When you run npm run build, our /dist folder reflects these split chunks:
dist/
├── assets/
│ ├── index-B3x9kL12.js # Core React logic + App setup
│ ├── Home-A19xK3.js # Only loaded when visiting /
│ ├── About-D8a19K.js # Only loaded when visiting /about
│ ├── Contact-F44m2Z.js # Only loaded when visiting /contact
│ └── index-C9m1aZ45.css # Global bundled styles
└── index.html # The single entry point serving it all
This gives you the best of both worlds: a multi-page feel for the user with blazing fast initial page loads.