React, Explained Directly - Episode 1: The Fundamentals
Part 1: Why React Was Built
Nephew: Before React, how did people actually build websites?
Uncle: Directly. Every time something needed to change on a page - a price updates, a list item gets added - the code reached straight into the real page (the DOM, which stands for Document Object Model - it's the browser's live, in-memory representation of everything on the page) and changed it by hand, using plain JavaScript.
The problem: touching the real DOM is expensive. Every time you change something in it, the browser has to recalculate layout and repaint parts of the screen. If you're making dozens of small changes, doing each one directly means dozens of expensive recalculations, one after another.
Nephew: Didn't jQuery fix that?
Uncle: jQuery made it easier to write - shorter code, simpler syntax for finding and changing elements. But it didn't fix the underlying problem. You were still deciding, by hand, exactly what to change and when. As an app grows, manually tracking every possible change and writing code for each one becomes unmanageable.
Part 2: The Actual Problem That Led to React
Nephew: What specifically pushed teams toward inventing something like React?
Uncle: Scale, and constantly-changing UI. Think about a site like Facebook: a "like" count changes, a friend's status changes, a notification badge updates, comments load in - dozens of small UI updates per second, across millions of users simultaneously. If every one of those updates means directly touching the real DOM, performance falls apart fast.
So the core idea became: don't touch the real DOM directly for every small change. Instead, keep an in-memory copy, compute what actually needs to change, and only then touch the real DOM - and only for the parts that actually changed. That in-memory copy is the Virtual DOM.
Part 3: The Virtual DOM
Nephew: How does the Virtual DOM actually work, step by step?
Uncle:
- βοΈ You write your component code - this describes what the UI should look like.
- π§© React builds a lightweight JavaScript object representing that UI. This is the Virtual DOM. It is not the real DOM - it's just a plain data structure in memory, and building/comparing it is much cheaper than touching the real DOM.
- β‘ Something happens - a state change, a prop change, a user event.
- π§© React builds a new Virtual DOM tree, reflecting the updated UI.
- π React compares the old Virtual DOM tree to the new one. This comparison process is called reconciliation, and the specific step of finding what's different is called diffing.
- β React applies only the differences to the real DOM - not the whole page, just the specific nodes that actually changed.
Old Virtual DOMβββ
ββββΊdiffβββΊminimal set of real DOM updates
New Virtual DOMβββ
This is the entire performance trick: computation happens cheaply in memory first; only the minimal necessary write happens on the real, expensive DOM.
Part 4: React Fiber and Concurrent Rendering
Nephew: What is "Fiber" and why does it matter?
Uncle: Fiber is the internal engine React uses to actually perform rendering work. Before Fiber, React's rendering process was synchronous and blocking - once React started rendering an update, it had to finish the entire thing before it could do anything else, including responding to new user input. That created a real problem: if a large re-render was in progress (say, rendering a long list) and the user typed in a search box at that exact moment, the typing could visibly lag, because React couldn't interrupt the render in progress to handle the more urgent keystroke.
Nephew: How does Fiber fix that?
Uncle: Fiber restructured rendering into small, interruptible units of work instead of one uninterruptible block. This enables Concurrent Rendering: React can pause a lower-priority render (like an off-screen update or a background data render), immediately handle a higher-priority update (like user input), and then resume the paused work afterward.
Old React: [ render block, cannot be interrupted ] β then handle input
New React: [ render chunk ] β urgent input arrives β [ handle input immediately ] β [ resume render ]
This scheduling is automatic - React's internal scheduler decides priority; you don't manually configure this for basic use, although APIs like useTransition and useDeferredValue let you hint priority explicitly for specific updates (covered in Episode 2).
Part 5: JSX
Nephew: Why does React let us write HTML-like syntax directly inside JavaScript?
Uncle: JSX is not something browsers understand natively. It's a syntax extension that gets compiled ("transpiled") into plain JavaScript function calls before it ever runs in a browser. The tool that does this is Babel (or, in modern toolchains, a similarly-purposed compiler bundled into tools like Vite or Next.js).
Before React 17, JSX compiled into calls to React.createElement:
// You write:
<h1>Welcome!</h1>
// It compiles to:
React.createElement('h1', null, 'Welcome!')
Since React 17, the compiled output no longer requires importing React in every file that uses JSX - the new JSX transform handles it differently under the hood, without changing how you write JSX day to day.
Rules JSX enforces, because it's structurally closer to XML than HTML:
- π Every tag must be explicitly closed (
<img />, not<img>) - π·οΈ
classbecomesclassName(becauseclassis a reserved word in JavaScript) - π« Multi-word HTML attributes become camelCase (
onclickβonClick,tabindexβtabIndex)
Part 6: Components
Nephew: What is a component, precisely?
Uncle: A function (or, in older code, a class) that returns JSX describing part of the UI. Components are meant to be composed - you build small components and combine them into larger ones, forming a tree structure.
App
/ \
Header Main
/ | \ |
Logo Nav ProductList
Nephew: Can the same component be used multiple times?
Uncle: Yes, and each usage is an independent instance - its own state, its own lifecycle, completely isolated from other instances of the same component elsewhere in the tree.
Function components (the modern standard) are plain functions:
function Welcome() {
return <h1>Hello</h1>;
}
Class components (the older style, still valid, still used in legacy codebases) extend React.Component:
class Welcome extends React.Component {
render() {
return <h1>Hello</h1>;
}
}
There was an even older pattern, React.createClass, deprecated since React 15.5 - you won't encounter it in modern code, but it's worth knowing it existed if you ever read old tutorials or legacy code.
Part 7: Props vs State
Nephew: What's the actual difference?
Uncle:
- π¦ Props - data passed into a component from its parent. Read-only from the receiving component's perspective - a component must never mutate its own props directly.
- ποΈ State - data a component manages internally, that it can change itself, typically in response to user interaction or other events.
Parent
β (props passed down)
βΌ
Child (uses props, manages its own state internally)
β (calls a function passed down as a prop, to notify parent of something)
β²
Parent (receives the notification, updates its own state if needed)
Nephew: Why is data flow one-directional like that?
Uncle: Because two-way binding (where a child can directly mutate a parent's data) makes it hard to trace why something changed - was it user interaction in the child, or a change originating from the parent? One-directional flow keeps updates traceable: data flows down through props, and any need to communicate upward happens through callback functions the parent explicitly provides - not by the child reaching up and mutating parent state directly.
This is also why mutating props directly is disallowed - React relies on the parent being the single source of truth for that data; if a child could silently change it, React's re-render logic and the parent's own state would fall out of sync.
Part 8: Class Component Internals
Nephew: What's actually happening with this, bind, and constructor in class components?
Uncle: The constructor runs once, when the component instance is created:
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
super(props) is required if you want to access this.props inside the constructor - it passes props up to React.Component's own constructor first.
β οΈ Why binding matters: In JavaScript, a function's this depends on how the function is called, not where it was defined. If you pass this.handleClick as a callback (say, to an onClick handler) without binding it, this inside handleClick will be undefined when it actually runs - because it's being called as a plain function reference, disconnected from the component instance.
// Without binding - this is undefined inside handleClick when it's actually called
<button onClick={this.handleClick}>Click</button>
// Binding fixes it:
this.handleClick = this.handleClick.bind(this);
β±οΈ Why setState doesn't update immediately:
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // may still log the OLD value
React batches state updates for performance - it doesn't necessarily apply and re-render on every single setState call the instant it's called. If your next value depends on the previous one, use the function form to get the guaranteed-current value:
this.setState((prevState) => ({ count: prevState.count + 1 }));
Part 9: Component Lifecycle (Class Components)
Nephew: What is "lifecycle," specifically?
Uncle: The sequence of methods React calls automatically as a component is created, updated, and removed.
π± Mounting (first render): constructor β render β componentDidMount
componentDidMount runs once, right after the component first appears in the DOM. This is the correct place to fetch initial data or set up subscriptions, because the DOM node now actually exists.
π Updating (state or props change): shouldComponentUpdate β render β componentDidUpdate
shouldComponentUpdate lets you explicitly return false to skip re-rendering entirely, if you know a particular change doesn't affect this component's output - a manual performance optimization.
πͺ Unmounting (component removed): componentWillUnmount
This runs right before the component is removed. Anything the component started that would otherwise keep running - timers, subscriptions, event listeners - must be cleaned up here.
Nephew: What happens if that cleanup is skipped?
Uncle: A memory leak. Something the component started - a setInterval, a subscription, an event listener - keeps running even though the component that created it no longer exists. This wastes resources and, worse, can cause errors if that leftover code tries to update state on a component that's already gone.
Part 10: Hooks - The Full Toolbox
Nephew: Why do hooks exist at all? Why not just always use classes?
Uncle: Two real reasons, and both matter.
Reason 1 - plain JavaScript functions don't retain data between calls. Every time a function runs, it starts fresh. Before hooks, function components had no way to hold onto data (like a counter, or a toggle) across re-renders - this is why they were once called "stateless" components. Hooks give function components the ability to hold and update data across renders, matching what class components already did with this.state.
Reason 2 - sharing logic between class components was genuinely painful. If two unrelated class components both needed the same piece of stateful behavior (say, "track window width" or "subscribe to a websocket"), your only real options were awkward patterns like Higher-Order Components or render props - both of which wrap your component in extra layers, making the component tree harder to read and debug. Hooks let you extract that exact same logic into a plain function (a custom hook, covered below) and reuse it directly, with no wrapping, no extra tree depth, no confusion about where a prop actually came from.
Nephew: So every hook is really just solving one of those two problems - "give me memory" or "let me reuse logic"?
Uncle: Exactly that. Keep that lens on as we go through each one - for every hook, ask "is this managing memory across renders, or letting me reuse/organize logic?" It'll make the whole list click into place instead of feeling like a random API surface to memorize.
useState - holding a value across renders
const [count, setCount] = useState(0);
Why we need it: a function component, by itself, forgets everything between renders. useState is React's way of saying "keep this specific value alive, tied to this specific component instance, across every re-render, until the component unmounts."
You never mutate count directly. Calling setCount(newValue) tells React to schedule a re-render with the new value. count itself is treated as immutable within a single render - it only "updates" because the next render gets a fresh value.
// If the next value depends on the current one, use the function form -
// it always receives the guaranteed-latest value, avoiding stale-value bugs:
setCount(prev => prev + 1);
useEffect - reaching outside the component
useEffect(() => {
// runs after the component mounts
}, []); // empty dependency array = run once, on mount only
useEffect(() => {
// runs after mount, and again every time `count` changes
}, [count]);
useEffect(() => {
const id = setInterval(() => {}, 1000);
return () => clearInterval(id); // cleanup - runs before unmount, or before the effect re-runs
}, []);
Why we need it: rendering a component should be a pure calculation - given the same props/state, it should produce the same output, with no side effects like fetching data, subscribing to something, or manually touching the DOM. But real apps need side effects. useEffect is the designated, controlled place for them - it runs after React
Comments
No comments yet. Start the discussion.