Knowledge/React/Introduction
28 Chapters
18+ Baseline
Living Document
Free License
Engineering Publication · Syed Omar Ibn Feroj← Back to portfolio
Engineering Notes · Open Knowledge Repository

React — Engineering
Notes for Components

A working reference for React as it's written today — UI as a function of state, hooks without the footguns, effects that don't lie, and the composition and performance patterns that hold up past a few hundred components.

28
Chapters
18+
Baseline
Living
Document
Free
License
Ch. 01
Mental Model
UI is a function of state. Everything else follows from that.
1.1 — Render, don't mutate

You describe what the UI should look like for a given state; React figures out the DOM changes. You never touch the DOM directly. A re-render is React calling your component function again — it's cheap and expected, not something to avoid.

The whole framework reduces to: view = f(state). When something's wrong on screen, the bug is almost always in the state or the function — not in "React didn't update". Trust the model before reaching for a ref or a manual DOM poke.
Ch. 02
JSX
Syntactic sugar for function calls — with a few hard rules.
2.1 — Expressions, one root, className
JSX
function Card({ title, n }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      {n > 0 && <span>{n} items</span>}
    </div>
  );
}
{0 && <X/>} renders 0, not nothing — && returns the falsy left value and React renders 0/NaN. Use {count > 0 && ...} or a ternary, never a bare number on the left of &&.
Ch. 03
Components
Functions that return JSX. Capitalised, pure during render.
3.1 — Pure render, no side effects
JSX
function Avatar({ user }) {
  // ❌ side effect during render
  // localStorage.setItem("seen", user.id)
  return <img src={user.avatar} alt={user.name} />;
}
A component must be pure during render: same props → same JSX, no mutations, no I/O, no DOM. Side effects belong in event handlers or useEffect. Strict Mode double-invokes render in dev specifically to surface impure components.
Ch. 04
Props
Read-only inputs flowing down.
4.1 — Props are immutable
JSX
<Button label="Save" onClick={save} disabled />

function Button({ label, onClick, disabled = false }) {
  return <button onClick={onClick} disabled={disabled}>{label}</button>;
}
Never mutate props (or their nested objects). Data flows one way: parent owns it, child reads it, child asks the parent to change it via a callback. Mutating a prop "works" until React reuses the object and the bug becomes nondeterministic.
Ch. 05
State
useState — the value that triggers a re-render.
5.1 — Set with a value or an updater
JSX
const [count, setCount] = useState(0);
setCount(count + 1);              // from a snapshot
setCount(c => c + 1);            // from latest — use in loops/async
State is a snapshot for that render — count doesn't change mid-function. Calling setCount(count+1) three times adds 1, not 3. Use the updater form setCount(c => c+1) whenever the next value depends on the previous.
Ch. 06
Events
Synthetic events, handlers, and not calling on render.
6.1 — Pass the function, don't call it
JSX
<button onClick={handleClick}>ok</button>        // ✅ reference
<button onClick={() => del(id)}>del</button>   // ✅ with arg
<button onClick={del(id)}>del</button>        // ❌ runs on every render
onClick={del(id)} calls del during render and uses its return value as the handler — an infinite-loop / fires-immediately bug. Wrap in an arrow when you need arguments.
Ch. 07
Conditional Render
Ternary, &&, and early return.
7.1 — The three forms
JSX
{isLoading ? <Spinner /> : <List data={data} />}
{error && <Error msg={error} />}

if (!user) return <Login />;   // guard clause, cleanest
Ch. 08
Lists & Keys
The key prop decides identity across renders.
8.1 — Stable, unique keys
JSX
{items.map(it => <Row key={it.id} {...it} />)}
// key = it.id  ✅   key = index  ❌ (for mutable lists)
Using the array index as key for a list that can reorder/insert/delete makes React reuse the wrong DOM/state — inputs keep the previous row's value, animations jump. Use a stable id from the data.
Ch. 09
Forms
Controlled inputs — state is the source of truth.
9.1 — value + onChange
JSX
const [name, setName] = useState("");
<input value={name} onChange={e => setName(e.target.value)} />
Setting value without an onChange makes a read-only input that ignores typing (React warns about this). Either control it (value + onChange) or make it uncontrolled (defaultValue + ref) — never half of each.
Ch. 10
Lifting State
Shared state lives in the closest common parent.
10.1 — Owner down, events up

