React Masterclass for Senior Full-Stack Developers: Components, Hooks and Production Architecture
Faz, let’s rebuild this properly.
This article is aimed at a senior full-stack developer who has touched React, built a few hobby screens, maybe created components, used useState, called an API with fetch, and now wants to understand React properly.
Not like a bootcamp student.
Like an experienced engineer asking:
What actually bootstraps a React application? Why does React exist? Why were hooks added? Why does state behave strangely sometimes? Why do effects cause bugs? Why do people use React Router, Redux, Zustand, TanStack Query and Next.js alongside React? What is React responsible for, and what is outside React’s job? How do I explain React confidently in a serious interview or architecture discussion?
That is the session.
React is currently documented at react.dev, and the React versions page lists the latest major/minor documentation target as React 19.2. React’s stable releases follow semantic versioning principles, and React’s own blog is the official place for important updates, releases and deprecation notices. (React)
So let’s talk like engineers.
1. Why React exists
React exists because user interfaces became too dynamic for manual DOM manipulation to remain pleasant.
Imagine a loan management dashboard.
The user can filter loan products, submit applications, see status changes, open modals, validate forms, search, paginate, refresh data, handle loading and show errors.
Without React, you may write browser code like this:
const list = document.getElementById("loan-products");
const button = document.getElementById("refresh-button");
button.addEventListener("click", async () => {
list.innerHTML = "";
const response = await fetch("/api/loan-products");
const products = await response.json();
for (const product of products) {
const item = document.createElement("li");
item.textContent = `${product.name} - ${product.interestRate}%`;
list.appendChild(item);
}
});
This is not evil. It works. But the problem is that you are manually telling the browser what to change.
Clear this element. Fetch data. Create nodes. Set text. Append children. Attach events.
As the screen grows, the DOM manipulation becomes harder to reason about.
React changes the mental model.
Instead of saying:
“Here is how to mutate the DOM.”
You say:
“For this state, this is what the UI should look like.”
function LoanProductList({ products }) {
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - {product.interestRate}%
</li>
))}
</ul>
);
}
This is the big idea:
UI = function of state
React’s job is to let you describe the UI declaratively. When state changes, React calculates what the UI should look like now and updates the screen.
Best practice: do not treat React as a fancy HTML string generator. Treat React as a state-driven UI model.
2. A short React history that matters to you
You do not need to memorise every React release, but you should understand the major shifts.
Early React was heavily class-component based. You wrote components as classes and used lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.
React 16.8 was a major turning point because it introduced Hooks as a stable feature. Hooks allow function components to use state and lifecycle-style React features without writing classes. (Engineering at Meta)
React 18 introduced major foundations such as automatic batching, startTransition, and streaming server-side rendering with Suspense; many React 18 capabilities sit on top of the concurrent renderer, which allows React to prepare multiple versions of UI work behind the scenes. (React)
In 2023, the official documentation moved to react.dev, and the new docs deliberately teach modern React using function components and Hooks from the beginning. (React)
React 19 became stable in December 2024 and added newer features such as Actions and improvements around forms, transitions, use, refs and server-oriented architecture. (React)
React 19.2 later added features including , useEffectEvent, cacheSignal, React Performance Tracks and partial pre-rendering support in React DOM. (React)
Why does this matter?
Because if you learn React from old tutorials, you may learn class components first. If you learn React from modern React, you start with functions, hooks, state snapshots, effects, composition and framework-aware patterns.
For interviews, say this:
“Modern React is mostly written with function components and Hooks. Class components are still supported, but React’s current teaching model focuses on components as functions of props and state, with Hooks used for state, effects, refs, context and other React features.”
3. What bootstraps a React application?
A React app does not begin with a component magically appearing. Something must mount React into the browser DOM.
A typical setup has an index.html file:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Loan Management Supermarket</title>
</head>
<body>
<!--
React will take control of this empty DOM node.
The browser receives this normal HTML first.
Then JavaScript loads.
Then React mounts our component tree inside this root.
-->
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Then your React entry point may look like this:
// src/main.jsx
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
/*
createRoot tells React:
"This DOM element is the root of my React application.
Manage everything inside it."
React 18 introduced the modern root API. Older apps used ReactDOM.render,
but modern React apps normally use createRoot.
*/
const rootElement = document.getElementById("root");
createRoot(rootElement).render(
/*
StrictMode is a development helper.
It does not render extra UI.
It helps reveal unsafe side effects and patterns during development.
In development, it may intentionally call some logic more than once
to help you find code that is not resilient.
*/
<React.StrictMode>
<App />
</React.StrictMode>
);
Then App is your top-level component:
// src/App.jsx
export default function App() {
return (
<main>
<h1>Loan Management Supermarket</h1>
<p>Compare loan products and submit applications.</p>
</main>
);
}
What is happening?
The browser loads HTML.
The JavaScript bundle loads.
React finds #root.
React renders .
App returns JSX.
React turns JSX into a UI description.
React updates the DOM.
Best practice: understand that React controls only the root you give it. It does not magically control the whole page unless your whole page is inside the root.
4. JSX: what it is and why React uses it
JSX is syntax for describing UI inside JavaScript.
This:
const heading = <h1>Loan Products</h1>;
is not raw HTML.
Conceptually, it describes a React element:
const heading = {
type: "h1",
props: {
children: "Loan Products"
}
};
Why was JSX added?
Because UI logic and UI structure are often closely connected. Instead of inventing a separate template language, React lets you write UI descriptions close to the JavaScript logic that controls them.
Example:
function LoanProductCard({ product }) {
const isExpensive = product.interestRate > 10;
return (
<article className="loan-card">
<h2>{product.name}</h2>
<p>
Interest rate: {product.interestRate}%
</p>
{isExpensive && (
<p className="warning">
This product has a high interest rate.
</p>
)}
</article>
);
}
Notice how JavaScript and UI work together.
The condition is JavaScript:
product.interestRate > 10
The UI is JSX:
<p className="warning">...</p>
Best practices:
Do not put heavy business logic inside JSX.
Do not make JSX unreadable with deeply nested ternaries.
Extract components when the UI section has a clear meaning.
Use className, not class.
Use htmlFor, not for.
Remember JSX expressions must return one parent wrapper, or use fragments.
return (
<>
<h1>Dashboard</h1>
<LoanProductList products={products} />
</>
);
A fragment lets you return multiple sibling elements without adding an unnecessary DOM wrapper.
5. Components: what they are and why React is built around them
A component is a reusable unit of UI and behaviour.
But for a senior developer, that definition is too soft.
A component is a boundary.
It says:
What data do I need? What do I display? What state do I own? What events do I expose? What responsibilities do I not own?
Example:
function LoanProductCard({ product, onApply }) {
return (
<article className="loan-card">
<h2>{product.name}</h2>
<p>Lender: {product.lenderName}</p>
<p>Rate: {product.interestRate}%</p>
<p>Max amount: £{product.maxAmount.toLocaleString()}</p>
<button onClick={() => onApply(product.id)}>
Apply
</button>
</article>
);
}
Why were components added as the backbone?
Because large UIs are impossible to manage as one big file. Components allow composition. You build small meaningful pieces and combine them into screens.
function LoanProductsPage() {
return (
<section>
<PageHeader title="Loan Products" />
<LoanProductFilters />
<LoanProductList />
</section>
);
}
This is like breaking a large backend system into services, classes or modules. A component should have a reason to exist.
Best practices:
Keep components focused. Do not fetch data inside every small display component. Use props to pass data. Use callbacks to notify parents about events. Prefer composition over giant configurable components. Avoid components that do API calls, navigation, validation, formatting and rendering all in one place.
A bad component knows too much:
function LoanProductCard({ product }) {
async function apply() {
const token = localStorage.getItem("token");
await fetch(`/api/products/${product.id}/apply`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`
}
});
window.location.href = "/applications";
}
return <button onClick={apply}>Apply</button>;
}
This component knows token storage, API URL and navigation. That is too much.
Better:
function LoanProductCard({ product, onApply }) {
return (
<button onClick={() => onApply(product.id)}>
Apply
</button>
);
}
Let the page decide what applying means.
6. Props: what they are and why they matter
Props are inputs passed from a parent component to a child component.
function WelcomeMessage({ name }) {
return <h1>Welcome, {name}</h1>;
}
function App() {
return <WelcomeMessage name="Faz" />;
}
Why do props exist?
Because components need to be reusable. Props allow the parent to configure the child.
In backend terms, props are like method parameters.
RenderCustomerCard(customer);
In React:
<CustomerCard customer={customer} />
Props should be read-only. A child should not mutate a prop.
Bad:
function LoanProductCard({ product }) {
product.interestRate = 0; // Do not mutate props
return <p>{product.interestRate}</p>;
}
Better:
function LoanProductCard({ product }) {
const displayRate = `${product.interestRate}%`;
return <p>{displayRate}</p>;
}
Best practices:
Pass only what the child needs. Do not pass huge objects everywhere by habit. Use clear prop names. Avoid deeply nested prop drilling when many layers do not care about the data. Use children for flexible component composition.
Example with children:
function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}
function Dashboard() {
return (
<Card title="Pending Applications">
<p>You have 12 applications waiting for review.</p>
</Card>
);
}
This is flexible and clean.
7. State: what it is and why React added it
State is data that changes over time and affects rendering.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Why was state added?
Because UI is interactive. Users click, type, select, open, close, submit and navigate. The screen must respond to changing data.
In React, when state changes, React renders the component again.
But here is the key mental model: state is a snapshot.
Each render has its own state values. If you write this:
function Counter() {
const [count, setCount] = useState(0);
function increaseThreeTimes() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
return (
<button onClick={increaseThreeTimes}>
Count: {count}
</button>
);
}
You may expect +3, but you may get +1, because each call uses the same count from the current render.
Better:
function increaseThreeTimes() {
/*
Functional update form.
React gives us the latest queued value.
This is safer when the next state depends on the previous state.
*/
setCount(current => current + 1);
setCount(current => current + 1);
setCount(current => current + 1);
}
Best practices:
Use state for data that affects UI. Do not use state for values you can calculate during render. Keep state as local as possible. Use functional updates when next state depends on previous state. Treat state as immutable.
Bad mutation:
function updateAmount(value) {
form.requestedAmount = value;
setForm(form);
}
Good immutable update:
function updateAmount(value) {
setForm(current => ({
...current,
requestedAmount: value
}));
}
React’s modern docs teach state as a snapshot and emphasise that props and state should not be mutated because predictable rendering depends on purity and immutability. (React)
8. Hooks: what they are and why they were added
Hooks are functions that let function components use React features such as state, effects, refs and context.
The most famous hooks are:
useState
useEffect
useRef
useMemo
useCallback
useReducer
useContext
useTransition
Why were hooks added?
Before hooks, stateful logic often required class components. Reusing logic across components often needed patterns like higher-order components or render props, which could become awkward. Hooks allowed function components to “hook into” React state and lifecycle features directly. React 16.8 was the first stable React release with Hooks. (Engineering at Meta)
Example:
function LoanProductSearch() {
const [searchText, setSearchText] = useState("");
return (
<input
value={searchText}
onChange={event => setSearchText(event.target.value)}
placeholder="Search loan products"
/>
);
}
Hooks have rules.
Bad:
function Page({ isAdmin }) {
if (isAdmin) {
const [notes, setNotes] = useState("");
}
return <div>Page</div>;
}
Good:
function Page({ isAdmin }) {
const [notes, setNotes] = useState("");
return (
<div>
{isAdmin && (
<AdminNotes value={notes} onChange={setNotes} />
)}
</div>
);
}
Why?
React relies on hooks being called in the same order on every render.
Best practices:
Call hooks only at the top level.
Call hooks from React components or custom hooks.
Do not call hooks inside loops, conditions or nested functions.
Name custom hooks with use.
Use hooks to express behaviour, not to hide chaos.
9. useEffect: what it is and why it causes confusion
useEffect lets your component synchronise with something outside React.
Examples:
API calls. Timers. Subscriptions. Browser storage. WebSocket or SignalR connections. Third-party widgets.
Example:
function ApplicationStatusWatcher({ applicationId }) {
const [status, setStatus] = useState("Loading");
useEffect(() => {
/*
This effect synchronises the component with the backend API.
It runs when applicationId changes.
*/
let cancelled = false;
async function loadStatus() {
const response = await fetch(`/api/applications/${applicationId}`);
const application = await response.json();
if (!cancelled) {
setStatus(application.status);
}
}
loadStatus();
/*
Cleanup prevents updating state after this effect is no longer relevant.
*/
return () => {
cancelled = true;
};
}, [applicationId]);
return <p>Status: {status}</p>;
}
Why was it added?
Function components needed a way to perform side effects that class components used lifecycle methods for. Instead of splitting logic across componentDidMount, componentDidUpdate and componentWillUnmount, an effect groups setup and cleanup by purpose.
Best practices:
Use effects for external synchronisation. Do not use effects for simple derived values. Always understand the dependency array. Clean up timers, subscriptions and connections. Avoid turning effects into hidden workflow engines.
Bad:
useEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);
Better:
const fullName = `${firstName} ${lastName}`;
If it can be calculated during render, calculate it during render.
React’s docs describe effects as synchronising with external systems, and Strict Mode can expose missing cleanup during development. (React)
10. Custom hooks: what they are and why serious apps use them
A custom hook is a function that reuses stateful logic.
A component reuses UI. A hook reuses behaviour.
Example:
function useLoanProducts(filters) {
const [state, setState] = useState({
data: [],
loading: false,
error: null
});
useEffect(() => {
let cancelled = false;
async function loadProducts() {
setState({
data: [],
loading: true,
error: null
});
try {
const queryString = new URLSearchParams(filters).toString();
const response = await fetch(`/api/loan-products?${queryString}`);
if (!response.ok) {
throw new Error("Could not load loan products.");
}
const data = await response.json();
if (!cancelled) {
setState({
data,
loading: false,
error: null
});
}
} catch (error) {
if (!cancelled) {
setState({
data: [],
loading: false,
error
});
}
}
}
loadProducts();
return () => {
cancelled = true;
};
}, [filters.loanType, filters.maxRate]);
return state;
}
Used like this:
function LoanProductsPage() {
const [filters, setFilters] = useState({
loanType: "",
maxRate: ""
});
const {
data: products,
loading,
error
} = useLoanProducts(filters);
if (loading) return <p>Loading loan products...</p>;
if (error) return <p>{error.message}</p>;
return (
<>
<LoanProductFilters value={filters} onChange={setFilters} />
<LoanProductList products={products} />
</>
);
}
Why are custom hooks important?
Because they stop your pages becoming huge. The page reads like a use case, while the hook hides the loading behaviour.
Best practices:
Use custom hooks for reusable behaviour.
Do not create a hook that does everything.
Keep hook names clear: useLoanProducts, useDebouncedValue, useCurrentUser.
Return a simple shape: data, loading, error, actions.
Move serious server-state concerns to a server-state library when needed.
11. Forms: what they are in React and why they matter
Forms are where hobby React becomes business React.
A hobby form has an input and a button.
A business form has:
Values. Validation. Touched fields. Dirty state. Submit state. Server errors. Disabled fields. Double-submit protection. Accessibility.
Controlled input:
function LoanApplicationForm({ onSubmit }) {
const [values, setValues] = useState({
applicantName: "",
email: "",
requestedAmount: ""
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
function updateField(field, value) {
setValues(current => ({
...current,
[field]: value
}));
/*
Clear the error when the user edits that field.
This improves user experience.
*/
setErrors(current => ({
...current,
[field]: undefined
}));
}
function validate() {
const nextErrors = {};
if (!values.applicantName.trim()) {
nextErrors.applicantName = "Applicant name is required.";
}
if (!values.email.includes("@")) {
nextErrors.email = "Enter a valid email address.";
}
if (Number(values.requestedAmount) <= 0) {
nextErrors.requestedAmount = "Requested amount must be greater than zero.";
}
return nextErrors;
}
async function handleSubmit(event) {
event.preventDefault();
const validationErrors = validate();
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0) {
return;
}
try {
setIsSubmitting(true);
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit} noValidate>
<label>
Applicant Name
<input
value={values.applicantName}
onChange={e => updateField("applicantName", e.target.value)}
disabled={isSubmitting}
/>
</label>
{errors.applicantName && (
<p role="alert">{errors.applicantName}</p>
)}
<label>
Email
<input
value={values.email}
onChange={e => updateField("email", e.target.value)}
disabled={isSubmitting}
/>
</label>
{errors.email && (
<p role="alert">{errors.email}</p>
)}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit Application"}
</button>
</form>
);
}
Why use controlled forms?
Because the UI can always reflect the state. The input value is not hidden inside the DOM; it is visible in React state. React does not require every form to be controlled: uncontrolled inputs and modern form-action patterns can also be appropriate. Choose deliberately based on validation, interaction and performance needs.
Best practices:
Use controlled inputs for predictable forms. For larger forms, consider a form library. Always handle loading and server errors. Disable submit while submitting. Do not rely on frontend validation for security. Backend validation remains the authority.
Senior phrase:
“Frontend validation improves user experience; backend validation protects the system.”
12. Context and reducers: what they are and when to use them
Context lets you pass values deeply without manually passing props through every level.
Example:
const AuthContext = createContext(null);
function App() {
const user = {
name: "Faz",
role: "Broker"
};
return (
<AuthContext.Provider value={user}>
<Dashboard />
</AuthContext.Provider>
);
}
function UserMenu() {
const user = useContext(AuthContext);
return <p>Welcome, {user.name}</p>;
}
Why was Context added?
Because prop drilling becomes annoying when many components need the same cross-cutting value.
Good use cases:
Authenticated user. Theme. Language. Tenant. Feature flags. Permissions.
Bad use case:
Every piece of changing screen state.
Do not create this:
<AppContext.Provider value={{
user,
theme,
loanProducts,
selectedProduct,
formValues,
errors,
notifications,
filters
}}>
This becomes a hidden global bag.
Reducer is useful when state changes become workflow-like.
function loanApplicationReducer(state, action) {
switch (action.type) {
case "field_changed":
return {
...state,
values: {
...state.values,
[action.field]: action.value
}
};
case "submit_started":
return {
...state,
status: "submitting"
};
case "submit_failed":
return {
...state,
status: "failed",
error: action.error
};
default:
return state;
}
}
Best practices:
Use useState for simple state.
Use useReducer for structured state transitions.
Use Context for cross-cutting shared values.
Split contexts by responsibility.
Do not turn Context into a poor version of a real state-management architecture.
React’s docs specifically show reducer plus context as a way to scale state management inside React when components need shared state and update functions. (React)
13. Keys and reconciliation: what they are and why they matter
When React renders a list, it needs identity.
function ApplicationList({ applications }) {
return (
<table>
<tbody>
{applications.map(application => (
<ApplicationRow
key={application.id}
application={application}
/>
))}
</tbody>
</table>
);
}
Why do keys exist?
React needs to know which item is the same item between renders. This helps React preserve component state correctly and update efficiently.
Bad:
applications.map((application, index) => (
<ApplicationRow key={index} application={application} />
));
If rows are inserted, deleted or reordered, index keys can cause subtle bugs.
Best practices:
Use stable IDs from data. Avoid index keys for dynamic lists. Use keys to intentionally reset component state when needed. Remember: keys are about React identity, not database identity alone.
Senior explanation:
“Keys help React understand identity during reconciliation. They are not just to remove console warnings.”
14. Routing: what it is and why React does not include everything
React itself is a UI library. It does not force one routing solution.
A SPA still needs URLs:
/loan-products
/loan-products/123/apply
/applications
/dashboard
That is why teams use routing libraries or frameworks.
Example with React Router style code:
function AppRoutes() {
return (
<Routes>
<Route path="/loan-products" element={<LoanProductsPage />} />
<Route path="/loan-products/:id/apply" element={<ApplyPage />} />
<Route path="/applications" element={<ApplicationsPage />} />
</Routes>
);
}
Why routing exists?
Because users need navigable, bookmarkable, shareable screens. The URL is part of application state.
Best practices:
Use route parameters for resource identity. Use query strings for filters, search and pagination. Protect routes for user experience, but enforce security on the backend. Use lazy loading for large route areas. Do not hide important state only in memory if the user should refresh/share/bookmark it.
Example:
/applications?status=Submitted&page=2
This is better than storing the filter only in component state if the filter defines the page view.
15. Server state: what it is and why famous libraries exist
Server state is data owned by the backend but displayed by the frontend.
Examples:
Loan products. Applications. User permissions. Dashboard summaries.
This is different from local UI state.
Local UI state:
Is modal open? Which tab is selected? What did the user type?
Server state has additional problems:
Loading. Errors. Caching. Retries. Invalidation. Stale data. Pagination. Background refetch. Optimistic updates.
You can write server fetching manually with useEffect, as shown earlier. But serious apps often use specialised libraries because server state is a serious concern.
Why does React not include all of this?
Because React focuses on UI rendering and component composition. It intentionally leaves many application architecture choices open. That is why the ecosystem exists.
Best practices:
Use simple custom hooks for simple screens. Use a server-state library when caching, invalidation and retries matter. Do not copy server data into global state unnecessarily. Do not fetch the same data in five components independently. Shape API responses for the screen. Paginate on the server for large datasets.
Senior phrase:
“Server state is not local state. The backend owns it; the frontend temporarily caches and displays it.”
16. Performance and modern React features
React 18 added automatic batching and concurrency-related capabilities such as transitions. Automatic batching means multiple state updates can be grouped into fewer renders in more situations, and transitions let React treat some updates as non-urgent so urgent updates like typing can remain responsive. (React)
Example:
import { useTransition, useState } from "react";
function LoanProductSearch({ products }) {
const [searchText, setSearchText] = useState("");
const [filteredProducts, setFilteredProducts] = useState(products);
const [isPending, startTransition] = useTransition();
function handleSearchChange(event) {
const value = event.target.value;
/*
Urgent update:
Keep the input responsive.
*/
setSearchText(value);
/*
Non-urgent update:
Filtering a large list can be deferred.
*/
startTransition(() => {
const filtered = products.filter(product =>
product.name.toLowerCase().includes(value.toLowerCase())
);
setFilteredProducts(filtered);
});
}
return (
<>
<input value={searchText} onChange={handleSearchChange} />
{isPending && <p>Updating results...</p>}
<LoanProductList products={filteredProducts} />
</>
);
}
What it is:
useTransition lets you mark some updates as less urgent.
Why it was added:
To keep interactions responsive when some UI updates are expensive.
Best practices:
Do not use transitions everywhere. Use them when expensive UI updates compete with urgent user input. Still prefer server-side filtering/pagination for serious data volumes. Measure before optimizing.
Memoization:
const expensiveSummary = useMemo(() => {
return calculateLoanSummary(applications);
}, [applications]);
What it is:
useMemo caches a calculation between renders.
Why it exists:
To avoid repeating expensive calculations when dependencies have not changed.
Best practices:
Do not wrap everything in useMemo.
Use it for genuinely expensive calculations or referential stability problems.
Better data shape often beats memoization.
17. React 19 and the modern direction
React 19 pushed React further into modern async UI and framework-aware patterns. The React 19 release notes discuss Actions for async data mutations, form-related improvements, use, ref changes and server-oriented capabilities. (React)
A simplified idea of an action-style form:
function UpdateApplicantNameForm({ updateName }) {
async function submitAction(formData) {
/*
In modern React/framework patterns, form actions can represent
async mutations more directly than manually wiring every loading state.
*/
await updateName(formData.get("name"));
}
return (
<form action={submitAction}>
<input name="name" />
<button type="submit">Update</button>
</form>
);
}
What it is:
A modern React direction for async form mutations and UI state around actions.
Why it was added:
Because mutations are a common pain point: submit, wait, show pending, handle errors, update UI. React is making these flows more first-class, especially in framework-supported environments.
Best practices:
Do not force React 19 features into older architecture blindly. Understand whether your framework supports the pattern. For normal SPAs, classic controlled forms and server-state libraries may still be appropriate. Keep backend validation and authorization strong.
React 19.2’s additions such as and useEffectEvent show React continuing to improve how apps manage hidden/visible UI work, effect logic and performance analysis. (React)
18. A professional React app structure
For a business system like Loan Management Supermarket, I would structure React by feature.
src/
app/
App.jsx
routes.jsx
providers.jsx
features/
loan-products/
api/
loanProductsApi.js
hooks/
useLoanProducts.js
components/
LoanProductCard.jsx
LoanProductFilters.jsx
LoanProductList.jsx
pages/
LoanProductsPage.jsx
loan-applications/
api/
loanApplicationsApi.js
hooks/
useLoanApplications.js
components/
LoanApplicationForm.jsx
ApplicationStatusBadge.jsx
pages/
ApplyForLoanPage.jsx
ApplicationsPage.jsx
shared/
components/
Button.jsx
LoadingState.jsx
ErrorState.jsx
EmptyState.jsx
api/
httpClient.js
hooks/
useDebouncedValue.js
utils/
formatCurrency.js
Why?
Because business systems grow by business capability, not by file type.
Best practices:
Pages coordinate use cases.
Components render focused UI.
Hooks encapsulate behaviour.
API modules call the backend.
Shared components should be genuinely shared.
Avoid dumping everything into components/.
A page should read like this:
function LoanProductsPage() {
const [filters, setFilters] = useState(defaultFilters);
const {
data: products,
loading,
error
} = useLoanProducts(filters);
if (loading) return <LoadingState message="Loading products..." />;
if (error) return <ErrorState message={error.message} />;
return (
<Page title="Loan Products">
<LoanProductFilters value={filters} onChange={setFilters} />
<LoanProductList products={products} />
</Page>
);
}
That is clean.
19. Production mentoring case: review a loan application safely
The first eighteen sections explain React's mechanics. Let us now build one workflow that forces those mechanics to meet backend reality.
An authenticated applicant opens /applications/:applicationId/review. They review personal details and requested amount, edit an optional note, accept declarations and submit. A staff user can later inspect the application and record a decision. We will focus on the applicant review page, but every boundary must anticipate privacy, retries, concurrency and accessibility.
Junior: Should I begin with>useStatefor every field and auseEffectthat fetches the application?
Senior: Begin with the state categories and contract. Hooks are implementation tools. If we mix navigation, server data, form drafts and submission state, no hook combination will make ownership clear.Classify state:
| State | Owner | Examples |
|---|---|---|
| Navigation | URL/router | application ID, return location, active review step |
| Server state | query/cache layer | application snapshot, version, permissions |
| Draft form | form/feature component | note, declarations, edited contact details |
| Transient UI | local component | disclosure open, focused help, dialog state |
| Durable business state | server/database | submitted application and audit trail |
Design the response for the page
export interface ApplicationReviewModel {
applicationId: string;
version: string;
applicant: {
displayName: string;
email: string;
};
product: {
name: string;
requestedAmount: string;
currency: string;
};
declarations: readonly DeclarationModel[];
applicantNote: string;
status: 'draft' | 'ready' | 'submitted';
permittedActions: readonly ('edit' | 'submit')[];
}
export interface DeclarationModel {
id: string;
version: string;
text: string;
required: boolean;
}
Money is represented as a contractually defined decimal string plus currency rather than a JavaScript number assumed to preserve arbitrary decimal precision. The declaration ID/version records exactly what the applicant accepted. version supports optimistic concurrency. permittedActions assists presentation; the server re-authorises commands.
The route loader or server-state library owns fetching and cancellation. The page composes:
export function ReviewApplicationPage() {
const { applicationId } = useParams();
const query = useApplicationReview(applicationId!);
if (query.isPending) return <ReviewSkeleton />;
if (query.isError) return <ReviewLoadError error={query.error} />;
if (!query.data) return <ApplicationNotFound />;
return <ApplicationReview model={query.data} />;
}
In production, validate the route parameter rather than using !. Not-found, forbidden and transient failure may intentionally render different routes/messages while avoiding sensitive existence disclosure.
20. Rendering must remain pure
A component is called to calculate UI for current props and state. It can be called more often than you expect, abandoned before commit, or invoked during server rendering. Do not perform business side effects during render.
// Wrong: submission starts while React is calculating UI.
function SubmitStatus({ shouldSubmit }: { shouldSubmit: boolean }) {
if (shouldSubmit) api.submitApplication();
return <p>Ready</p>;
}
Submission belongs in an event/action. Derived values belong in render:
const requiredAccepted = model.declarations
.filter(x => x.required)
.every(x => acceptedDeclarationIds.has(x.id));
const canSubmit =
model.permittedActions.includes('submit') &&
requiredAccepted &&
!submission.isPending;
Do not put canSubmit in state and update it with an Effect. It derives from current inputs; duplicate state can lag and produces an unnecessary extra render.
Strict Mode's development setup/cleanup cycle exposes Effects that are not symmetrical. It does not mean production “renders twice” in one simple universal sense, and disabling it is not a fix. Make subscriptions and external resources safe to set up, clean up and set up again.
Junior: If React can call a component again, can I increment an analytics counter in the component body?>
Senior: No. Rendering is calculation, not proof the user saw the page. Instrument a committed navigation through the routing/analytics integration with deduplication appropriate to that system.
21. Effects are synchronization boundaries
React's official definition is useful: useEffect synchronizes a component with an external system. Examples include a browser event listener, timer, network subscription or third-party widget.
For a before-unload warning while the form is dirty:
useEffect(() => {
if (!isDirty) return;
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault();
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [isDirty]);
This is genuine synchronization with the browser. It has matching cleanup. A router-level navigation blocker needs a separate accessible confirmation experience and careful browser support review; do not rely solely on beforeunload.
Avoid fetching ordinary route data in ad hoc Effects when the router/framework or server-state layer can coordinate loading, caching, cancellation and errors. Effect fetching can work, but you must prevent stale responses:
useEffect(() => {
const controller = new AbortController();
void api.getApplication(applicationId, controller.signal)
.then(data => setModel(data))
.catch(error => {
if (!controller.signal.aborted) setError(error);
});
return () => controller.abort();
}, [applicationId]);
Aborting reduces wasted work and prevents obsolete completion from updating this component. A data library/router provides broader behaviours but still relies on correct query identities and backend contracts.
useEffectEvent is not a dependency escape hatch
React 19.2 documents useEffectEvent for non-reactive logic called from Effects. It can read the latest committed values without reconnecting the external system:
const onAutosaveResult = useEffectEvent((result: SaveResult) => {
if (notificationsEnabled) {
showDraftSaved(result.savedAt);
}
});
useEffect(() => {
return draftChannel.subscribe(applicationId, onAutosaveResult);
}, [applicationId]);
Use the exact API according to the installed React/linter version. Do not pass Effect Events to child components, call them during render or use them to conceal a value that should cause resynchronization.
22. Model form state as a workflow
The form has initial data, edits, validation, save/submit and conflict. A reducer makes transitions visible:
type ReviewFormState =
| { kind: 'editing'; draft: ReviewDraft; dirty: boolean }
| { kind: 'submitting'; draft: ReviewDraft; requestId: string }
| { kind: 'conflict'; draft: ReviewDraft; latest: ApplicationReviewModel }
| { kind: 'submitted'; receipt: SubmissionReceipt };
type ReviewAction =
| { type: 'noteChanged'; note: string }
| { type: 'declarationToggled'; declarationId: string }
| { type: 'submissionStarted'; requestId: string }
| { type: 'submissionConflicted'; latest: ApplicationReviewModel }
| { type: 'submissionSucceeded'; receipt: SubmissionReceipt }
| { type: 'submissionFailed' };
Keep reducer logic pure. It should not call APIs or write storage. The event handler invokes the mutation and dispatches the result.
Controlled inputs are useful for validation and composed form state:
<label htmlFor="applicant-note">Additional information</label>
<textarea
id="applicant-note"
name="applicantNote"
value={state.draft.applicantNote}
onChange={event => dispatch({
type: 'noteChanged',
note: event.currentTarget.value
})}
aria-describedby="note-help note-error"
/>
Do not assume every keypress requires top-level state. Keep fields in the smallest coherent owner or use a form library/native actions whose model fits the feature. Validate client-side for quick feedback and repeat all authoritative validation server-side.
Preserve the user's input after server validation or conflict. If a declaration version changed, show the new text and require conscious reacceptance rather than keeping a checkbox against different wording.
23. Submission is a distributed operation
The click handler starts a command:
export interface SubmitApplicationCommand {
applicationId: string;
expectedVersion: string;
requestId: string;
applicantNote: string;
acceptedDeclarations: readonly {
id: string;
version: string;
}[];
}
requestId makes the logical submission identifiable. The server stores it atomically with the outcome. If the response is lost and the client retries, it can return the original receipt rather than creating another submission or audit record.
Disable the button while the local mutation is pending to improve usability, but do not confuse that with idempotency. Double clicks, tabs, proxies and retry logic exist outside one component.
async function handleSubmit(event: FormEvent) {
event.preventDefault();
const requestId = crypto.randomUUID();
dispatch({ type: 'submissionStarted', requestId });
try {
const receipt = await api.submit(toCommand(state, requestId));
dispatch({ type: 'submissionSucceeded', receipt });
} catch (error) {
if (isVersionConflict(error)) {
const latest = await api.getApplication(model.applicationId);
dispatch({ type: 'submissionConflicted', latest });
return;
}
dispatch({ type: 'submissionFailed' });
}
}
This sketch needs cancellation/lifecycle handling supplied by the chosen mutation layer. More importantly, a network failure is an uncertain outcome: the server may have committed. Reconcile by request ID or refetch authoritative status before offering another submit.
Junior: Can I optimistically show “Submitted” to make it feel fast?>
Senior: Not for an irreversible, regulated command unless the product explicitly accepts temporary false confirmation. Show “Submitting” and confirm only from an authoritative receipt.
24. Server state is not global client state
Server-state tools help cache, deduplicate, refetch, cancel and invalidate asynchronous data. They do not replace domain modelling.
Use stable keys containing every dimension that affects the response:
const applicationReviewKey = (
tenantId: string,
applicationId: string
) => ['application-review', tenantId, applicationId] as const;
Omitting tenant/permission scope can show cached data to the wrong context. A trusted server should derive tenant from identity rather than the client key, but the client cache must still separate visible contexts.
After submission, set the returned authoritative review model/receipt or invalidate the precise query. Avoid invalidating the entire application cache after every action; broad invalidation creates load and flicker. Conversely, manually modifying ten cached projections is error-prone. Choose a small invalidation boundary.
Stale time is a product consistency choice. A product catalogue may tolerate minutes. An application status after submission may need immediate reconciliation. “Cache for five minutes” is not universal performance advice.
Do not place every server response in Redux/Context and then add another query cache. Duplicate authorities drift. Use a client store for cross-cutting client workflows when appropriate and a server-state layer for remote resources.
25. Routing owns navigation state
The application ID and meaningful review step belong in the route. Filters/search belong in query parameters when links should be shareable. A modal that represents a navigable detail may deserve a route; a transient confirmation usually does not.
Route transitions must handle unsaved work, permissions, focus and scroll. On a successful navigation, move focus to the new page heading or use the framework's accessible navigation integration. Screen readers do not automatically understand that a SPA replaced its main content.
Validate redirect/return URLs. Never navigate to an arbitrary untrusted URL from a query parameter. Map internal route identifiers or enforce same-origin approved paths.
Lazy-load route groups to reduce initial JavaScript, but design loading and failure boundaries. A chunk can fail after deployment if the HTML/runtime references an old asset. Use content-hashed assets, sensible cache headers and a recovery page that can reload safely without discarding a recoverable draft.
Frameworks and routers evolve independently from React. Pin versions, follow their official migration guidance and keep route contracts in feature tests.
26. Error boundaries and expected errors
Error boundaries catch rendering errors in their child tree according to React's boundary semantics. They do not automatically catch every event-handler or asynchronous error, and they are not a substitute for domain error states.
Expected outcomes—validation, forbidden, conflict, not found and dependency unavailable—should flow through query/mutation state and render specific recovery. An unexpected component defect belongs in an error boundary with safe telemetry and a route/feature recovery action.
Place boundaries where recovery is meaningful. A document preview failure should not necessarily replace the whole application review. The page route needs a boundary for catastrophic failure. Avoid one tiny boundary around every span.
Do not display error.message directly if it came from a server/internal exception. Map known codes to user-safe content, preserve correlation ID for support, and send diagnostic detail only to protected telemetry.
Test the fallback and reset behaviour. If retry renders the same broken input, the boundary will fail again. A navigation or query refresh may be the correct reset key.
27. Accessibility is part of component API design
Use native controls and semantic regions. The review page needs one main heading, labelled sections, a real form, field labels, linked errors and a submit button.
Declarations should be individual checkboxes with their complete labels. If text is long, ensure the control/label association remains clear and the click target is useful. A checkbox must not be preselected.
On failed validation:
- render an error summary with links to fields;
- focus the summary or first invalid field according to the design;
- associate inline messages with inputs;
- preserve values;
- avoid announcing the same error repeatedly.
Custom components should require accessible inputs:
interface ConfirmationDialogProps {
title: string;
description: ReactNode;
confirmLabel: string;
cancelLabel: string;
returnFocusRef: RefObject<HTMLElement | null>;
onConfirm(): void;
onCancel(): void;
}
The implementation must also trap/manage focus, close predictably and restore focus. Prefer a proven accessible primitive if the team cannot maintain dialog behaviour.
28. Security: React renders trust boundaries, it does not enforce them
React escapes text values by default. dangerouslySetInnerHTML bypasses that protection. Do not render applicant notes or CMS content as HTML without an approved sanitisation policy.
Authorisation belongs on the server for reads and commands. A conditional button is helpful presentation only. Derive resource scope from authenticated identity, validate expected version and protect every referenced ID against cross-tenant access.
Cookie-authenticated mutations require an antiforgery strategy appropriate to the backend. Bearer tokens introduce storage, audience, expiry and XSS considerations. Prefer established authentication libraries and server-managed sessions/BFF patterns where suitable rather than inventing token handling.
Do not store sensitive application data or long-lived tokens in browser storage casually. A recoverable draft needs a threat/retention decision: perhaps an encrypted server draft keyed to the user is safer than local storage. Clear drafts after confirmed submission and on identity changes.
Content Security Policy, dependency review, subresource/resource controls and secure headers add layers. React does not remove browser security fundamentals.
Junior: TypeScript says the response matches ApplicationReviewModel. Is runtime validation unnecessary?>
Senior: TypeScript types disappear at runtime. Validate untrusted responses where contract mismatch creates meaningful risk, and always validate client requests on the server.
29. Performance starts with user journeys
Define budgets for initial route JavaScript, time to readable review, time to interactive form and submit-to-confirmation latency. Measure representative mobile/desktop devices and networks.
Use the React Profiler and browser performance tools to find actual expensive renders, long tasks and layout work. A component rendering frequently is not automatically a problem if it is cheap.
Before adding memoisation:
- keep state near its consumers;
- avoid Effects that mirror state;
- avoid rendering huge hidden subtrees;
- use stable item keys;
- split expensive independent routes/features;
- reduce data and work at the backend.
Manual useMemo, useCallback and memo remain tools, not correctness requirements. Their dependencies must be accurate, and maintaining them has cost. Do not memoize a trivial expression without evidence.
Lists need stable domain keys. Index keys can attach draft/focus state to the wrong row after reorder. Virtualisation is useful for genuinely large lists but complicates accessibility, focus and variable height. Paginate or narrow results first.
30. Server rendering, hydration and framework choices
React itself is a UI library. A production framework may provide routing, server rendering, data loading, streaming, mutations and asset handling.
Server rendering can improve initial HTML and discovery, but components run in server and client contexts. Browser APIs are unavailable during server rendering. Effects run only on the client. The initial client render must match server output sufficiently for hydration.
Avoid rendering Date.now(), random IDs or browser-only conditions directly into server/client output:
// Can differ between server and client.
return <p>Rendered at {Date.now()}</p>;
Use stable server-provided data, React's ID facilities for component IDs, or update client-only information after hydration when appropriate. Do not suppress hydration warnings as a general fix.
Server Components and server actions are framework/integration concerns with boundaries that vary by toolchain. Data passed to client components must be serialisable, and server-only secrets/code must remain server-only. Treat any callable server action as an endpoint requiring authentication, authorisation, validation and idempotency.
Choose a framework from requirements and team capability, not because “React needs Next.js.” A client-rendered authenticated dashboard, an SEO content site and a mixed commerce application have different needs.
31. Testing the workflow in layers
Pure tests
Test validation functions, reducer transitions, declaration-version comparison and command construction without rendering. These are fast and explain the workflow.
Component tests
Render through public behaviour and accessible queries:
it('prevents submit until required declarations are accepted', async () => {
render(<ApplicationReview model={reviewModel} />);
const submit = screen.getByRole('button', { name: /submit application/i });
expect(submit).toBeDisabled();
await user.click(screen.getByRole('checkbox', {
name: /information is accurate/i
}));
expect(submit).toBeEnabled();
});
This proves presentation, not server authorisation. API/integration tests call commands with forbidden identities, stale versions and duplicate request IDs.
Contract tests
Validate request/response schemas and error codes. Generate types from a trusted schema where it reduces drift, but review semantic changes. A response can be structurally valid and semantically incompatible.
Browser tests
Cover deep-link load, validation/focus, successful submission, uncertain response recovery, concurrency conflict, Back/refresh with dirty draft, keyboard operation and route chunk failure recovery. Keep the suite selective.
Avoid snapshots of the whole page and tests that assert hook implementation. Test what the user and contracts observe.
32. Observability without leaking applications
Give a submission a request/correlation ID. Trace the browser request through gateway, command handler, database and asynchronous publication. Show a safe support reference for unexpected outcomes.
Measure route loading, interaction responsiveness, query/mutation latency, error categories, conflict rate and uncertain-outcome reconciliation. Use low-cardinality metric labels. Application IDs and user IDs belong only in access-controlled diagnostic contexts, not metric dimensions.
Do not log form bodies, applicant notes, tokens or declaration text. Client error tools often capture URL, DOM or breadcrumbs; configure scrubbing and sampling deliberately.
Release telemetry should include application version/build so regressions correlate with deployment. Source maps improve diagnostics but require a secure publication/access strategy; do not accidentally expose source maps contrary to policy.
Product analytics must have purpose, minimisation and consent/legal review. “User clicked Submit” may be useful; full entered values are not.
33. Diagnose four React incidents
Incident one: requests loop continuously
An Effect depends on an object created every render and sets state after fetch. The dependency changes, so the Effect reruns.
Move pure derivation out of the Effect, construct stable query primitives, or let the router/server-state layer own fetching. Do not silence the linter or remove dependencies blindly; that creates stale closures.
Incident two: the wrong row keeps the note editor
A list uses array indices as keys. Sorting changes positions, and React preserves component state by key/position. Use stable application IDs. Decide deliberately whether state should reset when identity changes.
Incident three: submission happened twice
The team blames Strict Mode. The actual command is launched from an Effect when shouldSubmit becomes true, and retry repeats it. Move it to an explicit event/action, add server idempotency and reconcile uncertain outcomes. Strict Mode exposed an unsafe side effect; it did not create the business rule defect.
Incident four: old applicant data flashes after sign-out
The query cache key omitted identity/tenant and persisted across session change. Clear or partition sensitive caches on authentication transitions, include context in keys and enforce server access. Treat any cross-user display as a security incident.
For each incident, capture reproduction, component/state timeline, network/trace evidence and corrective test. Avoid “add useCallback” as a universal repair.
34. Delivery sequence for a real team
- Build the route and read-only server-authorised model.
- Design loading, forbidden, not-found, empty and failure states.
- Add the draft reducer and accessible form without submission.
- Add server validation and one idempotent versioned command.
- Handle conflict and uncertain response before optimistic polish.
- Add draft recovery only after privacy/retention design.
- Test keyboard, zoom and assistive-technology-informed flow.
- Measure route bundle, render and API journey.
- Add telemetry, support runbook and progressive rollout.
- Rehearse a deployment during an open draft and a lost response.
35. Design component APIs around valid combinations
A component API is a small language. Boolean props can accidentally permit contradictory sentences:
<Banner success error dismissible={false} />
Prefer a discriminated union:
type BannerProps =
| {
kind: 'success';
title: string;
receiptId: string;
onDismiss?(): void;
}
| {
kind: 'error';
title: string;
supportReference?: string;
onRetry?(): void;
}
| {
kind: 'info';
title: string;
children: ReactNode;
};
Now TypeScript narrows the permitted fields. Runtime inputs still require validation before becoming props; TypeScript does not inspect network JSON.
Use composition when the parent owns content structure:
<ReviewSection title="Declarations">
<DeclarationList
declarations={model.declarations}
acceptedIds={state.draft.acceptedDeclarationIds}
onToggle={handleDeclarationToggle}
/>
</ReviewSection>
Do not turn every layout slot into a callback renderer with an elaborate parameter object. children and focused slots are easier when the content is already React UI. Render props are useful when the reusable component owns behaviour/state and consumers own rendering.
Callbacks should describe intent: onDeclarationToggled(id) is clearer than passing setState. The child cannot arbitrarily mutate the parent's whole model. Avoid generic onChange(value: unknown) for business components when the event has richer meaning.
Controlled and uncontrolled design
A reusable disclosure can be uncontrolled with defaultOpen or controlled with open/onOpenChange. Document which source is authoritative. Do not switch between controlled and uncontrolled after mount.
type DisclosureProps =
| { open: boolean; onOpenChange(open: boolean): void; defaultOpen?: never }
| { defaultOpen?: boolean; open?: never; onOpenChange?: never };
For business forms, controlled does not require one giant parent object. A form library may register inputs while exposing controlled business outcomes. Choose from validation, dynamic dependencies, performance and test needs.
Ref as an escape hatch
Use refs for DOM focus/measurement or mutable values not used by rendering. If changing a value should update the screen, it belongs in state. Expose imperative handles sparingly—for example focusFirstError() on a complex field group—and prefer declarative props.
36. State preservation and reset are identity decisions
React preserves state for a component at the same position and identity in the tree. A different key tells React it is a different instance.
When navigating from application A to B, a local note draft must not leak:
return (
<ApplicationReview
key={model.applicationId}
model={model}
/>
);
This resets the entire subtree on application identity change. Use it only when that reset matches product intent. A key based on a value that changes every render destroys focus and state continuously.
Sometimes preserve drafts per application instead. Then state moves to a route-scoped draft store keyed by application ID with retention/security rules. This is a product and privacy decision, not a React trick.
Conditional rendering also affects preservation. These two branches may occupy the same or different structural position depending on their tree:
return mode === 'edit'
? <ReviewForm model={model} />
: <ReviewSummary model={model} />;
If switching mode should preserve form edits, keep the form mounted or lift the draft. If it should discard them, reset explicitly and confirm when dirty. Do not rely on accidental tree position.
Junior: Can I copy new props into state whenever the model changes?>
Senior: That can overwrite an active draft. Define a transition: pristine drafts may refresh automatically; dirty drafts need conflict/reload choices; a new application identity may reset.Keys are not only list-warning suppressors. They are identity declarations that control reconciliation and state lifetime. Review them as part of correctness.
37. External stores and tearing-safe subscriptions
Some state lives outside React: browser connectivity, a legacy event emitter, a shared observable store or a third-party editor. React provides useSyncExternalStore for subscribing consistently:
function useOnlineStatus(): boolean {
return useSyncExternalStore(
subscribeToOnlineStatus,
getOnlineSnapshot,
getServerOnlineSnapshot
);
}
The snapshot must be cached/stable until the underlying store changes; returning a new object every call can cause loops. The subscription must clean up. Server rendering needs a compatible server snapshot when used.
Do not implement a global store with useEffect plus setState in every component if a concurrency-safe subscription mechanism/library exists. Conversely, do not move ordinary local form state to an external store because several nested fields need it.
Context distributes a value; it is not automatically an efficient state store. Every consumer of a changed context value may render. Split contexts by responsibility, keep provider values stable where meaningful and measure before adding selector libraries.
Global state needs lifecycle rules: reset on sign-out/tenant switch, persistence schema/version, sensitive-data policy, cross-tab behaviour and SSR isolation. A module-level singleton store on a server can accidentally share data between requests if the framework does not create request-scoped instances.
38. Deploy client and API versions safely
A browser tab can run yesterday's JavaScript against today's API. A rolling deployment can serve mixed server versions. Compatibility is not optional.
Use additive contract changes first. New response fields should be optional to old clients. Do not rename an enum value or change meaning silently. Make clients handle unknown safe values and fail closed for security-critical states.
Content-hashed assets allow long immutable caching. The HTML/document and runtime manifest need shorter/revalidation-aware caching so they reference existing assets. Keep old chunks available long enough for active tabs or provide a controlled reload path.
A dynamic-import failure often occurs when a deployment removed the chunk an open tab requests. Catch it at the route boundary, preserve any approved draft and offer reload. Infinite automatic reload loops make an outage worse.
Service workers add another version/cache layer. Design activation, cache invalidation and offline behaviour deliberately. Never serve a sensitive authenticated page from a shared stale cache without the right security model.
Feature flags and schema compatibility
A flag may expose UI before every API instance supports it. Coordinate capability discovery or deploy backend-compatible support first, then enable the client. Remove expired flags and tests.
Database migrations should support old and new APIs during rollout. A React feature cannot compensate for a breaking server schema deployment.
Frontend release evidence
Before increasing traffic:
- load a deep link on a fresh and cached browser;
- keep an old tab open across deployment and submit safely;
- force a route-chunk failure and recover;
- test the API with old/new client contract fixtures;
- verify source-map and telemetry release association;
- check CSP/assets/CDN headers;
- monitor JavaScript errors, journey completion and server command outcomes;
- rehearse rollback without losing idempotency or drafts.
39. Senior pull-request checklist
- Is rendering pure and are derived values derived?
- Does every Effect synchronize with an identifiable external system?
- Are Effect dependencies truthful and cleanup symmetrical?
- Is state owned at the smallest coherent boundary?
- Are navigation, server, draft and transient states separated?
- Are route and query keys complete for tenant/identity context?
- Are commands server-authorised, versioned and idempotent?
- Is uncertain delivery reconciled before retry?
- Are loading, failure, empty, conflict and success explicit?
- Are form errors labelled, linked and focused accessibly?
- Are stable keys preserving the intended identity?
- Is sensitive data excluded from browser storage and telemetry?
- Is memoisation supported by profiling evidence?
- Do server/client renders avoid hydration instability?
- Do tests prove behaviour below and above React?
- Can support trace a safe request reference to its outcome?
40. A worked React code review
Consider this plausible component:
function Review({ applicationId }: { applicationId: string }) {
const [application, setApplication] = useState<any>();
const [canSubmit, setCanSubmit] = useState(false);
const [submit, setSubmit] = useState(false);
useEffect(() => {
fetch(`/api/applications/${applicationId}`)
.then(response => response.json())
.then(setApplication);
}, []);
useEffect(() => {
setCanSubmit(application?.declarations.every((x: any) => x.accepted));
}, [application]);
useEffect(() => {
if (submit) {
fetch(`/api/applications/${applicationId}/submit`, { method: 'POST' })
.then(() => alert('Submitted'));
}
}, [submit]);
return (
<button disabled={!canSubmit} onClick={() => setSubmit(true)}>
Submit
</button>
);
}
It may pass a happy-path demonstration. Review it in layers.
Contract and type review
any discards the compiler's help. The fetch path assumes every response is successful JSON and structurally valid. It distinguishes neither forbidden, not found nor transient failure. Define a DTO/schema, validate meaningful runtime boundaries and map expected API outcomes.
Fetch lifecycle review
The empty dependency array ignores applicationId changes. A navigation can display the previous application. The request is not cancelled, and no loading/error state exists. Use the route's loader or server-state hook with a key containing identity; if using an Effect, include dependencies, abort and ignore obsolete completion.
Derived-state review
canSubmit is derivable from the current model and other form conditions. Storing it creates an extra render and a state that can drift. Calculate it during render. Moreover, accepted appears to mutate server response objects; form draft state should be explicit and declaration versions retained.
Command review
Setting submit is an indirect event-to-Effect path. It remains true, so later dependency or remount behaviour is difficult. The command lacks body, expected version, declaration evidence, authentication considerations and idempotency key. It treats any resolved response—including an HTTP error—as success because fetch does not reject merely for non-success status.
Call a typed mutation from the submit handler/action. Disable locally while pending, but make the server idempotent. Handle validation, conflict, forbidden, uncertain delivery and success separately. Replace alert with an accessible, persistent receipt/focus transition.
Security and privacy review
The URL ID must not define authority. The server derives tenant/user context and authorises the resource. The command needs the application's current version and exact declarations. Error/telemetry handling must not capture application payloads. Cookie-authenticated POSTs need the application's antiforgery design.
Accessibility review
The component renders no loading explanation, application content, declaration controls or error summary. The button's disabled state alone cannot explain what remains. Each required declaration needs a labelled checkbox and validation relationship. Submission progress and result need status/focus design.
A better shape
The corrected page should read as orchestration:
function ReviewApplicationPage() {
const params = useValidatedApplicationParams();
const review = useApplicationReview(params.applicationId);
return (
<ReviewRouteBoundary query={review}>
{model => (
<ApplicationReview
key={model.applicationId}
initialModel={model}
onSubmit={command => submitApplication(command)}
/>
)}
</ReviewRouteBoundary>
);
}
The helper names represent contracts, not mandatory libraries. Review their implementations: the boundary maps explicit states, the hook has a complete key/cancellation policy, and submission reconciles uncertain outcomes.
The important lesson is not “never use useEffect.” It is that three Effects were hiding three different responsibilities: route data loading, pure derivation and a user command. Giving each responsibility the correct owner makes dependencies, errors, security and tests visible.
41. Mentoring exercises
Exercise one: remove unnecessary Effects
Take a component that uses Effects to calculate fullName, filtered items and submit readiness. Move those calculations into render or event handling. Explain which Effect remains and which external system it synchronizes.
Exercise two: reproduce stale data
Create two controllable API promises. Navigate from application A to B and resolve A last. Demonstrate the defect, then fix it using the route/query layer or abort plus identity checking.
Exercise three: uncertain submission
Make the fake server commit but drop the response. Reload by request ID and show the receipt without a second business effect. Prove the server, not a disabled button, supplies idempotency.
Exercise four: declaration conflict
Change a required declaration version while the applicant edits. Preserve their note, invalidate only the changed acceptance and focus an explanation. Test reducer and browser behaviour.
Exercise five: profile before memoising
Record a slow interaction with the React Profiler and browser tooling. Identify whether cost is render, JavaScript task, layout, payload or API. Apply one targeted change and record before/after evidence.
Exercise six: teach it across the stack
Trace Submit from React event to HTTP contract, authentication, authorisation, validation, concurrency, database transaction, audit/outbox, response, query cache and accessible confirmation. Identify the owner/test for every guarantee.
42. Version-aware React references
As reviewed in July 2026, React's versions page lists React 19.2 as the latest documented version. Verify again before adopting a minor-version API or compiler configuration.
- React versions
useEffectreferenceuseEffectEventreference- React Compiler introduction
- React built-in hooks
43. Continue the learning path
Use React and TypeScript Production Mastery for deeper typing and implementation patterns, Full-Stack React, TypeScript and Node for the backend integration, JavaScript Core Mechanisms for closures/event loop, and Frontend Architecture Patterns for multi-feature scale. The HTTP, Web Security, Testing JavaScript/TypeScript and CSS/Accessible UI guides deepen the boundaries exercised here.
Definition of done for the review workflow
The feature is complete when an authorised applicant can open a direct link, understand every reviewed fact, edit and validate the draft, accept the exact current declarations, submit once and receive an authoritative receipt. Refresh, route changes, slow responses, an expired permission, a stale version and a lost submit response must produce documented, recoverable behaviour. A keyboard and assistive-technology-informed journey must reach the same outcome with deliberate focus and announcements.
The server must independently prove resource authorisation, validation, concurrency and idempotency. React tests prove UI transitions; they do not substitute for command integration tests. Browser tests cover route loading, validation, conflict, uncertain delivery and deployment recovery. Performance evidence covers route payload, rendering and submit latency on representative devices. Telemetry correlates the outcome without retaining applicant form content.
Finally, deploy a mixed-version rehearsal: keep an old tab with a dirty draft, roll out compatible API and assets, then submit and lazy-load another route. If the user loses work, receives false confirmation or cannot recover a missing chunk, the feature is not production-ready. React correctness includes the world around the component—the browser, cache, network, API, database, deployment and person trying to finish an important task reliably and confidently.
44. Final senior React mental model
After this session, you should not say:
“I know React because I know useState and useEffect.”
You should say:
“I understand React as a declarative UI library where components describe the UI for a given state. JSX creates React element descriptions, not DOM directly. React renders by calling components, comparing UI descriptions and committing necessary DOM updates. Props pass data down, callbacks send events up, and state changes trigger rendering. Hooks let function components use React features such as state, effects, refs, reducers and context. Effects are for synchronising with external systems, not for ordinary calculations. State should be treated as immutable and as a snapshot per render. Keys preserve identity during reconciliation. React does not solve every application problem by itself, so routing, server-state management, complex forms and framework-level rendering may use libraries or frameworks. A professional React app separates pages, components, hooks and API modules, keeps state ownership clear, handles loading/error/empty states, respects accessibility and relies on backend authorization for real security.”
That is the level.
React mastery is not memorising every hook.
It is understanding what each feature is, why it exists, how it behaves mechanically, when to use it, and when not to.
A hobby React developer builds screens.
A serious React developer designs UI behaviour.
A senior full-stack developer connects React behaviour to backend contracts, API design, security, performance, user experience and maintainable architecture.
That is the shift.
