Why React Exists
Imperative DOM mutation scaling introduces untenable state-synchronization complexities. React resolves this architectural bottleneck by implementing a declarative, unidirectional data flow, effectively decoupling the state machine from the underlying imperative rendering heuristics of the browser engine.
React flips the model. Instead of writing step-by-step instructions to change the DOM ("find this, update that"), you describe what the UI should look like for a given state, and React figures out the minimal DOM changes for you. You worry about data; React worries about the DOM.
Imperative vs declarativeVanilla JS is imperative: "do this, then this." React is declarative: "here's what it should look like." The declarative style is why large apps stay maintainable.
React also introduces two ideas that changed front-end forever: breaking the UI into reusable components, and keeping a fast in-memory copy of the page (the Virtual DOM).
The Virtual DOM & Reconciliation
Updating the real DOM is historically slow and expensive. When your data changes, React creates a new Virtual DOM (a lightweight JavaScript representation of what the DOM should look like) and compares it with the previous Virtual DOM. This comparison process is called Reconciliation. React then calculates the absolute minimum number of changes needed and updates the real DOM in one swift motion (called Diffing). This is why React apps feel so fast without manual optimization.
Under the Hood: React is a UI equationReact's fundamental paradigm is UI = f(state). The UI is a pure projection of the application's state at any given moment. This mathematical determinism means you never manually mutate the DOM; you simply mutate state, and the engine guarantees the UI aligns.
Components & JSX
The fundamental unit of React architecture is the Component—a pure functional abstraction that encapsulates discrete UI logic. Components leverage JSX syntax, an XML-like ECMAScript extension, to topologically define the Virtual DOM tree prior to reconciliation.
function App() {
return (
<div>
<Navbar />
<Feed />
</div>
);
}That HTML-looking syntax inside JavaScript is JSX. It's not really HTML — it's JavaScript in disguise that React turns into real elements. A component's name is Capitalised, and you use it like a custom tag: <Navbar />.
Embedding JavaScript in JSX
JSX allows you to write dynamic UI by embedding plain JavaScript expressions inside curly braces {}. You can read variables, call functions, or do math right inside your markup.
const user = "Ali";
const avatar = "https://example.com/ali.png";
return (
<div className="card">
<img src={avatar} alt={user} />
<h1>Hello, {user.toUpperCase()}</h1>
</div>
);The Single Parent Rule & FragmentsA component must return one single top-level element. If you need to return multiple side-by-side elements without wrapping them in an extra <div>, use a React Fragment: <> ... </>.
Composing an app from components
- Everything lives inside one top-level component:
<App />. - It renders a
<Navbar />component — a self-contained piece you could reuse on any page. - …and a
<Feed />component below it. Nest components inside components to build any interface.
Think in componentsLook at any website and mentally draw boxes: header, search bar, each card, each button. Every box is a component. That decomposition is React thinking.
Under the Hood: JSX is just React.createElementBrowsers cannot read JSX. Babel compiles <div id="a">Hi</div> into React.createElement('div', {id: 'a'}, 'Hi'). This is why components must return a single parent node: a JavaScript function cannot return multiple distinct objects without wrapping them in an array or a Fragment.
Props: Passing Data In
Component interoperability demands strict, immutable data parameterization. Props establish a rigid, unidirectional data contract from parent to child, ensuring that downstream rendering logic remains deterministic and free from hidden side-effects.
function Card({ name }) {
return <div>Hello, {name}</div>;
}
// reuse it with different props:
<Card name="Sara" />
<Card name="Omar" />One component, many props
- Pass
name="Sara"and theCardrenders "Hello, Sara". - The same component with
name="Omar"renders a different card. Zero duplicate code. - Feed it a list of users and you get a card for each — this is how real feeds and galleries are built.
The children Prop
Sometimes you don't know what content a component will hold beforehand (like a Modal or a Card wrapper). You can pass content between the opening and closing tags of a component, and access it using the special children prop.
function CardWrapper({ children }) {
return <div className="card-shadow">{children}</div>;
}
// Usage:
<CardWrapper>
<h1>Title inside wrapper</h1>
</CardWrapper>Props flow one way — and are read-onlyData flows down from parent to child. A component must never modify its own props. To change something over time, you need state.
Under the Hood: Props are strictly immutableAttempting to reassign a prop (e.g. props.name = 'new') violates React's unidirectional data flow. If a child needs to mutate data owned by a parent, the parent must pass down a callback function as a prop, allowing the child to request the parent to mutate its own state.
State: A Component's Memory
To facilitate interactivity, components require dynamic memory allocation. State represents a mutable internal payload that, upon modification via bounded dispatcher functions, automatically schedules the component tree for subsequent reconciliation and re-rendering cycles.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>;
}useState(0) is a Hook. It gives you two things: the current value (count) and a function to update it (setCount). You never assign count directly — you call setCount, and that's what tells React to re-render.
Updating Objects & Arrays (Immutability)
When your state is an object or an array, you cannot modify it directly (e.g., user.name = "Ali"). React won't know it changed. Instead, you must create a new object or array using the spread operator (...):
// Updating an object
setUser({ ...user, name: "Ali" });
// Adding to an array
setItems([...items, newItem]);Updating based on Previous State
When the new state depends on the old state (like incrementing a counter), it's safer to pass a function to the setter. React guarantees this function gets the absolute most recent state.
// Safer counter update
setCount(prevCount => prevCount + 1);setState → automatic re-render
- Click calls
setCount(1). State changes from 0 → 1. - React re-renders automatically — the displayed number updates itself. You never touched the DOM.
This is the whole point of ReactIn vanilla JS you change data and manually update the DOM (two steps, easy to desync). In React you only change state — the UI is guaranteed to follow. One source of truth.
Under the Hood: State updates are asynchronous batchesCalling setCount(1) does not change count immediately. React queues the update and waits for the current event handler to finish before triggering a single, batched re-render. If you need the next state to depend on the previous, you must pass an updater function: setCount(prev => prev + 1).
Rendering Lists & Conditional UI
Advanced UI construction requires programmatic rendering vectors. Utilizing native iterative algorithms (e.g., Array.prototype.map) and logical branching (ternary operators, short-circuit evaluation), developers dynamically construct JSX topologies responsive to shifting underlying data schemas.
Lists: .map() + a key
Remember .map() from JavaScript? In React you use it to turn a data array into an array of elements. Each item needs a unique key so React can track it efficiently:
const fruits = ["🍎", "🍌", "🍇"];
return (
<ul>
{fruits.map((f, i) => <li key={i}>{f}</li>)}
</ul>
);Conditional rendering
Show different UI based on state using a normal ternary ? : or the && operator right inside your JSX:
{isLoggedIn
? <h2>Welcome back!</h2>
: <button>Log in</button>}A mapped list, then conditional UI
maprenders the first item, 🍎, as its own element.- …then 🍌…
- …then 🍇. One line of code, a whole list on screen.
- Now conditional UI: when
isLoggedInis true, show a welcome message. - Flip it to false and the same component shows a log-in button instead.
The danger of using index as keyReact uses key to tell items apart when the list changes. If you use the array index (0, 1, 2...) as a key, and then sort or delete an item, React gets confused because the indices shift. Always use a stable, unique ID from your database (like user.id). Use the index only if the list will never be reordered or filtered.
Under the Hood: The 'key' prop drives the Diffing algorithmWhen mapping arrays to JSX, React requires a unique key. Without it, if you reorder the list, React destroys and recreates the DOM nodes, wiping out focus and input state. The key tells the reconciliation engine exactly which item moved, allowing efficient DOM node recycling.
Forms & Controlled Components
Native HTML input nodes maintain their own internal state. The Controlled Component paradigm fundamentally overrides this behavior, forcing input values to derive strictly from React state, thereby centralizing the source of truth and permitting interceptive validation logic.
State is the single source of truth
In plain HTML, the input box itself remembers what you typed. In React, we flip that: a piece of state holds the value, the input is told to display that state (value={name}), and every keystroke updates the state through onChange. The input never stores anything on its own — it just mirrors state. That's a "controlled component."
const [name, setName] = useState("");
return (
<input
value={name} // shows state
onChange={e => setName(e.target.value)} // writes state
/>
);Typing flows through state and back to the UI
- The input is controlled: its
valueis read from state, and it has anonChangehandler ready. - The user types. Each keystroke fires
onChange, which callssetName(e.target.value)— writing to state. - State changes → React re-renders → the input and the live "Hello, Sara!" preview both update from the same value.
- The input can never drift out of sync with your data, because the data is the input. One source of truth.
Handling Multiple Inputs
Instead of making a separate useState for every single field in a large form, you can keep them in one object and use the input's name attribute to update the correct field dynamically:
const [form, setForm] = useState({ email: "", password: "" });
function handleChange(e) {
setForm({ ...form, [e.target.name]: e.target.value });
}Why bother?Because state holds the value, you can instantly validate as the user types, disable the submit button until the form is valid, transform input (uppercase, formatting), or pre-fill from a server — all just by reading and setting one piece of state. The form's data is always right there in your hands.
Under the Hood: Controlled inputs bypass the DOM's source of truthIn vanilla HTML, the element maintains its own memory. A Controlled Component binds the input's value to React state, forcing the React state to become the single source of truth. This interception allows instant validation, masking, or formatting before the user even sees the keystroke.
Routing: Multiple Pages in a SPA
Single Page Applications circumvent traditional HTTP navigation, necessitating virtualized routing architectures. Client-side routers intercept the native HTML5 History API, orchestrating component unmounting and mounting to simulate discrete hypermedia state transitions.
The URL picks the component
The core idea is beautifully simple: you map each URL path to a component. When the URL changes, React Router swaps in the matching component — without a full page reload, so it stays instant:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<Profile />} />
</Routes>The URL changes, the component swaps
- The URL is
/→ React Router shows the<Home>component. - Click an "About" link → the URL becomes
/aboutand<About>swaps in — no page reload, instant. - A dynamic route
/user/:idcaptures the id from the URL, so/user/42renders that specific profile. Real, shareable URLs on a single-page app.
Nested Routes & Layouts
React Router allows you to nest routes to share layouts (like a persistent sidebar) across multiple pages. An <Outlet /> component is used in the parent to render the matching child route.
<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="stats" element={<Stats />} />
<Route path="settings" element={<Settings />} />
</Route>Use <Link>, not <a>Inside a React Router app, navigate with its <Link to="/about"> component instead of a plain <a href>. A normal link triggers a full page reload (throwing away your app's state); <Link> swaps the component instantly and keeps everything alive.
Under the Hood: Client-side routing intercepts the History APIReact Router prevents the browser from sending a GET request to the server when a link is clicked. Instead, it intercepts the click, pushes a new URL to the browser's window.history, and synchronously swaps out the top-level React component, achieving instant navigation.
Context: Sharing State Globally
Deeply nested component trees suffer from structural "prop drilling," a recognized anti-pattern. The Context API establishes a localized topological broadcast system, enabling explicit Publisher-Subscriber data distribution that bypasses intermediate component layers.
The problem: prop drilling
Imagine the current user is known at the top of your app, but a tiny avatar component ten levels deep needs it. Without Context, every component in between must accept and forward the user prop — even ones that don't use it at all. Tedious and fragile.
The fix: a Provider any component can read
Context lets you put a value in one place (a Provider) and read it directly from any descendant with useContext, skipping all the layers in between:
// 1. Provide the value near the top
<UserContext.Provider value={user}>
<App />
</UserContext.Provider>
// 2. Read it anywhere below — no props needed
const user = useContext(UserContext);Context skips the layers
- The Provider at the top holds the value:
user = "Sara". - The middle layers — Layout, Sidebar — don't use
userat all. Without Context, they'd still have to forward it. (Prop drilling!) - With Context, the deep Avatar reads the value directly with
useContext— the data beams straight past the layers that don't care. Clean and scalable.
Custom Hooks for Context
Instead of importing both useContext and UserContext in every file, it's a best practice to create a custom hook to encapsulate it. This makes reading context cleaner and allows you to throw a helpful error if the context is missing.
export function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error("useUser must be used within a UserProvider");
}
return context;
}Context is for "global-ish" dataUse it for things many components need — the theme, the current user, the language. Don't reach for it for every piece of state; ordinary props are simpler and clearer for local data. For large, complex app state, teams often add a dedicated library (Redux, Zustand) — but Context covers most needs.
Under the Hood: Context causes unavoidable sub-tree rendersWhen a Context Provider's value changes, every component consuming that context is forced to re-render, bypassing React.memo. For high-frequency state (like mouse coordinates), Context will bottleneck the application. It is designed for low-frequency state like theme or auth.
Effects & Where to Go Next
Pure functional components must remain idempotent. The useEffect hook provides an escape hatch, allowing components to synchronize with external systems (DOM mutations, timers, network requests) strictly after the critical rendering pass has successfully executed.
useEffect(() => {
// runs after the component appears
fetch("/api/user")
.then(res => res.json())
.then(data => setUser(data));
}, []); // [] = run once, on mountThe Dependency Array
The second argument to useEffect is the dependency array. It tells React exactly when to re-run the effect:
useEffect(fn, [])— Runs only once when the component appears (mounts). Perfect for initial data fetching.useEffect(fn, [id])— Runs on mount, and wheneveridchanges. Useful for fetching new data when a user selects a different profile.useEffect(fn)(no array) — Runs on every single render. This is very rarely what you want and can cause infinite loops!
Cleanup Functions
If your effect creates something that runs continuously (like an interval, or an event listener), you must clean it up when the component is removed to prevent memory leaks. You do this by returning a cleanup function:
useEffect(() => {
const timer = setInterval(() => setTick(t => t + 1), 1000);
// React runs this cleanup function before the component unmounts
return () => clearInterval(timer);
}, []);A component loads data with useEffect
- The component appears on screen (it "mounts").
useEffectfires and starts fetching data from the server — the UI shows a loading state.- The response arrives…
setUser(data)updates state, React re-renders, and the real profile appears.
Advanced useEffect: Race Conditions
When fetching data based on a changing ID, old network requests might resolve after newer ones. You should use a boolean flag in your cleanup function to ignore stale responses:
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!ignore) setUser(data);
});
return () => { ignore = true; };
}, [userId]);Under the Hood: useEffect is for synchronization, not lifecycleDo not think of useEffect as 'run this when the component mounts.' Think of it as 'synchronize React state with an external system' (like a network, DOM API, or subscription). If your effect doesn't talk to an external system, you probably don't need an effect.
Custom Hooks: Reusable Logic
Architectural modularity demands the separation of business logic from presentation. Custom Hooks encapsulate discrete domains of state and effect management, providing composable, testable units of functional logic that can be injected into any functional component.
Writing a Custom Hook
A custom hook is just a JavaScript function whose name starts with "use" and that calls other Hooks.
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(d => { setData(d); setLoading(false); });
}, [url]);
return { data, loading };
}Now, instead of duplicating that code everywhere, any component can just call useFetch:
function UsersList() {
const { data, loading } = useFetch("/api/users");
if (loading) return <p>Loading...</p>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}Under the Hood: Hooks rely on strict call orderReact does not identify Hooks by name; it identifies them by the exact order they are called. This is why you can never place a Hook inside an if statement or a loop. If the call order shifts between renders, React's internal linked list of state cells corrupts.
Performance Optimization & Next Steps
React\'s default reconciliation cascade traverses the entire subtree of a mutated node. In highly complex applications, mitigating this cascading render cost necessitates strategic implementation of memoization heuristics (React.memo, useMemo, useCallback) to aggressively prune the reconciliation tree.
React.memo
You can wrap a component in React.memo() to skip re-rendering it if its props haven't changed. This is great for pure UI components.
const ExpensiveChart = React.memo(function Chart({ data }) {
// Huge D3 rendering logic...
return <svg>...</svg>;
});useMemo and useCallback
Sometimes you need to prevent heavy calculations on every render, or keep a function reference perfectly stable so it doesn't trigger React.memo updates in child components.
useMemocaches the result of a calculation until its dependencies change.useCallbackcaches a function definition until its dependencies change.
Where to go from here
You now understand React's core: components, JSX, props, state, effects, hooks, and optimization. That's genuinely the 95% of day-to-day React. To go professional, explore next:
- State Management Libraries — for massive applications, look into libraries like Zustand, Redux Toolkit, or Jotai.
- Modern Meta-Frameworks — take your React skills full-stack with Next.js or Remix for server-side rendering, better SEO, and API routes.
- React Native — use the exact same React knowledge you just learned to build real iOS and Android mobile apps.
You can build your own website nowAcross these courses you went from "what is HTML" to structuring, styling, programming and building complex interactive React apps. The only thing left is practice — pick a project you care about and build it.
Under the Hood: Memoization has a mathematical costWrapping components in React.memo or objects in useMemo is not free. You are trading CPU time (running the render function) for Memory overhead (storing previous props for comparison). Over-memoizing trivial components actually degrades performance. Measure before optimizing.
Advanced State: useReducer
Complex, interdependent state mutations within a component often result in race conditions. The useReducer hook formalizes state transitions into an explicit finite state machine, centralizing mutation logic into a deterministic, action-driven reducer function.
What is a Reducer?
A reducer is a pure function that takes the current state and an action, and returns the new state. This centralizes your state update logic outside of your component.
function cartReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, action.payload];
case 'remove':
return state.filter(item => item.id !== action.payload.id);
default:
return state;
}
}Using useReducer in a Component
Instead of calling a state setter, you call dispatch and pass an action object describing what happened. The reducer handles the how.
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, []);
return (
<button onClick={() => dispatch({ type: 'add', payload: { id: 1, name: 'Apple' } })}>
Add Apple
</button>
);
}When to use useReducer?If you find yourself writing setLoading(true), setError(null), and setData(null) all at the same time in multiple places, a single dispatch({ type: 'FETCH_START' }) is much cleaner and less prone to bugs.
Under the Hood: useReducer implements Finite State MachinesWhen multiple useState hooks must update in tandem (e.g., fetching = true, error = null), useReducer guarantees atomic state transitions. By dispatching intent ({ type: 'FETCH_START' }) rather than specific values, the business logic is entirely decoupled from the component render cycle.
Error Boundaries: Catching UI Crashes
Unhandled exceptions during the render phase fundamentally corrupt the React virtual DOM tree. Error Boundaries function as structural `catch` blocks within the component hierarchy, intercepting rendering faults to prevent total application failure and enabling graceful UI degradation.
What is an Error Boundary?
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. They catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}Class Components RequiredCurrently, there is no Hook equivalent to getDerivedStateFromError or componentDidCatch, so error boundaries must be written as class components. Alternatively, you can use the popular react-error-boundary library which provides a ready-made component and Hook.
Under the Hood: Error Boundaries cannot catch async errorsAn Error Boundary will catch render-phase exceptions. It will not catch errors inside setTimeout, fetch promises, or event handlers. To catch those, you must manually capture the error and push it into React's state to trigger the Boundary.
Portals: Escaping the DOM Tree
Strict adherence to structural DOM hierarchy can obstruct the execution of overlay interfaces (modals, tooltips) due to inherited z-index and overflow contexts. Portals bypass this constraint, permitting a component to dynamically mount its virtual children into a distinct physical DOM node.
Using createPortal
React provides Portals to achieve this. You can use createPortal(child, container) to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
import { createPortal } from 'react-dom';
function Modal({ children }) {
// Imagine <div id="modal-root"></div> exists in index.html
const modalRoot = document.getElementById('modal-root');
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
modalRoot
);
}Event Bubbling through PortalsEven though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way. Context works exactly the same, and crucially, events bubble up from the portal to its ancestors in the React tree, not the DOM tree. So a click inside a modal can be caught by its React parent!
Under the Hood: Portals preserve React Context propagationEven though a Portal physically renders a Modal at the end of the <body> tag, it remains logically rooted in its original position within the React Tree. Events bubble up the React tree, and Context flows down, completely ignoring the physical DOM structure.
Refs & DOM Access (useRef)
Declarative state cannot natively encompass imperative DOM operations or silent variable tracking. The useRef hook establishes a persistent, mutable reference pointer whose mutations explicitly bypass the reconciliation engine, preserving object identity across render cycles.
Accessing the DOM
Pass a ref object to a React element's ref attribute, and React will set its .current property to the actual DOM node.
function FocusInput() {
const inputRef = useRef(null);
function focusIt() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusIt}>Focus the input</button>
</>
);
}Storing Mutable Values
If you need to keep track of an interval ID, a previous state value, or a simple flag that shouldn't cause the UI to update when it changes, useRef acts like an instance variable in a class.
Under the Hood: useRef is an escape hatch from the render cycleA ref is essentially a mutable instance variable (a plain JavaScript object { current: value }) that survives re-renders. Mutating ref.current is synchronous and intentionally does not trigger a re-render. Use it for tracking DOM nodes or intervals, never for UI data.
Advanced Data Fetching (React Query)
Manual orchestration of asynchronous network requests within `useEffect` rapidly degenerates into architectural chaos. TanStack Query implements a highly robust server-state synchronization engine, automatically managing cache invalidation, deduplication, and background polling heuristics.
import { useQuery } from '@tanstack/react-query';
function UserProfile() {
const { data, isLoading, isError } = useQuery({
queryKey: ['user', 1],
queryFn: () => fetch('/api/user/1').then(res => res.json()),
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error fetching data!</div>;
return <div>Hello, {data.name}</div>;
}Under the Hood: Server state is fundamentally different from UI stateUI state (isModalOpen) is synchronous and owned by the client. Server state (userData) is asynchronous, cached, and owned by the database. Treating server data as UI state via useEffect leads to race conditions. React Query abstracts caching, invalidation, and background synchronization.
State Management (Zustand)
While the Context API facilitates simple prop propagation, its inherent lack of selective re-rendering limits its scalability. Architectures with heavy global flux demand sophisticated, atomic state management systems like Zustand or Redux Toolkit to enforce deterministic data flows.
import { create } from 'zustand';
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
function BearCounter() {
const bears = useStore((state) => state.bears);
return <h1>{bears} around here ...</h1>;
}Under the Hood: Atomic state management prevents render thrashingZustand and Jotai utilize an atomic, selector-based pattern. If a component subscribes to state.cartCount, it will only re-render when cartCount changes, ignoring mutations to state.userName. This granular subscription model provides massive performance gains over Context.
React Server Components & Next.js
The modern React paradigm transcends client-side rendering. Meta-frameworks like Next.js deploy React Server Components (RSC) and Streaming Server-Side Rendering (SSR), shifting heavy computational logic to the backend and delivering highly optimized, non-interactive HTML byte-streams to the client.
- SEO & Performance: Server rendering means search engines can easily index your pages, and users see content immediately.
- Direct Database Access: Server Components can securely talk to your database without exposing secrets to the browser.
import db from './database';
// This component runs EXCLUSIVELY on the server.
export default async function UsersPage() {
const users = await db.query('SELECT * FROM users');
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Under the Hood: React Server Components bypass the client bundleRSCs execute exclusively on the Node.js backend. They can read directly from a database or file system, and serialize into a proprietary byte-stream format. Because their code never reaches the browser, importing a heavy 3MB library in an RSC adds exactly 0kb to the client bundle.
You've mastered the fundamentals 🎓
HTML, CSS, JavaScript, the DOM and React — you now have the full front-end skill set. There's one course left: the capstone, where you put it all together and build a complete, real website from scratch.