01 — The stack, from main.tsx up
Read this first: this lesson opens the first file the browser executes and discovers that the whole frontend architecture is already visible in it — five providers, a query cache with strong opinions, and a toaster that no page can unmount. By the end you can name every layer in the tree and defend its position.
Time: about 20 minutes. This is the first lesson — it assumes you know basic React and nothing about this repo.
One file owns the boot
Here is frontend/src/main.tsx, whole. Read it once now; the rest of the lesson takes it apart.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider as ReduxProvider } from 'react-redux';
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import axios from 'axios';
import { ColorModeProvider } from './ui/components/color-mode';
import { Toaster } from './ui/components/toaster';
import { store } from './redux/store';
import { system } from './ui/theme';
import App from './App';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import '@fontsource-variable/outfit/index.css';
import './index.css';
const STALE_TIME_MS = 2 * 60 * 1000;
const GC_TIME_MS = 10 * 60 * 1000;
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: STALE_TIME_MS,
gcTime: GC_TIME_MS,
refetchOnWindowFocus: false,
retry: (failureCount, error) => {
if (axios.isAxiosError(error) && error.response && error.response.status >= 400 && error.response.status < 500) {
return false;
}
return failureCount < 1;
},
},
},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ChakraProvider value={system}>
<ColorModeProvider>
<ReduxProvider store={store}>
<QueryClientProvider client={queryClient}>
<App />
<Toaster />
</QueryClientProvider>
</ReduxProvider>
</ColorModeProvider>
</ChakraProvider>
</StrictMode>,
);
Two things happen at module scope, before React renders anything: the queryClient is
constructed, and the Redux store arrives already built from ./redux/store. The rule:
long-lived state objects are created outside the component tree. If the QueryClient were built
inside a component, every remount would mint a fresh empty cache — and StrictMode, which
double-invokes renders in dev to flush out unsafe effects, would make that bug invisible locally
and real in production.
Notice also the font imports. @fontsource/inter and @fontsource-variable/outfit are npm
packages, so Vite bundles the font files and the app serves them from its own origin. No request
ever leaves for a font CDN — which matters because in Docker this app is a static build behind
nginx, and everything it needs must be in the build.
The tree is a dependency declaration
Read the JSX from the outside in. Each provider serves everything nested below it, so the nesting is a literal statement of who depends on whom.
StrictMode is dev-only armor: it double-invokes renders and effects so that an effect which
misbehaves when run twice fails on your machine, not in production. When you see a duplicate API
call in the dev console, check this before blaming the data layer.
ChakraProvider value={system} supplies the design system — not Chakra's default theme, but
the repo's own system from ui/theme.ts (brand tokens, recipes; lesson
06 decodes it). It sits outermost because everything below it renders
Chakra components — including the Toaster, which is not inside App at all. Move Chakra inside
any other provider and whatever ends up above it loses its styling context and crashes.
ColorModeProvider is fourteen lines of the repo's own code wrapping next-themes:
export const ColorModeProvider = (props: ThemeProviderProps) => {
return (
<ThemeProvider
attribute="class"
disableTransitionOnChange
defaultTheme="light"
{...props}
/>
);
};
attribute="class" stamps light or dark onto the <html> element, and Chakra's semantic
tokens key off that class. disableTransitionOnChange exists because without it, flipping the
mode makes every element with a CSS transition animate its color change at once — a whole-page
shimmer instead of a clean swap. Any component that calls useColorMode needs this provider
above it, which in practice means everything.
ReduxProvider store={store} gives useSelector and useDispatch to the tree. Do not read
too much into its size: the store holds exactly one slice — auth. Every other kind of state lives
elsewhere, and lesson 05 explains why that split is deliberate.
QueryClientProvider client={queryClient} is the server-state cache. Every useQuery and
useMutation hook in hooks/api/ — about fifty of them — resolves to this one client and shares
its cache and its defaults, which is why the next section matters so much.
App and Toaster are siblings. App contains the router and every page. The Toaster
deliberately sits beside it, not inside it, so no navigation can ever unmount a toast mid-display.
And because toaster.tsx creates its queue at module scope —
export const toaster = createToaster({
placement: 'top-end',
pauseOnPageIdle: true,
});
— plain non-React modules, like the axios error handling in the API layer, can push toasts by
importing toaster directly. The component only renders the queue; the queue belongs to no
component.
The cache has opinions
Back to the QueryClient defaults. They encode what kind of app this is.
Predict: you open the Employees grid, switch to your email tab for five minutes, then click back. Which network requests fire at the moment the tab regains focus? Write your answer down before reading on.
None. refetchOnWindowFocus: false turns off TanStack Query's most famous default. The upstream
behavior is tuned for dashboards where silently refreshing on focus feels magical. An ERP is the
opposite case: the screen you left is usually a grid holding server-side rows, scroll position,
and half-applied filters, and its users tab out to spreadsheets and email constantly. A focus
refetch would re-fire every mounted list query on every return — load the backend, and worse,
reshuffle rows under the user's cursor. So a refetch happens only when you cause one: a
navigation, a mutation's invalidation, or an explicit refresh.
The two timing constants set the freshness window. staleTime of two minutes means a query
remounted within two minutes of its last fetch serves cache and does not touch the network at
all. gcTime of ten minutes means data for pages you have left is kept that long — navigate
back within the window and the page paints instantly from cache, then revalidates in the
background if the two minutes have passed.
The retry function is the sharpest opinion:
retry: (failureCount, error) => {
if (axios.isAxiosError(error) && error.response && error.response.status >= 400 && error.response.status < 500) {
return false;
}
return failureCount < 1;
},
A 4xx response means the request is wrong — bad input, missing row, or a permission you do not
hold. Asking the same question again gets the same answer, so retrying a 403 only delays the
error toast the user needs to see. Everything else — a network blip, a 502 from a restarting
backend — gets failureCount < 1: exactly one retry, two attempts total. TanStack's default of
three retries with exponential backoff would leave a user staring at a spinner for many seconds
before admitting failure; one quick retry catches the transient cases without hiding the real
ones.
Twelve libraries, one course
package.json lists about forty runtime dependencies. These are the load-bearing ones, and this
course decodes each where its story lives:
| Library | Version | Role here | Decoded in |
|---|---|---|---|
| react + react-dom | ^19.2.6 | UI runtime | this lesson |
| typescript | ~6.0.2 | Language; tsc -b runs before every build | this lesson |
| vite | ^8.0.12 | Dev server + bundler | this lesson |
| react-router | ^7.17.0 | Routing — the unified package, not react-router-dom | lesson 02 |
| axios | ^1.17.0 | HTTP client + auth interceptors | lesson 04 |
| @tanstack/react-query | ^5.101.0 | All server state | lessons 04–05 |
| @reduxjs/toolkit + react-redux | ^2.12.0 / ^9.3.0 | Auth state only | lesson 05 |
| zustand | ^5.0.14 | Small client-state islands | lesson 05 |
| @chakra-ui/react | ^3.36.0 | Component system + theming | lesson 06 |
| ag-grid-community + ag-grid-react | ^33.3.2 | Every data table | lessons 07–09 |
| recharts | ^3.8.1 | Analytics charts | lesson 17 |
| three + @react-three/fiber | ^0.185.1 / ^9.6.1 | 3D scenes, always code-split | lesson 20 |
| react-hook-form + zod | ^7.79.0 / ^3.25.76 | Forms + validation | lesson 21 |
The rest — TipTap, FullCalendar, @pdfme, dnd-kit, the WebSocket clients, and more — are cataloged with their roles in the Frontend reference, which is the authoritative stack table. When you need the exhaustive list, go there, not here.
What the build bakes in
One production reality is worth carrying from lesson one: in the Docker stack there is no Vite
process. The frontend container is nginx serving a static build, which means VITE_API_URL is
baked into the JavaScript at image build time — it defaults to empty, so API calls are
relative URLs that nginx proxies to the backend on the same origin. Editing source does nothing
to a running stack; you rebuild the image or you run the HMR dev loop instead. The full recipe
and its consequences live in the Frontend reference under "The
static-Docker-build reality".
Where this shows up in MotorPH
- frontend/src/main.tsx — the file this lesson decoded
- frontend/src/ui/components/color-mode.tsx — the
next-themeswrapper - frontend/src/ui/components/toaster.tsx — the module-scope toast queue
- frontend/src/redux/store.ts — the one-slice store
- frontend/src/ui/theme.ts — the
systempassed toChakraProvider - frontend/package.json — the authoritative dependency list
- the Frontend reference and ../frontend/state.md — the map this course walks
Recap
- The tree reads StrictMode → Chakra → ColorMode → Redux → QueryClient → App + Toaster, and the nesting is a dependency declaration: each provider must sit above every consumer of its context.
- Chakra is outermost because the Toaster is its farthest consumer — anything rendering
Chakra components below it would crash without the
systemin scope. - The Toaster is App's sibling, and its queue is module-scope — so navigation can never unmount a toast, and non-React code can raise one.
- Long-lived state objects are built outside the tree — a
QueryClientcreated inside a component would lose its cache on every remount, and StrictMode would hide that in dev. - The cache never refetches on focus, holds data fresh for two minutes, and never retries a 4xx — because ERP users tab away constantly, grids hate churn, and a 403 is still a 403 the second time you ask.