A beginner-friendly React guide covering JSX, components, hooks, architecture, rendering, and lifecycle.
React is a JavaScript library for building user interfaces out of composable, reusable components. It uses a declarative model: you describe what the UI should look like for a given state, and React figures out how to update the DOM to match. You never manually write document.createElement or .appendChild — you describe the end result, and React handles the DOM manipulation for you.
Before diving into syntax, it's worth understanding what React is actually doing behind the scenes. This section explains the mental model that everything else in this guide builds on.
JSX isn't HTML — it's syntax sugar. Every JSX element compiles down to a React.createElement() call, which returns a plain JavaScript object describing what you want rendered. React never touches the real DOM at this step; it's just building a description of it.
That plain object tree is called the Virtual DOM: a lightweight, in-memory representation of what the UI should look like. It's just JavaScript objects — cheap to create and compare, unlike real DOM nodes, which are expensive to create and mutate.
Every time a component's state or props change, React doesn't throw away the real DOM and rebuild it from scratch. Instead it re-runs the component function to get a new Virtual DOM tree, then compares it against the previous Virtual DOM tree. This comparison process is called reconciliation.
React walks both trees and only applies the minimal set of real DOM changes needed to make the browser match the new tree — this final step is called the commit phase. If only a button's text changed, React updates that one text node, not the entire page.
This is why key matters so much when rendering lists (covered later): keys are how React matches up old and new tree nodes during reconciliation. Without a stable key, React can't reliably tell "this is the same item, just moved" from "this is a brand new item," which leads to bugs like input fields losing their value when a list reorders.
A component re-renders when its state changes (via a setter like setCount), when its props change, or when its parent re-renders. "Re-render" means React calls the function component again, top to bottom — the entire function body re-executes, not just the part that changed.
This is a common surprise for beginners: nothing in a function component "runs once and stays." Every render re-executes every line, which is why expensive calculations sometimes need useMemo (covered in the hooks section) to avoid redoing work unnecessarily.
React data flows in one direction: props flow down, events flow up. A parent passes data to a child via props; the child never directly modifies its parent's data. If a child needs to change something in the parent, the parent passes down a function as a prop, and the child calls that function — it hands the event back up rather than reaching up to mutate state directly.
This one-way flow makes apps easier to debug: if a value is wrong, you only need to trace it in one direction (up through the props it was passed) instead of hunting through code that could have mutated it from anywhere.
JSX is a syntax extension that lets you write HTML-like markup inside JavaScript, using the rules explained above — it compiles to React.createElement calls that build a Virtual DOM tree.
Any valid JavaScript expression goes inside curly braces. Attributes use camelCase instead of lowercase HTML.
class is reserved in JavaScript, so JSX uses className. Fragments group elements without adding an extra DOM node — useful because a component can only return a single root element.
JSX has no if statements inside markup, so conditionals rely on expressions. The ternary operator is the most common pattern for choosing between two outputs inline.
More conditional rendering patterns (the && operator and early returns) are covered in their own section further down, once components and state have been introduced.
Components are reusable, self-contained pieces of UI — JavaScript functions that return JSX. Modern React almost exclusively uses functional components (as opposed to older class-based components).
Props pass data from a parent component down to a child, following the unidirectional data flow described above.
Destructured parameters can define fallback values directly, used whenever a prop isn't passed in.
The special children prop holds whatever is nested between a component's tags. This enables the composition pattern (wrapping components around content) mentioned in the Tips section.
State lets a component remember and update data between renders. Without state, a component would forget everything and reset to its initial values on every re-render.
useState returns the current value and a setter function. Calling the setter schedules a re-render — it does not mutate the original variable, and it doesn't update the value immediately in the same line of code.
This trips up almost every beginner at some point: calling setCount does not update count right away. React schedules the update and re-renders the component later, batching multiple state updates together into a single re-render for performance.
The fix is the functional update form, covered next — it's the same category of problem javascript.mdx's let-in-a-loop example demonstrates: using a stale value captured at the wrong time.
When new state depends on the previous value, pass a function to the setter instead of a plain value. React guarantees this function receives the most up-to-date state, even across multiple queued calls.
Older React (class components) had explicit lifecycle methods: componentDidMount, componentDidUpdate, and componentWillUnmount. Function components don't have these methods — instead, useEffect covers all three phases, controlled by its dependency array. If you've seen lifecycle methods in a tutorial before, this table is the mental bridge:
| Lifecycle phase | Class component method | Hook equivalent |
|---|---|---|
| Mount (first render) | componentDidMount | useEffect(fn, []) |
| Update (on specific changes) | componentDidUpdate | useEffect(fn, [dep1, dep2]) |
| Unmount (removed from screen) | componentWillUnmount | cleanup function returned from useEffect |
Effects let components synchronize with external systems — APIs, subscriptions, timers, or the DOM — after the render has already happened and the DOM has been updated.
The dependency array controls when the effect re-runs. An empty array means "mount only" — it runs once, after the very first render, and never again.
Leaving the array off entirely makes the effect run after every render (rarely what you want); an empty array [] makes it run only once, on mount.
Returning a function from an effect cleans up subscriptions or timers. React calls this cleanup function right before the component is removed from the screen (unmount), and also right before the effect re-runs due to a dependency change — preventing duplicate timers or leaked subscriptions.
React wraps native DOM events in a consistent, cross-browser system called SyntheticEvent.
Call preventDefault() to stop the browser's default full-page reload.
Now that components and state are covered, here are the other two conditional rendering patterns you'll use constantly, beyond the ternary shown earlier.
Renders something, or nothing at all — useful when there's no "else" case.
Guards an entire component against an invalid or loading state, returning different JSX before the "main" render logic runs.
Use Array.prototype.map to turn an array of data into an array of elements. Keys help React's reconciliation process (explained in Architecture & Rendering) identify which items changed, were added, or were removed — and must be unique among siblings.
A controlled component keeps its value in React state, making state the single source of truth instead of the DOM. This fits the unidirectional data flow model: the input's displayed value always comes from state (down), and every keystroke updates that state (up) via onChange.
A closer look at the hooks you'll reach for most often beyond useState and useEffect, each with a complete, runnable example.
useMemo re-runs a calculation only when its dependencies change, instead of on every render. Reach for it when a computation is genuinely expensive (sorting or filtering a large array, heavy math) — for cheap operations, it adds overhead without meaningful benefit.
Every render creates brand-new function instances by default. useCallback keeps the same function reference across renders unless its dependencies change — important when passing callbacks to memoized child components, since a new function reference would defeat that memoization.
useRef holds a mutable value that survives across renders, but changing it does NOT trigger a re-render (unlike useState). It's most commonly used to directly access a DOM element.
Passing props down through five layers of components just so the bottom one can read a value is called "prop drilling." useContext lets any descendant read a value directly, as long as it's wrapped in that context's Provider — no need to pass it through every layer in between.
useMemo) and callbacks (useCallback) where it actually matters — not by default on every value.children or props) over inheritance.useEffect, not directly in the render body — remember, the entire function body re-runs on every render.