1. Overview & Questions (SQ3R: Survey & Question)

SQ3R Step 1: Survey the big picture and formulate key questions.

What is React?

React is an open-source JavaScript UI library developed by Meta (formerly Facebook) for building web and native user interfaces. Since its release in 2013, React has become one of the most widely adopted frontend libraries in the world. Its core philosophy is to break the UI into independent, reusable pieces called Components, each managing its own logic and appearance.

The current latest version is React 19.2, which introduces major features including Server Components, the React Compiler, and the use API. React is not a full-featured framework — it focuses on the view layer and works with complementary libraries for routing, state management, and more.

Core problems React solves:

  • Managing UI complexity: Decomposing complex interfaces into maintainable, composable units through componentization
  • Keeping state and views in sync: Through declarative programming, developers describe what the UI should look like, and React efficiently updates the DOM
  • Cross-platform development: The same component model works for web (React DOM) and mobile (React Native)

Key Questions

  • When should you use React? — Single-page applications, interaction-heavy web apps, products requiring cross-platform support
  • What advantages does React have over alternatives? — Mature ecosystem, massive community, flexible composition, sustained investment from Meta
  • What prerequisites are needed? — HTML, CSS, JavaScript (ES6+), basic DOM knowledge

Technology Landscape

React's architecture can be summarized as the collaboration of these core modules:

React Application
├── Describing the UI
│   ├── Components — The fundamental building blocks of UI
│   ├── JSX — Declarative markup syntax
│   ├── Props — Data passing between components
│   └── Conditional & List Rendering
├── Adding Interactivity
│   ├── Events — Event handling
│   ├── State — A component's memory
│   └── Render & Commit
├── Managing State
│   ├── Lifting State Up
│   ├── Reducer — Complex state logic
│   ├── Context — Deep data passing
│   └── Reducer + Context pattern
└── Escape Hatches
    ├── Refs — Referencing DOM nodes and values
    ├── Effects — Synchronizing with external systems
    └── Custom Hooks — Logic reuse

2. Explained in Plain Language (Feynman Technique)

Feynman Technique core idea: If you can't explain something in simple language, you don't truly understand it.

Core Concepts

Components

A component is a JavaScript function that returns the markup you want to display. A component can be as small as a button or as large as an entire page.

function MyButton() {
  return <button>I'm a button</button>;
}
 
// Using a component inside another component
export default function MyApp() {
  return (
    <div>
      <h1>Welcome to my app</h1>
      <MyButton />
    </div>
  );
}

Important: Component names must start with a capital letter (MyButton), while HTML tags use lowercase (button).

JSX

JSX is a syntax for writing HTML-like markup inside JavaScript. It is stricter than HTML: tags must be self-closing (like <br />), and a component cannot return multiple root tags — you need to wrap them in a <div> or an empty <>...</> fragment.

function AboutPage() {
  return (
    <>
      <h1>About</h1>
      <p>
        Hello there.
        <br />
        How do you do?
      </p>
    </>
  );
}

Use curly braces {} to embed JavaScript expressions inside JSX:

const user = { name: "Hedy Lamarr" };
return <h1>{user.name}</h1>;

Props

Props are data passed from a parent component to a child component — like function arguments.

function MyButton({ count, onClick }) {
  return <button onClick={onClick}>Clicked {count} times</button>;
}
 
// Parent component passes props
<MyButton count={count} onClick={handleClick} />;

State

State is a component's "memory." Use the useState Hook to declare a state variable.

import { useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>;
}

useState returns two things: the current state value (count) and a setter function (setCount). Every time you call setCount, React re-renders the component.

Event Handling

Event handlers are defined inside components and attached via onEventName attributes.

function MyButton() {
  function handleClick() {
    alert("You clicked me!");
  }
  return <button onClick={handleClick}>Click me</button>;
}

Notice: onClick={handleClick} has no parentheses at the end. You pass the function itself, not the result of calling it. React will call it when the user clicks.

Conditional Rendering

React has no special syntax for conditions. Use plain JavaScript:

// if...else
let content;
if (isLoggedIn) {
  content = <AdminPanel />;
} else {
  content = <LoginForm />;
}
return <div>{content}</div>;
 
// Ternary (works inside JSX)
<div>{isLoggedIn ? <AdminPanel /> : <LoginForm />}</div>
 
// Logical AND (when no else branch needed)
<div>{isLoggedIn && <AdminPanel />}</div>

Rendering Lists

