The Mental Model Shift
Part 2 →
Modern React in Five Concepts
You already think in systems. You reason about data flowing through pipelines, services calling services, and state persisting in databases. That intuition is an asset — but it needs recalibration before it helps you write good frontend code.
The single biggest mistake backend engineers make when picking up React is treating it like a template engine bolted onto a request handler. It isn't. The mental model is fundamentally different, and until that clicks, every abstraction will feel arbitrary.
The Request/Response Mental Model (What You Know)
In server-side code, execution is linear. A request arrives, middleware runs top to bottom, a handler fires, a response leaves. State lives outside your code — in a database, in Redis, on disk. Your code is largely stateless and ephemeral.
Request → [auth middleware] → [validate] → [handler] → [DB query] → ResponseSide effects are explicit. You call db.query() or cache.set(). The output is deterministic given the same input (plus external state). You control when things happen.
The Render-Tree Mental Model (What Frontend Needs)
React's model is a persistent tree that the framework continuously reconciles against a description you provide. You don't imperatively update the DOM. You declare what the UI should look like given the current state, and React figures out the minimum set of DOM mutations required.
This loop runs continuously. There is no "request" — there is a live, in-memory representation of the UI that reacts to state mutations.
Imperative vs Declarative: A Direct Comparison
If you have ever written jQuery, you wrote imperative UI code. You reached into the DOM and changed things:
// Imperative — tell the browser HOW to update
document.getElementById('count').textContent = count + 1;
document.getElementById('btn').disabled = true;React is declarative — you describe WHAT the UI should look like:
// Declarative — describe the desired state
function Counter({ count, loading }: { count: number; loading: boolean }) {
return (
<div>
<span>{count}</span>
<button disabled={loading}>Increment</button>
</div>
);
}React figures out the DOM mutations. You never touch the DOM directly.
Components Are Not Functions Called Once
Here is the subtlety that trips up most backend engineers. A React component is a function, and you will be tempted to reason about it like one: call it once, get output, done.
But React calls your component function every time it needs to re-render — which may be many times per second. The return value is not sent over a wire; it feeds into React's reconciler, which compares it against the previous render and computes a diff.
// This function may run dozens of times per second
function UserCard({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
// Effects run AFTER render, not during
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Pure description — no side effects here
return user ? <div>{user.name}</div> : <div>Loading…</div>;
}The rule: render functions must be pure. The same props and state should always produce the same JSX output. Side effects (API calls, subscriptions, timers) belong in useEffect or event handlers, not in the render body.
State Lives in the Tree, Not in a Store (Initially)
In backend code, state persistence is explicit — you decide where it goes. In React, state is attached to component instances in the tree. When a component unmounts, its state is destroyed. When it remounts, state resets.
Each useState call is scoped to that node in the tree. If UserProfile unmounts and remounts, its local state resets — similar to a stateless server function, not a stateful service.
Thinking in Data Flow Direction
Backend systems often have bidirectional data flows — service A calls service B and B calls A. React enforces unidirectional data flow: data flows down via props, events flow up via callbacks.
// Data flows DOWN: parent passes data to child
// Events flow UP: child calls parent's callback
function Parent() {
const [count, setCount] = useState(0);
return <Child count={count} onIncrement={() => setCount(c => c + 1)} />;
}
function Child({ count, onIncrement }: { count: number; onIncrement: () => void }) {
return <button onClick={onIncrement}>{count}</button>;
}This is analogous to an event-driven system where downstream services emit events that upstream consumers handle — but simplified into a single tree.
The Useful Mapping
| Backend concept | Frontend equivalent |
|---|---|
| Database | Server state / React Query cache |
| In-memory cache | useState / useReducer |
| Request handler | Event handler |
| Middleware | Higher-order component / layout |
| API contract | Props interface (TypeScript) |
| Health check / poll | useEffect with interval |
These mappings are imperfect, but they give you a starting vocabulary. The rest of this series builds on them.
Key Takeaways
- React's model is a continuously reconciled render tree, not a request/response pipeline — your code runs repeatedly, not once.
- Declarative UI means you describe the desired state; the framework computes the DOM mutations.
- Render functions must be pure; side effects belong in
useEffector event handlers. - State is scoped to component instances in the tree and is destroyed when a component unmounts.
- Data flows down via props; events flow up via callbacks — unidirectional by design.
- Your backend intuition about data flow and system boundaries is an asset once reframed in tree and state terms.
Part 2 →
Modern React in Five Concepts