How a React App is Wired
The files every React project has — index.html, main.tsx,
App.tsx — and how a click on a link ends up rendering a component. Interviewers
often open with "walk me through your project structure", and this is that answer.
How does a React app actually start?
The browser loads one HTML file with one empty <div>.
main.tsx then attaches React to that div, and React builds everything else.
That's the whole chain. Three files do the wiring, and everything below is your app.
| File | Job |
|---|---|
index.html | The single page. Holds <div id="root"> and the script tag. |
src/main.tsx | Entry point. Mounts React and wraps the app in global providers. |
src/App.tsx | Routing. Maps URLs to pages, and applies the layout and route guards. |
There is genuinely only one HTML file. Navigating doesn't fetch a new page —
React Router changes the URL with the history API and swaps which component renders inside
that same #root div (Module 08).
What goes in main.tsx?
Two things only: mount React onto the DOM, and wrap the app in everything that must be available globally.
// index.html (at the project root, not in src/)
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css"; // global styles / Tailwind
import { ThemeProvider } from "./context/ThemeContext";
import { AuthProvider } from "./context/AuthContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 30_000, retry: 1 } }
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<AuthProvider>
<App />
</AuthProvider>
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>
);
| Line | What it does |
|---|---|
createRoot(...) | Connects React to that empty div. React 18+ API — the old one was ReactDOM.render(). |
.render(<App />) | Draws your app inside it. |
<React.StrictMode> | Development-only checks. Deliberately mounts twice to expose missing cleanup (Q87). Removed automatically in the production build. |
| Providers | Anything the whole app needs: auth, theme, query cache, i18n, MSAL. |
! after getElementById | TypeScript non-null assertion — "trust me, this div exists". |
The old ReactDOM.render(<App />, document.getElementById("root")) still works
but logs a warning and disables concurrent features. React 18+ uses
createRoot, and React 19 removed the old API completely. Knowing which
one you're looking at dates a codebase instantly.
main.tsx is the entry point. It imports the root component, finds the
#root div from index.html, and calls
ReactDOM.createRoot(...).render(<App />) — createRoot being the
React 18 API that enables concurrent rendering. It's also where I put everything that must be
global: the QueryClientProvider, auth and theme contexts, and StrictMode, which double-mounts
in development to catch effects with missing cleanup. No business logic lives there — it's
purely wiring.
What goes in App.tsx?
The routing table — which URL renders which page, wrapped in the layout and the auth guard.
// src/App.tsx
export default function App() {
return (
<BrowserRouter>
<Routes>
{/* public */}
<Route path="/login" element={<SignIn />} />
{/* private: guard → layout → pages */}
<Route element={<ProtectedRoute />}>
<Route element={<AppLayout />}>
<Route index path="/" element={<Dashboard />} />
<Route path="/brands" element={<BrandsPage />} />
<Route path="/brands/:id" element={<BrandDetailPage />} />
<Route path="/orders" element={<OrdersPage />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
Read the nesting out loud, because that's how you'd explain it in an interview:
// src/layout/AppLayout.tsx — rendered ONCE, not re-mounted on navigation
export default function AppLayout() {
return (
<div className="layout">
<AppSidebar />
<div className="content">
<AppHeader />
<main><Outlet /></main> {/* the matched page appears here */}
</main>
</div>
);
}
Why does the order of providers matter?
A provider can only be used by components inside it — so anything that depends on another provider must sit further in.
The usual order, outermost first:
- StrictMode — dev checks, wraps everything
- ErrorBoundary — so it can catch failures in the providers below
- QueryClientProvider / MSAL — data and auth infrastructure
- ThemeProvider, i18n — no dependencies, safe anywhere high up
- AuthProvider — often calls an API, so it goes inside the query/HTTP layer
- BrowserRouter — inside anything its routes need; a guard that reads auth must be inside
AuthProvider - App
Providers re-render everything below them when their value changes, so don't put fast-changing state in a top-level provider, and memoize the value object (Q94).
What's in a Vite React project, file by file?
| Command | What it does |
|---|---|
npm run dev | Dev server with hot reload (usually port 5173) |
npm run build | Type-check + bundle into dist/ for deployment |
npm run preview | Serve dist/ locally to test the production build |
# .env
VITE_API_URL=https://localhost:7001
// used in code
const BASE_URL = import.meta.env.VITE_API_URL;
Only variables prefixed VITE_ are exposed, and they are
baked into the bundle at build time — so they are public. Never put a secret
there; secrets belong in the .NET API.
CRA is deprecated. Vite starts in milliseconds because it serves native ES modules in
development and only bundles for production (with Rollup). If a codebase still has
react-scripts in package.json, it's CRA — and migrating to Vite is a
good thing to suggest.
"Walk me through your project" — the answer
Describe it as layers with one direction of dependency, exactly as you'd describe Clean Architecture on the .NET side.
Then give the reasons, not just the boxes:
- One place for cross-cutting concerns. Auth, base URL and error shape live in the API client — if auth changes, one file changes, not forty components.
- Pages stay dumb, services stay smart. That keeps pages readable and lets tests mock a service instead of the network.
- Feature folders scale. With 20+ modules, "everything about stickers is in
one folder" beats one giant
components/directory. - Types mirror the backend DTOs, so a renamed field is a compile error rather than a production bug.
You've built exactly this: an admin portal with 20+ feature modules, a
services/ layer over a single fetch-based api-client returning
{ isSuccess, data, messageCode }, Context for auth/theme/sidebar, and a
ProtectedRoute guard — talking to a .NET 9 Clean Architecture API. Describe that,
then add what you'd improve next: React Query for caching,
lazy-loaded routes, and a shared usePagedList hook for the
repeated list-screen logic. Naming your own improvements is what makes it sound senior.
The browser loads a single
index.html with an empty root div.
main.tsx mounts React with createRoot and wraps the app in the
global providers — query client, auth, theme. App.tsx holds the routing: public
routes, then a protected group where a guard checks auth, a layout renders the sidebar and
header once with an Outlet, and the pages render inside it. Below that it's
layered: pages render, services own the endpoints and types, and a single API client owns the
base URL, the bearer token and a consistent response envelope. Shared concerns live in
context, hooks and components/ui, and the folders are
organised by feature rather than by file type so it scales past twenty modules.