Use JavaScript's map() function to render lists. Each list item needs a unique key:

const products = [
  { title: "Cabbage", id: 1 },
  { title: "Garlic", id: 2 },
  { title: "Apple", id: 3 },
];
 
const listItems = products.map((product) => <li key={product.id}>{product.title}</li>);
return <ul>{listItems}</ul>;

The key helps React identify which items have changed (inserted, deleted, or reordered).

Analogies

  • Components = LEGO bricks: Each brick has its own shape and function. You can freely combine them into larger creations. A button is one brick; a page is many bricks assembled together.
  • Props = Delivery packages: A parent component "ships packages" to child components via props. The child receives and uses the contents but cannot modify the package itself.
  • State = A component's notebook: Each component has its own little notebook to record information that changes. When the notebook content changes, the UI updates automatically.
  • JSX = HTML's supercharged sibling: It is like writing HTML where you can embed JavaScript expressions directly using curly braces {}.
  • Hooks = Plug-in interfaces: Hooks are like USB ports that let you "plug in" various React capabilities (state management, side effects, references) into your components.
  • Context = Indoor broadcast: If Props are a delivery person passing packages floor by floor, Context is like a room broadcast — a top-level component broadcasts a message, and any component at any nesting level can hear it.

Common Misconceptions

  1. "React is an MVC framework" — No. React is just the V (view layer). Routing, data fetching, and other concerns require additional libraries.
  2. "JSX is just HTML" — No. JSX is a JavaScript syntax extension that is stricter (tags must be closed, use className instead of class).
  3. "State changes immediately update the DOM" — No. React batches state updates and applies them at the right time.
  4. "Every render rebuilds the entire DOM" — No. React uses the Virtual DOM diff algorithm to update only what actually changed.
  5. "Hooks can be called inside if/for" — No. Hooks must be called at the top level of a component, never inside conditions, loops, or nested functions.
  6. "useEffect is just like Vue's watch" — Not quite. useEffect runs after the render is committed to the screen and is meant for synchronizing with external systems, not simply watching data changes.
  7. "Context can replace all state management" — No. Context is great for "long-distance" data passing, but overuse causes unnecessary re-renders.

3. Cone of Depth (Simon Learning Method)

Simon Learning Method: Focused effort, goal-oriented, cone-shaped depth — start from the core and expand outward.

Layer 1: Core Fundamentals

1. useState — Managing State

useState is the most fundamental Hook, giving components "memory."

import { useState } from "react";
 
function Form() {
  const [firstName, setFirstName] = useState("Mary");
  const [lastName, setLastName] = useState("Poppins");
 
  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }
 
  return (
    <>
      <label>
        First name:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <p>
        <b>
          Good morning, {firstName} {lastName}.
        </b>
      </p>
    </>
  );
}

Key points:

  • useState(initialValue) returns [state, setState]
  • Calling setState triggers a re-render
  • State updates may be asynchronous; use functional updates for the latest value: setCount(c => c + 1)

2. useEffect — Side Effects

useEffect synchronizes components with external systems after rendering.

import { useEffect, useRef } from "react";
 
function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);
 
  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }, [isPlaying]); // Only re-runs when isPlaying changes
 
  return <video ref={ref} src={src} loop playsInline />;
}

Three steps to write an Effect:

  1. Declare the Effect (useEffect(() => { ... }))
  2. Specify dependencies ([dep1, dep2])
  3. Add cleanup if needed (return () => { ... })

3. Props and Lifting State Up

When multiple components need to share state, lift it to their closest common parent.

import { useState } from "react";
 
export default function MyApp() {
  const [count, setCount] = useState(0);
 
  function handleClick() {
    setCount(count + 1);
  }
 
  return (
    <div>
      <h1>Counters that update together</h1>
      <MyButton count={count} onClick={handleClick} />
      <MyButton count={count} onClick={handleClick} />
    </div>
  );
}
 
function MyButton({ count, onClick }) {
  return <button onClick={onClick}>Clicked {count} times</button>;
}

4. useRef — Referencing Values

useRef holds values that don't trigger re-renders, or references DOM elements.

import { useRef } from "react";
 
function MyInput({ value, onChange }) {
  const ref = useRef(null);
 
  function focusInput() {
    ref.current.focus();
  }
 
  return <input ref={ref} value={value} onChange={onChange} />;
}

Difference from useState: Modifying ref.current does not trigger a re-render.

5. Event Handling