When two siblings need the same data, move the state to their nearest common ancestor and pass it down as props plus a setter callback. The parent owns the truth; children render it and request changes.

If "lifting" pushes state many levels up and prop-drilling gets noisy, that's the signal to introduce Context (Ch. 16) or a store (Ch. 26) — not to duplicate the state in two places.
Ch. 11
Composition
children and slots beat configuration props.
11.1 — Pass JSX, not a hundred props
JSX
function Card({ children, footer }) {
  return <div className="card">{children}<footer>{footer}</footer></div>;
}
<Card footer={<Buttons />}><Body /></Card>
Prefer composition (children, render props, slot props) over a growing list of boolean/config props. It keeps components open for extension without each one knowing every use case ("inversion of control").
Ch. 12
Rules of Hooks
Two rules. Breaking either corrupts state silently.
12.1 — Top level, React functions only
JSX
// ❌ conditional hook — order changes between renders
if (open) { const [x] = useState(); }

// ✅ always called, in the same order
const [x, setX] = useState();
if (open) { /* use x */ }
Hooks are matched by call order. Calling one inside a condition, loop, or after an early return shifts the order between renders → React associates state with the wrong hook. Always call hooks unconditionally at the top of the component.
Ch. 13
useEffect
Synchronise with the outside world — and clean up.
13.1 — Effect + cleanup
JSX
useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);   // cleanup runs before next effect / unmount
}, []);
Every subscription/timer/listener an effect creates must be torn down in its cleanup return, or you leak and double-fire (Strict Mode mounts twice in dev to catch exactly this). An effect without cleanup that should have one is the most common React bug.
Ch. 14
Effect Dependencies
The array is a correctness contract, not an optimisation.
14.1 — List every reactive value used
JSX
useEffect(() => {
  load(userId);
}, [userId]);   // re-run when userId changes
Don't lie to the dependency array to "stop it running". Omitting a dep means the effect closes over a stale value. If a dep changes too often, fix the cause (memoise the function, move it inside, or use a ref) — never silence the lint rule.
Ch. 15
useRef
A mutable box that survives renders without causing them.
15.1 — Values and DOM nodes
JSX
const inputRef = useRef(null);
const renders = useRef(0); renders.current++;
<input ref={inputRef} />   // inputRef.current = the DOM node
Changing ref.current does not re-render. Use a ref for things the UI doesn't derive from (timer ids, the latest value in a closure, a DOM node). If rendering should reflect it, it's state, not a ref.
Ch. 16
useContext
Skip prop-drilling for cross-cutting values.
16.1 — Provider + consumer
JSX
const Theme = createContext("light");
<Theme.Provider value={theme}><App /></Theme.Provider>
const theme = useContext(Theme);   // in any descendant
Every consumer re-renders when the Provider value changes identity. Passing a fresh object literal as value each render re-renders the whole subtree — memoise the value, and split contexts by update frequency.
Ch. 17
useReducer
When state transitions get too complex for useState.
17.1 — Action in, next state out
JSX
function reducer(s, a) {
  switch (a.type) {
    case "inc": return { ...s, n: s.n + 1 };
    default: return s;
  }
}
const [state, dispatch] = useReducer(reducer, { n: 0 });
Reach for useReducer when several values change together, the next state depends on the previous, or you want the update logic testable in isolation. The reducer must be pure and return new objects (no mutation).
Ch. 18
useMemo / useCallback
Caching — measure before you reach for it.
18.1 — Memoise value vs function
JSX
const sorted = useMemo(() => heavy(items), [items]);
const onSel = useCallback(id => pick(id), []);
These are optimisations with their own cost (a deps comparison every render). Adding them everywhere makes code slower and noisier. Use them when a memoised React.memo child needs a stable prop, or a genuinely expensive computation repeats — confirmed by the profiler.
Ch. 19
Custom Hooks
Extract stateful logic, not markup.
19.1 — A hook is a function that uses hooks
JSX
function useDebounced(value, ms) {
  const [v, setV] = useState(value);
  useEffect(() => {
    const t = setTimeout(() => setV(value), ms);
    return () => clearTimeout(t);
  }, [value, ms]);
  return v;
}
Custom hooks share logic, not state — each call gets its own independent state. Name them useX so the linter enforces the rules of hooks inside them.
Ch. 20
Refs & the DOM
The escape hatch — used sparingly.
20.1 — Focus, measure, integrate
JSX
useEffect(() => { inputRef.current?.focus(); }, []);
// forwardRef to expose a child's node to a parent
const Input = forwardRef((p, ref) => <input ref={ref} {...p} />);
Manipulate the DOM via refs only for things React doesn't model: focus, scroll position, text selection, measuring, or wiring a non-React library. Don't use a ref to read/write what should be state.
Ch. 21
Error Boundaries
Contain render-time crashes.
21.1 — Still a class (or react-error-boundary)
JSX
class Boundary extends React.Component {
  state = { err: null };
  static getDerivedStateFromError(e) { return { err: e }; }
  render() { return this.state.err ? <Fallback/> : this.props.children; }
}
Boundaries catch errors in rendering only — not in event handlers, async code, or the boundary itself. Handle those with try/catch and state. Wrap routes/widgets so one crash doesn't blank the whole app.
Ch. 22
Suspense & lazy
Code-split and stream with a fallback.
22.1 — lazy + Suspense
JSX
const Chart = lazy(() => import("./Chart"));
<Suspense fallback={<Spinner />}><Chart /></Suspense>
Lazy-load route-level and heavy below-the-fold components — it cuts the initial bundle. Keep one Suspense per meaningful loading region, not one giant boundary that blanks the page.
Ch. 23
Portals
Render outside the parent DOM, keep the React tree.
23.1 — Modals, tooltips, toasts
JSX
createPortal(<Modal />, document.body)
A portal escapes the DOM hierarchy (so it isn't clipped by overflow:hidden) but stays in the React tree — context still works and events still bubble through the React parent, which can surprise click-outside logic. Account for that in dismiss handlers.
Ch. 24
Performance
Find the real re-render before you memoise.
24.1 — Profile, then fix the cause
  • Profile with the React DevTools Profiler — find what re-renders and why
  • Stabilise props (memoised callbacks/values) so React.memo can skip
  • Lift expensive subtrees out, or split state so unrelated parts don't re-render
  • Virtualise long lists; lazy-load heavy routes
  • Don't create new objects/arrays/functions in render unless needed
Re-renders are usually fine — React is fast. Reach for memoisation only when the profiler shows a specific component re-rendering expensively. Premature memo/useCallback everywhere is slower and harder to read.
Ch. 25
Data Fetching
Effects fetch — but a library does it better.
25.1 — The raw effect, and why to graduate
JSX
useEffect(() => {
  const c = new AbortController();
  fetch(url, { signal: c.signal }).then(...);
  return () => c.abort();   // cancel on unmount / url change
}, [url]);
Hand-rolled effect fetching needs you to handle race conditions, cancellation, caching, retries and loading/error states yourself — easy to get subtly wrong. For real apps use TanStack Query / SWR / the framework's data layer; they solve all of that.
Ch. 26
State Management
Choose by scope, not by hype.
26.1 — The decision
ScopeUse
One componentuseState / useReducer
A subtree, low-frequencyContext
App-wide client stateZustand / Redux Toolkit / Jotai
Server dataTanStack Query / SWR (not a global store)
URL-derivedthe router
The most common architecture mistake is dumping server data into a global client store and manually syncing it. Server cache is its own concern — let a query library own it; keep client stores for genuine UI/client state.
Ch. 27
Common Pitfalls
The recurring React bugs.
27.1 — The list
  • Index as key on a mutable list
  • Stale closure from a lying dependency array
  • Missing effect cleanup → leaks / double subscriptions
  • Mutating state/props instead of replacing
  • {count && <X/>} rendering 0
  • Calling a handler instead of passing it
  • New object as Context value every render
  • Conditional/looped hook calls
  • Server data in a global client store
Ch. 28
Modern Guidelines
The defaults that keep a React app sane.
28.1 — The short list
  • Function components + hooks; keep render pure
  • Derive, don't duplicate — compute from existing state in render
  • Effects only for synchronising with the outside world; always clean up
  • Never lie to the dependency array
  • Stable keys from data, not array index
  • Compose with children before adding config props
  • A query library for server data; small store for client state
  • Profile before memoising; lazy-load heavy routes
REF
Hooks Cheatsheet
What each hook is for, in one line.
The hooks
HookUse it for
useStatelocal state that drives rendering
useReducercomplex/related state transitions
useEffectsynchronise with the outside world + cleanup
useRefmutable value / DOM node, no re-render
useContextread a provided value without drilling
useMemocache an expensive computed value
useCallbackstable function identity for memo'd children
useIdstable unique id for a11y attributes