React useEffect Missing Dependency — Stale Profile Data Fix
Missing dependency in React useEffect causes stale UI on userId switch.
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- React is a declarative UI library — you describe what the UI should look like for a given state, and React handles the DOM updates.
- Components are reusable JS functions returning JSX; props flow one-way from parent to child.
- State is local data managed via useState; never mutate state directly — always use the setter to trigger a re-render.
- Virtual DOM is a JavaScript object tree diffed against the previous render; React applies only the minimal real DOM changes after reconciliation.
- Performance insight: React's batch updates and fiber architecture keep re-renders fast, but unnecessary renders from wrong use of useEffect or missing keys can slow down complex UIs.
- Production insight: Stale closures in useEffect (missing dependencies) cause memory leaks and outdated data; always include all variables the effect reads in the dependency array.
React is a library for building user interfaces through components. Unlike a framework, it handles only the view layer: rendering UI and reacting to state changes. React's core insight is declarative rendering — you describe what the UI should look like for a given state, and React figures out how to update the actual DOM efficiently.
This eliminates the manual DOM traversal and mutation that plagues vanilla JavaScript apps. Instead of writing document.getElementById chains, you write functions that return a description of the UI. React then reconciles that description (a virtual representation) with the real browser DOM, applying only the minimal changes needed.
This architecture makes applications predictable, testable, and easier to reason about. React is not a complete solution — you add routing, data fetching, and state management from the ecosystem as needed. But for UI logic alone, React dominates because it removed the hardest part of frontend development: keeping the DOM in sync with application state.
Imagine you're building a LEGO city. Instead of sculpting the whole city from one block of clay — which means restarting whenever you want to move a building — you build it from individual LEGO bricks that snap together. Each brick knows exactly what it looks like. React is the same idea: you build your UI from small, reusable 'component' bricks, and when one brick changes, only that brick gets rebuilt — not the whole city.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Every time you 'like' a post on Instagram, see a live search result drop down, or watch a shopping cart update without the whole page reloading — you're watching React do its job. React is a JavaScript library built by Meta in 2013 that fundamentally changed how developers think about building user interfaces. It's now used by Facebook, Airbnb, Netflix, and thousands of other production apps because it makes complex UIs manageable without turning your codebase into a tangled mess of DOM manipulations.
What useEffect Missing Dependencies Actually Means
The useEffect hook in React lets you synchronize a component with an external system — an API call, a subscription, or a DOM event. Its dependency array tells React when to re-run the effect: only when a listed value changes. Omitting a dependency that the effect closure captures creates a stale closure: the effect sees the initial value forever, not the current one.
When the dependency array is missing or incomplete, React runs the effect on every render (no array) or only on mount (empty array). In both cases, any variable referenced inside the effect — like a user ID from props — is captured at the time the effect was created. Subsequent renders update the component's state, but the effect's closure still holds the old reference. This is a direct consequence of JavaScript's lexical scoping, not a React bug.
You must list every reactive value (props, state, derived values) that the effect reads. The React compiler (or the eslint-plugin-react-hooks) enforces this at build time. In production, a missing dependency causes silent data staleness: a profile page shows yesterday's data because the effect never re-fetches when the user ID changes. The fix is always to include the missing variable in the array, or to restructure the effect to avoid reading stale values.
Why React Exists — The Problem With Vanilla DOM Manipulation
Before React, building a dynamic UI meant manually querying the DOM and surgically updating elements. This works fine for a simple counter, but it falls apart fast. Imagine a social feed: a new like comes in, which changes the like count, which might affect whether the 'Popular' badge shows, which might reorder the feed. Now you're tracking every dependency by hand, syncing state between a dozen DOM nodes, and praying nothing gets out of sync.
React's core insight is this: instead of describing HOW to change the UI step by step, you describe WHAT the UI should look like for any given state, and let React figure out the minimal set of DOM changes needed. It's declarative rather than imperative — you write the 'end result', not the 'recipe'.
This mental shift is the real reason React matters. Your code describes intent, not procedure. That makes it dramatically easier to reason about, test, and maintain — especially in a team where multiple people are touching the same UI.
// ─── VANILLA JS APPROACH ─────────────────────────────────────────── // Manually track and update every affected DOM node when data changes. // This gets chaotic the moment your UI has more than a handful of states. let likeCount = 0; function handleLikeClick() { likeCount += 1; // Manually update the counter label document.getElementById('like-count').textContent = likeCount; // Manually update the button colour based on whether we've liked it const likeButton = document.getElementById('like-btn'); likeButton.style.color = likeCount > 0 ? 'blue' : 'grey'; // Manually show a 'Popular' badge once it crosses a threshold const badge = document.getElementById('popular-badge'); badge.style.display = likeCount >= 10 ? 'block' : 'none'; // ... and it keeps growing. Every new rule = more manual syncing. } // ─── REACT APPROACH ──────────────────────────────────────────────── // Describe WHAT the UI should look like for any given likeCount. // React calculates the minimum DOM changes needed — you never touch the DOM. import React, { useState } from 'react'; function LikeButton() { // likeCount is our single source of truth. React re-renders this // component whenever likeCount changes — we don't manually update anything. const [likeCount, setLikeCount] = useState(0); const hasLiked = likeCount > 0; const isPopular = likeCount >= 10; return ( <div> {/* The UI is a pure function of likeCount — no manual DOM calls */} <button onClick={() => setLikeCount(likeCount + 1)} style={{ color: hasLiked ? 'blue' : 'grey' }} id="like-btn" > 👍 {likeCount} </button> {/* Conditional rendering: React handles show/hide based on state */} {isPopular && <span id="popular-badge">🔥 Popular</span>} </div> ); } export default LikeButton;
Components and Props — Building Real UI From Reusable Pieces
A React component is just a JavaScript function that returns JSX — a syntax that looks like HTML but is actually JavaScript under the hood. The power isn't in the syntax though. It's in composability: components can contain other components, and data flows down through props (short for properties) like water flowing downhill.
Props are how a parent component communicates with a child. They're read-only from the child's perspective — a child component never modifies its own props. This one-way data flow is intentional. It makes debugging dramatically easier because you always know where data originates.
The real-world pattern you'll use constantly is the 'smart parent / dumb child' split. A parent component fetches data and holds state. It passes that data down as props to presentational child components that just render what they receive. This separation keeps your logic in one place and your UI components reusable across the whole app.
// ─── ProductCard.jsx ──────────────────────────────────────────────── // A reusable 'dumb' component — it receives product data as props // and renders it. It has no idea where the data came from. import React from 'react'; // Destructure props directly in the parameter list for clarity. // This component will re-render whenever its props change. function ProductCard({ name, price, imageUrl, isOnSale }) { return ( <div className="product-card"> <img src={imageUrl} alt={name} /> <h3>{name}</h3> <div className="price-block"> {/* Conditional: show a sale badge only when isOnSale is true */} {isOnSale && <span className="sale-badge">SALE</span>} {/* Template literal makes the price format explicit */} <p className="price">${price.toFixed(2)}</p> </div> </div> ); } // ─── ProductList.jsx ───────────────────────────────────────────────── // The 'smart parent' — it owns the data and maps it into ProductCards. // ProductCard doesn't care how many products there are or where they come from. import React from 'react'; import ProductCard from './ProductCard'; function ProductList() { // In a real app this data would come from an API via useEffect/fetch. // For now, hardcoded to keep the focus on component composition. const products = [ { id: 1, name: 'Mechanical Keyboard', price: 129.99, imageUrl: '/images/keyboard.jpg', isOnSale: false, }, { id: 2, name: 'Ergonomic Mouse', price: 49.99, imageUrl: '/images/mouse.jpg', isOnSale: true, }, { id: 3, name: 'USB-C Hub', price: 34.99, imageUrl: '/images/hub.jpg', isOnSale: true, }, ]; return ( <section className="product-list"> <h2>Our Products</h2> {/* Map over the array — each item becomes a ProductCard */} {/* The 'key' prop is mandatory here. React uses it to track */} {/* which items changed, added, or removed in the list. */} {products.map((product) => ( <ProductCard key={product.id} name={product.name} price={product.price} imageUrl={product.imageUrl} isOnSale={product.isOnSale} /> ))} </section> ); } export default ProductList;
key prop in lists that can reorder or filter — e.g., key={index}. React uses keys to identify which DOM nodes to reuse. If item order changes, React will match the wrong nodes and produce subtle UI bugs like inputs keeping the wrong values. Always use a stable, unique ID from your data.One-way Data Flow — Why Props Flow Down and State Stays Local
React's architecture is built around one simple rule: data flows in one direction — from parent to child via props. Child components cannot modify their props; they can only read them. If a child needs to communicate back to the parent, it does so by calling a callback function passed down as a prop. This pattern keeps your application predictable: you always know where a piece of data came from because it flows downward from a single source of truth.
State, on the other hand, is local to a component. When a component needs to manage data that changes over time (like form inputs, toggle switches, or fetched data), it uses useState to create a state variable. That state is private to the component and its children (via props). Lifting state up means moving state to a common ancestor so multiple children can share it — but even then, the data flow remains one-way.
This visual shows the flow: parent holds state, passes it down as props to children. Children can call event handlers (also passed as props) to tell the parent something happened, but the parent decides how to update state.
// ─── Parent Component (owns state) ─── function Parent() { const [count, setCount] = useState(0); // Pass state down as props, pass event handler down return ( <div> <p>Count: {count}</p> <Child count={count} onIncrement={() => setCount(c => c + 1)} /> </div> ); } // ─── Child Component (reads props, calls callback) ─── function Child({ count, onIncrement }) { return ( <button onClick={onIncrement}> Clicked {count} times </button> ); }
State and the Virtual DOM — How React Knows What to Re-render
State is data that, when it changes, should cause the UI to update. That's the full definition. React gives you useState to manage local component state, and the rules are simple: never mutate state directly — always call the setter function. This isn't bureaucracy; it's how React detects that something changed.
When you call a state setter, React doesn't immediately blast the real DOM with updates. Instead it re-runs your component function to produce a new virtual DOM — a lightweight JavaScript object tree describing what the UI should look like. React then diffs the new virtual DOM against the previous one (this is called 'reconciliation'), finds the minimum set of actual DOM changes, and applies only those. This is why React is fast even for complex UIs.
Understanding this flow — state change → re-render → virtual DOM diff → minimal real DOM update — explains most of React's behaviour, including why state updates can feel 'async' and why you should keep expensive calculations out of the render path.
// ─── ShoppingCart.jsx ─────────────────────────────────────────────── // A practical example that shows: useState, state updates via setter, // and derived state (calculating total from cart items). import React, { useState } from 'react'; // Initial product catalogue — would normally come from an API. const AVAILABLE_PRODUCTS = [ { id: 101, name: 'React T-Shirt', price: 25 }, { id: 102, name: 'JavaScript Mug', price: 12 }, { id: 103, name: 'TypeScript Hoodie', price: 55 }, ]; function ShoppingCart() { // cartItems holds an array of products the user has added. // We initialise it as empty — nobody has added anything yet. const [cartItems, setCartItems] = useState([]); function addToCart(product) { // NEVER do: cartItems.push(product) — this mutates state directly. // React won't detect the change and the UI won't update. // // CORRECT: create a NEW array with the spread operator. // React sees a new reference → triggers a re-render. setCartItems([...cartItems, product]); } function removeFromCart(productId) { // Filter returns a new array — again, no mutation. setCartItems(cartItems.filter((item) => item.id !== productId)); } // Derived state: calculate the total from cartItems on every render. // Don't store total in its own useState — it's always derivable from cartItems. // Storing it separately would create two sources of truth that can drift apart. const cartTotal = cartItems.reduce((total, item) => total + item.price, 0); return ( <div className="shopping-cart"> {/* ── Product Catalogue ── */} <section className="catalogue"> <h2>Catalogue</h2> {AVAILABLE_PRODUCTS.map((product) => ( <div key={product.id} className="catalogue-item"> <span>{product.name} — ${product.price}</span> <button onClick={() => addToCart(product)}>Add to Cart</button> </div> ))} </section> {/* ── Cart ── */} <section className="cart"> <h2>Your Cart ({cartItems.length} items)</h2> {cartItems.length === 0 ? ( // React renders this when the array is empty <p>Your cart is empty. Add something!</p> ) : ( cartItems.map((item, index) => ( // Using index as key here is acceptable ONLY because we remove // from the end and items don't reorder — see the callout below. // In production, use item.id (or a cart-entry UUID) instead. <div key={`${item.id}-${index}`} className="cart-item"> <span>{item.name}</span> <span>${item.price}</span> <button onClick={() => removeFromCart(item.id)}>Remove</button> </div> )) )} {cartItems.length > 0 && ( <div className="cart-total"> <strong>Total: ${cartTotal}</strong> </div> )} </section> </div> ); } export default ShoppingCart;
cartTotal from cartItems — calculate it during render instead of storing it in a second useState. Two states that must stay in sync will eventually drift apart, creating hard-to-reproduce bugs. React re-renders are fast enough that deriving values inline is almost never the performance bottleneck you think it is.Virtual DOM Reconciliation — How React Differs Before Painting the Screen
The Virtual DOM is a JavaScript object that mirrors the structure of the real DOM. When state changes, React creates a new Virtual DOM tree, then compares (diffs) it against the previous tree using its reconciliation algorithm. Instead of updating every real DOM node, React computes the minimal set of changes needed and applies them in a batch. This process avoids expensive layout thrashing and repaints.
Reconciliation relies on two heuristics: (1) different component types produce different trees — React will tear down the old tree and build a new one from scratch if the type changes; (2) keys in lists identify which children are stable across renders. These assumptions make the algorithm O(n) for typical UI trees, rather than O(n^3) for a naive tree diff.
The diagram below illustrates a simple reconciliation: when a state update causes a list item's text to change, React identifies only the changed text node and updates it, rather than re-rendering the entire list.
// Initial render: // <ul> // <li key="a">Apple</li> // <li key="b">Banana</li> // </ul> // After state update (change "Banana" to "Blueberry"): // React diffs the virtual trees: // - Same root <ul> type: update in place // - Same keys "a" and "b": match existing DOM nodes // - Only the text content of the second <li> changed // Result: React updates only that text node, leaving the rest untouched.
Fetching Real Data — useEffect and the Component Lifecycle
So far our data has been hardcoded. Real apps fetch data from APIs, and that's where useEffect comes in. The hook lets you synchronise your component with something outside React — a network request, a timer, a WebSocket, or a browser API.
The dependency array is the most misunderstood part of useEffect. It tells React WHEN to re-run the effect: empty array [] means run once after the first render (equivalent to 'on mount'); an array with values like [userId] means re-run whenever userId changes; no array at all means run after EVERY render (almost never what you want).
The cleanup function returned from useEffect is equally important and equally ignored by beginners. It runs before the component unmounts or before the effect re-runs. Without cleanup, you can end up with memory leaks, stale data from cancelled requests populating your state, or event listeners that stack up forever.
// ─── UserProfilePage.jsx ──────────────────────────────────────────── // Real-world pattern: fetch user data when a userId changes, // handle loading & error states, and clean up properly to avoid // updating state on an unmounted component. import React, { useState, useEffect } from 'react'; function UserProfilePage({ userId }) { const [userProfile, setUserProfile] = useState(null); const [isLoading, setIsLoading] = useState(true); const [fetchError, setFetchError] = useState(null); useEffect(() => { // AbortController lets us cancel the fetch if userId changes // before the previous request finishes — avoids race conditions. const abortController = new AbortController(); // Reset state before each fresh fetch so stale data doesn't linger setIsLoading(true); setFetchError(null); setUserProfile(null); async function loadUserProfile() { try { const response = await fetch( `https://jsonplaceholder.typicode.com/users/${userId}`, { signal: abortController.signal } // tie the request to the controller ); if (!response.ok) { throw new Error(`Server returned ${response.status}`); } const data = await response.json(); setUserProfile(data); // update state with the fetched profile } catch (error) { // AbortError is expected when we clean up — not a real error if (error.name !== 'AbortError') { setFetchError(error.message); } } finally { // Only mark loading as done if the request wasn't aborted if (!abortController.signal.aborted) { setIsLoading(false); } } } loadUserProfile(); // Cleanup: cancel the in-flight request if userId changes or // the component unmounts before the fetch completes. return () => { abortController.abort(); }; }, [userId]); // Re-run this entire effect whenever userId changes // ── Render the right UI for each data-fetching state ── if (isLoading) { return <div className="skeleton-loader">Loading profile...</div>; } if (fetchError) { return ( <div className="error-banner"> <p>Could not load profile: {fetchError}</p> <p>Check your connection and try again.</p> </div> ); } return ( <div className="user-profile"> <h1>{userProfile.name}</h1> <p>📧 {userProfile.email}</p> <p>🏢 {userProfile.company?.name}</p> <p>🌐 {userProfile.website}</p> </div> ); } export default UserProfilePage;
React Hooks Lifecycle — When useEffect, useLayoutEffect, and Cleanup Fire
Understanding when React hooks execute is essential for building reliable components. The lifecycle of a functional component with hooks can be broken into three phases: mount, update, and unmount. On mount, React runs the component function, renders JSX, commits the DOM, then runs effects in order: useLayoutEffect fires synchronously after DOM mutations, useEffect fires later asynchronously. On update (state or props change), the same cycle repeats: render, commit, then cleanup of previous effects (if dependencies changed), then new effect. On unmount, cleanup functions for effect hooks run.
This visual shows the exact sequence for a component that uses useState and useEffect. Each phase triggers specific hooks and cleanup functions.
import React, { useState, useEffect, useLayoutEffect } from 'react'; function LifecycleDemo({ id }) { const [count, setCount] = useState(0); useEffect(() => { console.log('useEffect mount/update', id, count); return () => { console.log('useEffect cleanup', id, count); }; }, [id, count]); useLayoutEffect(() => { console.log('useLayoutEffect - synchronously after DOM commit'); }, [id]); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }
Performance Optimisation: Memoization and Avoiding Unnecessary Re-renders
React's default behaviour is to re-render a component whenever its parent re-renders, even if the props haven't changed. This is intentional — React prioritises correctness over optimisation. But in performance-critical parts of your app, unnecessary re-renders can cause jank, especially with large lists or heavy components.
React gives you three tools to prevent wasted renders: React.memo, useMemo, and useCallback. React.memo wraps a component and only re-renders it when its props change (by shallow comparison). useMemo caches the result of an expensive computation between renders unless its dependencies change. useCallback caches a function reference so that child components using it as a prop don't re-render unnecessarily.
Here's the trade-off: memoization adds overhead from comparison and memory. Don't wrap everything — profile first, then memoize. The rule of thumb is to use React.memo on components that receive the same props frequently and don't change, and use useMemo/useCallback only when you've measured a performance problem.
// ─── ExpensiveList.jsx ────────────────────────────────────────────── // Demonstrates performance optimisation with React.memo and useCallback. import React, { useState, useCallback, useMemo } from 'react'; // A heavy presentational component — wraps with React.memo // so it only re-renders when its props actually change. const ExpensiveRow = React.memo(({ item, onToggle }) => { console.log('ExpensiveRow rendered:', item.id); return ( <div className="row"> <span>{item.name}</span> <button onClick={() => onToggle(item.id)}> {item.completed ? '✓ Done' : '○ Pending'} </button> </div> ); }); function ExpensiveList() { const [items, setItems] = useState( Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}`, completed: false, })) ); const [searchTerm, setSearchTerm] = useState(''); // useCallback ensures onToggle's reference stays stable across renders // so ExpensiveRow doesn't re-render just because the parent re-rendered. const handleToggle = useCallback((id) => { setItems((prev) => prev.map((item) => item.id === id ? { ...item, completed: !item.completed } : item ) ); }, []); // useMemo avoids filtering the entire list on every search keystroke // if the searchTerm hasn't changed. const filteredItems = useMemo(() => { if (!searchTerm) return items; return items.filter((item) => item.name.toLowerCase().includes(searchTerm.toLowerCase()) ); }, [items, searchTerm]); return ( <div> <input placeholder="Filter items..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> {filteredItems.map((item) => ( <ExpensiveRow key={item.id} item={item} onToggle={handleToggle} /> ))} </div> ); } export default ExpensiveList;
- React.memo wraps a component and does shallow prop comparison before re-render.
- useMemo caches the result of a function call until its dependencies change — use it for expensive calculations inside render.
- useCallback caches a function reference — pass it as a prop to memoized children to keep their props stable.
- Over-memoizing can hurt: the comparison itself has a cost. Profile with React DevTools before adding memoization.
- Default behaviour (re-render on every parent render) is fine for small component trees — don't optimise prematurely.
What React Actually Is (and Isn't)
React is a library for building user interfaces. That's it. It's not a framework. Angular is a framework. Vue straddles the line. React handles one job: rendering UI components and keeping them in sync with your application state.
Facebook released it in 2013 because they had a specific problem: their DOM was a tangled mess of event listeners, manual state management, and inconsistent UI states. Sound familiar? That's every jQuery app ever written.
Here's what React doesn't do: routing, HTTP clients, form validation, state management beyond useState, or data fetching. Those are ecosystem problems. You bring your own tools. This isn't a weakness — it's by design. React stays out of your way so you can architect your app however you want.
The misconception that React is a 'full framework' causes more confusion than any other single thing. Junior devs expect it to have opinions about everything. It doesn't. It has opinions about components, state, and rendering. Everything else is up to you.
// io.thecodeforge — javascript tutorial // React isn't a framework — it won't fetch data for you import { useState, useEffect } from 'react'; function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); // You bring your own data fetching useEffect(() => { fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => { setUser(data); setLoading(false); }); }, [userId]); if (loading) return <div>Loading...</div>; return <div>{user.name}</div>; }
JSX — Why It Exists and What It Compiles To
JSX looks like HTML in your JavaScript. It isn't. It's syntactic sugar for React.createElement calls. Every JSX tag you write gets compiled into a function call that returns a plain JavaScript object called a React element.
Why did Facebook create it? Because building UIs with pure JavaScript is painful. Try writing a nested component tree with vanilla document.createElement — you'll end up with a spaghetti factory of nested function calls and string concatenation. JSX gives you a declarative syntax that mirrors the UI structure.
Here's the critical part: JSX is NOT HTML. That className attribute? It's class in HTML. htmlFor instead of for. These aren't arbitrary decisions — they exist because class and for are reserved words in JavaScript. JSX compiles to JavaScript, not HTML.
The compiler (Babel or TypeScript) transforms <div className="container"> into React.createElement('div', { className: 'container' }). That returns an object: { type: 'div', props: { className: 'container' }, children: [] }. React then uses this object to build the virtual DOM.
Stop thinking of JSX as 'HTML in JavaScript.' It's 'JavaScript that looks like HTML.'
// io.thecodeforge — javascript tutorial // What you write (JSX) const element = ( <div className="user-card"> <h1>Hello, User</h1> </div> ); // What it compiles to (plain JavaScript) const element = React.createElement( 'div', { className: 'user-card' }, React.createElement('h1', null, 'Hello, User') ); // The resulting React element object // { // type: 'div', // props: { // className: 'user-card', // children: { // type: 'h1', // props: { children: 'Hello, User' } // } // } // }
class becomes className and why you can't use if statements inside JSX.className and htmlFor exist because of JavaScript reserved words.The JavaScript You Actually Need Before React
You don't need to be a JavaScript wizard to start React. But you need to be comfortable with three specific concepts before you touch your first component: arrow functions, destructuring, and the spread operator. Learn these, or you'll spend more time debugging syntax than building UI.
Arrow functions matter because React components are functions. You'll use them everywhere — from event handlers to callbacks. The this binding confusion that plagues class components vanishes with arrow functions.
Destructuring isn't optional. Every single React tutorial, including this one, destructures props: function Card({ title, description }). If you don't understand const { name, age } = person, you'll be lost in the first five minutes.
The spread operator (...) is how you handle immutable state updates. You'll see setUsers([...users, newUser]) and setState({ ...prevState, updatedField: value }) constantly. Without it, you'll accidentally mutate state and trigger bugs that are maddeningly difficult to debug.
Promises and async/await? Yes, for data fetching. Array methods like .map(), .filter(), .reduce()? Absolutely — React loves to transform arrays into JSX. If you can't confidently use these, spend a week with plain JavaScript before jumping in. Your future self will thank you.
// io.thecodeforge — javascript tutorial // You will use this pattern EVERYWHERE in React // Arrow functions + destructuring + spread const UserList = ({ users, onRemove }) => { return ( <ul> {users.map(user => ( <li key={user.id}> {user.name} <button onClick={() => onRemove(user.id)}>X</button> </li> ))} </ul> ); }; // Immutable state update (the spread operator) const handleAddUser = (newUser) => { setUsers(prev => [...prev, newUser]); // Never: users.push(newUser) }; // Array methods are your bread and butter const activeUsers = users.filter(u => u.isActive); const userNames = users.map(u => u.name);
users.push(newUser)), React won't detect the change and your UI won't re-render. You'll stare at a stale screen for 45 minutes before realizing the bug.What is React? A Quick Intro
React is a library for building user interfaces through components. Unlike a framework, it handles only the view layer: rendering UI and reacting to state changes. React's core insight is declarative rendering — you describe what the UI should look like for a given state, and React figures out how to update the actual DOM efficiently. This eliminates the manual DOM traversal and mutation that plagues vanilla JavaScript apps. Instead of writing document.getElementById chains, you write functions that return a description of the UI. React then reconciles that description (a virtual representation) with the real browser DOM, applying only the minimal changes needed. This architecture makes applications predictable, testable, and easier to reason about. React is not a complete solution — you add routing, data fetching, and state management from the ecosystem as needed. But for UI logic alone, React dominates because it removed the hardest part of frontend development: keeping the DOM in sync with application state.
// io.thecodeforge — javascript tutorial // React is just a function that returns UI function Greeting({ name }) { return <h1>Hello, {name}!</h1>; } // State change triggers re-render automatically const [count, setCount] = React.useState(0); // React compares old and new virtual DOM, // then patches only the changed <p> element. <p>Count: {count}</p>
External Resources for Deeper Dives
Mastering React requires looking beyond the library itself. Start with the official React documentation — it's now excellent, with interactive examples and clear explanations of every hook and pattern. For understanding how React works under the hood, Dan Abramov's blog posts and talks (especially "The Algebraic Effects of React") explain fiber architecture and reconciliation. The "React, Visualized" course by Lydia Hallie offers animated explanations of core concepts like batching and suspense. For state management patterns, read the Redux docs even if you don't use Redux — they explain immutability, middleware, and normalized state better than most blog posts. Kent C. Dodds's blog covers practical patterns for testing, error boundaries, and component composition. Finally, the ECMAScript specification itself clarifies JavaScript quirks that bite React developers — things like closure traps in useEffect, stale closures in callbacks, and the event loop's impact on state updates. Avoid medium articles with code snippets that lack explanation. Prefer official sources, curated newsletters like React Status, and the React RFCs repository for future-looking insights.
// io.thecodeforge — javascript tutorial // Links to essential React resources: // react.dev — official docs // overreacted.io — Dan Abramov's blog // kentcdodds.com — practical patterns // react-status.com — weekly curated news // github.com/reactjs/rfcs — future proposals // Example: fetching the React RFC list fetch('https://api.github.com/repos/reactjs/rfcs') .then(r => r.json()) .then(console.log);
Summary of the Handbook
This handbook equips you with the core mental model behind React, not just syntax. We began by clarifying what React actually is: a declarative UI library for building component-based interfaces, not a framework. You then learned the essential JavaScript prerequisites—destructuring, arrow functions, spreads, and promises—because React relies on modern JS features. The deep dive into JSX explained why it exists (to marry markup and logic in one file) and how it compiles to React.createElement calls. We explored the Virtual DOM and its reconciliation algorithm, which lets React efficiently update only the parts of the real DOM that changed. Then came lifecycle management with useEffect, including cleanup functions and the difference between useEffect and useLayoutEffect. Finally, we covered performance optimization through memoization (React.memo, useMemo, useCallback) to avoid wasted re-renders. By now, you understand React's internal reasoning—why state changes trigger re-renders, how the diffing algorithm works, and when effects fire. This summary consolidates those pieces so you can see how the architecture fits together.
// io.thecodeforge — javascript tutorial const handbookSummary = { coreIdea: 'Declarative UI with components', essentialJS: ['destructuring', 'arrows', 'promises'], jsxCompilesTo: 'React.createElement', rendering: 'Virtual DOM diffing algo', lifecycle: 'useEffect & cleanup', perf: 'memoization (memo, useMemo)' }; console.log(handbookSummary);
Introduction to JSX
JSX is a syntax extension for JavaScript that looks like HTML but lives inside your .js files. It was created because React needs a way to describe UI structure declaratively—what the interface should look like based on current state—instead of imperatively mutating the DOM step by step. Without JSX, you'd write React.createElement('div', { className: 'app' }, 'Hello') for every element. JSX transforms that into <div className="app">Hello</div>, which is far more readable. Under the hood, JSX compiles to those exact createElement calls via Babel. This means every JSX tag becomes a function call that returns a lightweight JavaScript object (a virtual node). Crucially, JSX is not HTML—attributes like className replace class, htmlFor replaces for, and self-closing tags must end with />. Understanding this translation helps you debug when React complains about unexpected tokens or invalid attribute names.
// io.thecodeforge — javascript tutorial // JSX you write: const element = <h1 className="title">Hello, React!</h1>; // What it compiles to: const compiled = React.createElement( 'h1', { className: 'title' }, 'Hello, React!' ); console.log(compiled);
<img /> or <br /> will cause a build error. Unlike HTML, JSX is strict XML.Missing Dependency in useEffect Causes Stale Profile Data
- Always include every variable from the component scope that the effect reads or writes in the dependency array.
- Use the exhaustive-deps ESLint rule to catch missing dependencies at compile time.
- Cancel in-flight requests in the cleanup function to avoid stale state updates on unmounted components.
console.log('State before:', cartItems); setCartItems([...cartItems, newItem]);Add a useEffect dependency to log state changes: useEffect(() => { console.log('State updated:', cartItems); }, [cartItems]);console.log('Effect triggered at', Date.now());Log the dependencies inside the effect: console.log('Deps:', userId, fetchUrl);useEffect(() => { const controller = new AbortController(); fetch(url, { signal: controller.signal }); return () => controller.abort(); }, [url]);Add a boolean flag inside the effect: let cancelled = false; setData(result); if (!cancelled) setData(result); return () => { cancelled = true; };| Aspect | Vanilla JS (DOM manipulation) | React |
|---|---|---|
| UI update model | Imperative — you describe each step | Declarative — you describe the end state |
| DOM interaction | Direct (querySelector, innerHTML) | Virtual DOM diff — React touches real DOM minimally |
| State tracking | Manual — sync every affected element by hand | Automatic — React re-renders when state changes |
| Code reuse | Copy-paste or custom modules | Components — self-contained, composable, shareable |
| Data flow | Ad-hoc, any direction | One-way top-down via props — predictable and traceable |
| Learning curve | Low initially, chaotic at scale | Moderate upfront, scales cleanly to large apps |
| Ecosystem | Bare — you build or find everything | Rich — hooks, context, Next.js, React Query, etc. |
| File | Command / Code | Purpose |
|---|---|---|
| VanillaVsReact.js | let likeCount = 0; | Why React Exists |
| ProductCard.jsx | function ProductCard({ name, price, imageUrl, isOnSale }) { | Components and Props |
| OneWayFlow.jsx | function Parent() { | One-way Data Flow |
| ShoppingCart.jsx | const AVAILABLE_PRODUCTS = [ | State and the Virtual DOM |
| UserProfilePage.jsx | function UserProfilePage({ userId }) { | Fetching Real Data |
| LifecycleDemo.jsx | function LifecycleDemo({ id }) { | React Hooks Lifecycle |
| ExpensiveList.jsx | const ExpensiveRow = React.memo(({ item, onToggle }) => { | Performance Optimisation |
| WhatsReactNot.js | function UserProfile({ userId }) { | What React Actually Is (and Isn't) |
| JSXCompilation.js | const element = ( | JSX |
| RequiredJS.js | const UserList = ({ users, onRemove }) => { | The JavaScript You Actually Need Before React |
| HelloReact.js | function Greeting({ name }) { | What is React? A Quick Intro |
| ExternalLinks.js | fetch('https://api.github.com/repos/reactjs/rfcs') | External Resources for Deeper Dives |
| SummaryExample.js | const handbookSummary = { | Summary of the Handbook |
| JSXCompilation.js | const element = Hello, React!; | Introduction to JSX |
Key takeaways
Common mistakes to avoid
4 patternsMutating state directly instead of using the setter
cartItems.push(newItem) or user.name = 'Alice' doesn't trigger a re-render.[...cartItems, newItem]), Object.assign, or array methods like .filter() and .map() that return new arrays. React compares references, not deep equality.Forgetting the useEffect dependency array or leaving it wrong
userId, or it never re-runs when userId changes and you see perpetually wrong data.eslint-plugin-react-hooks with the exhaustive-deps rule catches this automatically — enable it.Treating state updates as synchronous
setCount(count + 1) twice in the same function increments by 1 instead of 2.setCount(prev => prev + 1) — this receives the latest state as an argument and composes correctly regardless of batching.Over-memoizing without profiling
Interview Questions on This Topic
What is the Virtual DOM and why does React use it instead of updating the real DOM directly? What are the performance trade-offs?
Explain the difference between props and state. When would you choose to lift state up rather than keep it local to a component?
What does the useEffect dependency array control, and what happens if you pass an empty array vs. no array at all? How do you prevent memory leaks from async operations inside useEffect?
[] means the effect runs once after the first render (mount) and runs cleanup on unmount. No array (undefined) runs the effect after every render — almost never what you want. To prevent memory leaks, always return a cleanup function that cancels async operations — use AbortController for fetch, clearTimeout for timers, and removeEventListener for subscriptions. Also use the exhaustive-deps ESLint rule to catch missing dependencies.How does React's reconciliation algorithm differ from a simple comparison of two trees? Why can it achieve O(n) complexity instead of O(n^3)?
Frequently Asked Questions
React is strictly a UI library — it handles only the view layer. It has no built-in router, no HTTP client, and no global state manager. That's intentional: you compose it with best-of-breed tools like React Router, React Query, or Zustand. Frameworks like Next.js are built on top of React and add those missing pieces.
Reach for React when your UI has multiple pieces of state that need to stay in sync, when the same UI component needs to appear in many places, or when your team is building something that will grow over months. For a simple static page or a single interactive widget, vanilla JS is perfectly fine and significantly lighter.
React doesn't watch your variables for changes the way some frameworks do. It only knows to re-render a component when you call the setter from useState. If you update a plain variable, React has no knowledge that anything changed, so it never re-renders and your UI stays frozen. The useState setter is React's notification system — it's how you tell React 'something changed, please recalculate the UI'.
React.memo is a higher-order component that wraps a component and prevents re-render if its props haven't changed (by shallow comparison). useMemo is a hook that caches the return value of a function — it's used for expensive computations inside the render body, not for preventing component re-renders itself. They work together: React.memo prevents the child from re-rendering, useMemo prevents expensive calculations from running on every render.
Use useCallback when you pass the function as a prop to a child component that is wrapped in React.memo. Without useCallback, a new function reference is created every render, causing the memoized child to re-render unnecessarily. Also use it in useEffect dependencies to avoid infinite loops when the function is used inside the effect. For most other cases, inline functions are fine — remember to profile before optimising.
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
That's React.js. Mark it forged?
10 min read · try the examples if you haven't