function Form() {
  function handleSubmit(e) {
    e.preventDefault();
    console.log("Form submitted");
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}

6. Conditional & List Rendering

function ProductList({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;
 
  products.forEach((product) => {
    // Filtering logic
    if (product.name.toLowerCase().indexOf(filterText.toLowerCase()) === -1) {
      return;
    }
    if (inStockOnly && !product.stocked) {
      return;
    }
    // Conditionally render category header
    if (product.category !== lastCategory) {
      rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
    }
    rows.push(<ProductRow product={product} key={product.name} />);
    lastCategory = product.category;
  });
 
  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

7. Updating Objects and Arrays

State in React is immutable. You must create new objects/arrays to trigger updates.

import { useState } from "react";
 
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState("");
 
  function addTodo() {
    // Create a new array, don't mutate the original
    setTodos([...todos, text]);
    setText("");
  }
 
  function removeTodo(index) {
    setTodos(todos.filter((_, i) => i !== index));
  }
 
  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map((todo, i) => (
          <li key={i}>
            {todo}
            <button onClick={() => removeTodo(i)}>Delete</button>
          </li>
        ))}
      </ul>
    </>
  );
}

Layer 2: Advanced Patterns

1. useContext — Deep Data Passing

Context solves the "prop drilling" problem — passing data through many layers of components without explicit props at every level.

// 1. Create Context
import { createContext, useContext } from "react";
 
const LevelContext = createContext(1);
 
// 2. Use in a child component
function Heading({ children }) {
  const level = useContext(LevelContext);
  const Tag = `h${level}`;
  return <Tag>{children}</Tag>;
}
 
// 3. Provide from a parent component
function Section({ level, children }) {
  return <LevelContext value={level}>{children}</LevelContext>;
}
 
// Usage: Section automatically passes level to all nested Headings
function Page() {
  return (
    <Section level={1}>
      <Heading>Title</Heading>
      <Section level={2}>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
      </Section>
    </Section>
  );
}

Typical use cases: Theme switching, current user, i18n language settings, routing information.

2. useReducer — Complex State Logic

When state logic gets complex, useReducer offers more clarity than useState.

import { useReducer } from "react";
 
function reducer(state, action) {
  switch (action.type) {
    case "incremented":
      return { count: state.count + 1 };
    case "decremented":
      return { count: state.count - 1 };
    default:
      throw new Error("Unknown action: " + action.type);
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <button onClick={() => dispatch({ type: "decremented" })}>-</button>
      {state.count}
      <button onClick={() => dispatch({ type: "incremented" })}>+</button>
    </>
  );
}

3. useCallback & useMemo — Performance Optimization

import { useState, useCallback, useMemo } from "react";
 
function ProductPage({ products }) {
  const [filterText, setFilterText] = useState("");
 
  // useMemo: cache computed results
  const filteredProducts = useMemo(() => {
    return products.filter((p) => p.name.toLowerCase().includes(filterText.toLowerCase()));
  }, [products, filterText]);
 
  // useCallback: cache function references
  const handleFilterChange = useCallback((e) => {
    setFilterText(e.target.value);
  }, []);
 
  return (
    <>
      <input value={filterText} onChange={handleFilterChange} />
      <ProductList products={filteredProducts} />
    </>
  );
}

Note: Don't overuse these. React 19 with the React Compiler can automatically handle most optimization cases.

4. Custom Hooks

Custom Hooks let you share stateful logic between components.

// useOnlineStatus.js
import { useState, useEffect } from "react";
 
export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
 
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
 
    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);
 
    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);
 
  return isOnline;
}
 
// Using in multiple components
function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? "Online" : "Disconnected"}</h1>;
}
 
function SaveButton() {
  const isOnline = useOnlineStatus();
  return <button disabled={!isOnline}>{isOnline ? "Save progress" : "Reconnecting..."}</button>;
}

Naming convention: Custom Hooks must start with use (e.g., useOnlineStatus) so React's lint rules can properly check their usage.

5. Correct Effect Patterns

Effects are for synchronizing with external systems, not a general-purpose tool for reacting to state changes.

// Correct: Connecting to a chat server
function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    return () => connection.disconnect(); // Cleanup function
  }, [roomId]); // Only reconnect when roomId changes
}
 
// Wrong: You don't need an Effect to compute state from other state
// Bad:
useEffect(() => {
  setFullName(firstName + " " + lastName);
}, [firstName, lastName]);
 
// Good: Compute during rendering
const fullName = firstName + " " + lastName;

Key cleanup patterns for Effects:

  • Event subscriptions -> addEventListener / removeEventListener
  • Data fetching -> Set an ignore flag to discard stale responses
  • Animations -> Reset to initial values
  • Timers -> setTimeout / clearTimeout

Layer 3: Deep Dive

1. Render Mechanism: Render & Commit

React updates the UI in three steps:

  1. Trigger: A setState call or the initial render
  2. Render: React calls your component function to compute new JSX (Virtual DOM)
  3. Commit: React applies the changes to the real DOM

State is a snapshot: Each render has its own fixed state values. Event handlers and Effects capture state values from that specific render.

function Counter() {
  const [count, setCount] = useState(0);
 
  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count); // Shows the count at click time, not current
    }, 3000);
  }
 
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

2. Batched State Updates & Queuing

React batches state updates. Multiple setState calls in the same event handler trigger only one re-render.

function handleClick() {
  setCount(count + 1); // count is still the old value here
  setCount(count + 1); // Still based on old value, only +1
}
 
// Correct: Use functional updates
function handleClick() {
  setCount((c) => c + 1); // c is the latest pending value
  setCount((c) => c + 1); // Based on previous update, result is +2
}

3. React Compiler

React 19 introduces the React Compiler, a build-time optimization tool that automatically memoizes components and values:

  • Automatically identifies where computations should be cached
  • Reduces the need for manual useMemo and useCallback
  • Can be adopted incrementally, compatible with existing code
  • Integrated via @babel/plugin-transform-react-compiler

4. The use API

The use API, new in React 19, reads Promises and Context during rendering:

import { use, Suspense } from "react";
 
function Message({ messagePromise }) {
  const messageContent = use(messagePromise);
  return <p>Here is the message: {messageContent}</p>;
}
 
export function MessageContainer({ messagePromise }) {
  return (
    <Suspense fallback={<p>Downloading message...</p>}>
      <Message messagePromise={messagePromise} />
    </Suspense>
  );
}

5. Thinking in React — The React Mindset

Five steps to building a React application:

  1. Break the UI into a component hierarchy: Divide based on data model and design mockups
  2. Build a static version: Create the UI with props only, no interactivity yet
  3. Find the minimal state: Follow the DRY principle — identify the smallest set of changing data
  4. Identify where state lives: Place state in the closest common parent of the components that use it
  5. Add inverse data flow: Child components update parent state via callback functions

6. Reducer + Context Pattern

This is the pattern for managing complex application state in React:

import { createContext, useContext, useReducer } from "react";
 
// 1. Create Contexts
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
 
// 2. Define Reducer
function tasksReducer(tasks, action) {
  switch (action.type) {
    case "added":
      return [...tasks, { id: action.id, text: action.text }];
    case "deleted":
      return tasks.filter((t) => t.id !== action.id);
    default:
      throw new Error("Unknown action: " + action.type);
  }
}
 
// 3. Provider component
export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>{children}</TasksDispatchContext>
    </TasksContext>
  );
}
 
// 4. Custom Hooks for child components
export function useTasks() {
  return useContext(TasksContext);
}
export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

7. Component Purity & Rules of React

React has three core rules:

  1. Components and Hooks must be pure: Same inputs produce same outputs, no side effects during rendering
  2. React is responsible for calling components and Hooks: Do not call component functions manually
  3. Rules of Hooks: Only call Hooks at the top level of components or other Hooks — never inside conditions or loops

4. Key Notes (Cornell Note-Taking Method)

Cornell Method: Divide notes into cues, notes, and summary for efficient review and retrieval.

Key Concepts Quick Reference

Cue / KeywordDetailed Notes
ComponentA JavaScript function returning JSX; name must start with a capital letter
JSXJavaScript markup syntax extension; embed expressions with curly braces
PropsRead-only data passed from parent to child; like function arguments
StateMutable data inside a component; declared with useState
HooksFunctions starting with use; can only be called at the top level of components
Virtual DOMReact's lightweight DOM representation used for diff calculations
Re-renderComponent function re-executes when state/props change
Render & CommitTrigger -> Compute JSX -> Apply to real DOM
State SnapshotEach render's state values are fixed and immutable
Lifting State UpMoving shared state to the closest common parent component
EffectRuns after rendering; used to synchronize with external systems
CleanupFunction returned from an Effect; runs before next Effect and on unmount
ContextParent provides data to any descendant regardless of depth
ReducerManages complex state logic via dispatch(action)
Custom HookUser-defined function starting with use for reusing stateful logic
KeyUnique identifier in list rendering; helps React track elements
React CompilerBuild-time automatic memoization optimization tool
use APINew in React 19; reads Promises/Context during rendering

Core API Reference

API / HookPurposeExample
useStateDeclare a state variableconst [val, setVal] = useState(0)
useEffectRun side effects after renderuseEffect(() => { ... }, [dep])
useRefReference DOM or store non-reactive valuesconst ref = useRef(null)
useContextRead a Context valueconst value = useContext(MyContext)
useReducerComplex state managementconst [state, dispatch] = useReducer(reducer, init)
useCallbackCache a function referenceconst fn = useCallback(() => {}, [deps])
useMemoCache a computed valueconst val = useMemo(() => compute(a), [a])
createContextCreate a Context objectconst Ctx = createContext(defaultValue)
useRead a Promise during renderingconst data = use(promise)
useSyncExternalStoreSubscribe to external data storesconst val = useSyncExternalStore(subscribe, getSnapshot)
useEffectEventEffect event handler (excluded from deps)const onMsg = useEffectEvent(handler)

Section Summary

React's core philosophy is componentization and declarative UI. Components receive data through Props, manage internal state with State, and pass data deeply via Context. Hooks (useState, useEffect, useRef, etc.) are the sole entry points for components to access React's capabilities. Effects synchronize with external systems, while Custom Hooks enable reuse of stateful logic. React 19's Compiler and use API represent the framework's direction — less boilerplate, more automated optimization.

5. Review & Practice (SQ3R: Recite & Review)

SQ3R final two steps: Recite key points and solidify understanding through practice.

Key Takeaways

  1. React components are JavaScript functions that return JSX; component names must start with a capital letter
  2. Use curly braces {} to embed JavaScript expressions inside JSX
  3. Props are a read-only data flow from parent to child; State is mutable data internal to a component
  4. useState returns [value, setter]; calling the setter triggers a re-render
  5. Lifting State Up is the standard pattern for sharing state between components
  6. List rendering requires a unique key prop on each item
  7. useEffect synchronizes with external systems; always specify a dependency array
  8. Effects need cleanup functions to prevent resource leaks (subscriptions, connections, timers)
  9. Context solves prop drilling but should not replace all state passing
  10. Custom Hooks start with use and share stateful logic between components
  11. useReducer suits complex state logic; combined with Context it forms a global state management solution
  12. React 19's Compiler handles memoization automatically, reducing manual optimization
  13. Rendering process: Trigger -> Render (compute JSX) -> Commit (update DOM)
  14. State is a fixed snapshot per render; closures capture values from that specific render
  15. Three Rules of React: component purity, React controls invocation, Hook rules

Hands-On Exercises

  1. Static Personal Card Page: Create a component with an avatar, name, and bio. Practice component creation, Props passing, and JSX basics.

  2. Searchable Product List: Follow the "Thinking in React" tutorial to build a product table with search filtering and stock-only toggle. Practice lifting state, conditional rendering, and list rendering.

  3. Chat Room Application: Build a chat component that connects to a mock server. Practice useEffect declaration, dependency arrays, and cleanup functions.

  4. Theme Switching System: Use Context to implement light/dark theme toggling where all nested components automatically adapt. Practice createContext, useContext, and Providers.

  5. Task Management System: Build a Todo app with the Reducer + Context pattern, supporting add, delete, and mark-as-complete. Practice useReducer, Custom Hooks, and Context composition.

Common Pitfalls

  • Forgetting the dependency array: useEffect without a dependency array runs after every render, potentially causing infinite loops

    // Wrong: infinite loop
    useEffect(() => {
      setCount(count + 1);
    });
     
    // Correct: explicit dependencies
    useEffect(() => {
      setCount((c) => c + 1);
    }, []);
  • Mutating state objects directly: You must create new objects/arrays

    // Wrong
    state.items.push(newItem);
    setState(state);
     
    // Correct
    setState({ ...state, items: [...state.items, newItem] });
  • Using Effects for the wrong things: Actions like purchasing products or submitting forms should be in event handlers, not Effects

  • Using array index as key: When lists can be reordered, inserted, or deleted, using indices as keys causes rendering bugs. Use stable unique IDs instead

  • Overusing Context: When a Context value changes, all consumers re-render. For frequently changing data, consider a state management library or scoping state more narrowly

Further Reading