Frameworks reference

React Reference

Components, state, effects, hooks and routing, with the reasoning behind each rule rather than only the syntax.

The mental model

React asks you to give up one habit: reaching into the page and changing it. Instead you describe what the page should look like for a given set of data, and React works out which parts of the document to touch. Almost every rule below follows from that one trade.

What React actually does

You write a function that returns a description of some markup. React calls it, compares the result with what is currently on screen, and changes only what differs.

// Not this: find the element and mutate it
document.querySelector("#count").textContent = 5;

// This: describe what it should say, given the data
function Counter() {
  const [count, setCount] = useState(0);
  return <p id="count">{count}</p>;
}
  • You never write the update. You change the data and let React find the difference.
  • That description is a plain object, not real DOM. React builds the real thing.
  • This is why direct DOM edits inside a component get silently undone on the next render.

Render

One call of your component function. It happens on first display and again every time that component's state or props change.

function Greeting({ name }) {
  console.log("rendered");   // fires on every render, not once
  return <h1>Hello, {name}</h1>;
}
  • A render is just a function call. It should not change anything outside itself.
  • Renders are frequent and cheap. Do not try to prevent them until you have measured a problem.
  • Work that touches the world outside React belongs in an effect, not in the render.

Declarative versus imperative

Imperative code lists the steps. Declarative code states the outcome. React is the second, which is why the code reads like the finished screen.

// Imperative: the steps to reach the state
if (loading) { spinner.style.display = "block"; list.style.display = "none"; }
else { spinner.style.display = "none"; list.style.display = "block"; }

// Declarative: what is true when
return loading ? <Spinner /> : <List items={items} />;
  • The declarative version cannot drift out of sync, because there is only one description.
  • The imperative version has two places to update and will eventually disagree with itself.

Components

A component is a JavaScript function that returns markup. That is the whole idea. Everything else is a rule about how those functions may behave.

Defining a component

A function whose name starts with a capital letter and which returns JSX.

function CourseCard() {
  return (
    <article className="card">
      <h2>Introduction to HTML &amp; CSS</h2>
      <p>Fourteen modules, free forever.</p>
    </article>
  );
}

export default CourseCard;
  • The capital letter is required. React reads <courseCard /> as an HTML tag and <CourseCard /> as your component.
  • One component per file is the common convention, exported as the default.
  • Arrow functions work identically: const CourseCard = () => { ... }

Using a component

Write it as a tag. Self-closing when it has no children.

function Curriculum() {
  return (
    <main>
      <CourseCard />
      <CourseCard />
    </main>
  );
}
  • The same component can appear any number of times. Each copy has its own state.
  • Import it first if it lives in another file.

Composition

Components take other components as children, which is how a page is assembled without any component knowing the whole.

function Panel({ title, children }) {
  return (
    <section className="panel">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

// used as
<Panel title="Your progress">
  <ProgressBar value={0.4} />
  <p>Six of fourteen modules complete.</p>
</Panel>
  • children is whatever you put between the opening and closing tags.
  • This is how you write a wrapper without hard-coding what goes inside it.
  • Prefer composition to a component that takes twelve props deciding what to show.

Where to split a component

Split when a piece has its own reason to change, its own state, or is repeated. Not simply because a file feels long.

  • A component doing two unrelated jobs is the strongest signal to split.
  • Repeating the same markup three times is the second strongest.
  • Splitting too early produces a maze of files that each do almost nothing.

JSX

JSX is markup written inside JavaScript. It is not HTML and not a string. A build step turns each tag into a function call, which is why the rules below exist.

Embedding values

Curly braces switch from markup back into JavaScript. Anything that produces a value is allowed.

const name = "Amara";
const modules = 14;

return (
  <p>
    {name} has {modules} modules left, about {modules * 40} minutes.
  </p>
);
  • An expression, not a statement. {if (x) ...} is invalid; use a ternary or a variable.
  • Strings, numbers and arrays render. Objects do not and will throw.
  • null, undefined, false and true all render as nothing, which is useful deliberately.

Attributes

Named for the DOM property rather than the HTML attribute, because you are setting properties on an object.

<label htmlFor="email">Email</label>
<input id="email" className="field" readOnly maxLength={40} />

<img src={logoUrl} alt="Sankofa Code" />
<div style={{ marginTop: "1rem", color: "var(--ink-2)" }} />
  • class becomes className and for becomes htmlFor, because both are reserved words in JavaScript.
  • Most others become camelCase: tabindex is tabIndex, onclick is onClick.
  • style takes an object, not a string, and the inner braces are the object literal.
  • Quotes for literal text, braces for a value: src="logo.png" or src={logoUrl}.

One root element

A component returns exactly one element. Wrap siblings, or use a fragment when a wrapper would be wrong.

// Fails: two roots
return <h1>Title</h1><p>Body</p>;

// Fragment: groups without adding a DOM node
return (
  <>
    <h1>Title</h1>
    <p>Body</p>
  </>
);
  • A function returns one value, and JSX is a value. That is the entire reason.
  • Use a fragment inside tables and lists, where a stray div would be invalid HTML.
  • The long form is <React.Fragment key={id}> when you need a key on it.

Closing every tag

JSX has no void elements. Tags that need no closing tag in HTML must close themselves here.

<br />
<hr />
<img src={url} alt="" />
<input type="text" />
  • Forgetting the slash is one of the most common first-week errors.

Comments

JavaScript comments inside braces.

return (
  <div>
    {/* This is a comment in JSX */}
    <p>Visible</p>
  </div>
);
  • An HTML comment would render as literal text.

Props

Props are the arguments a component receives. They flow one way, from parent to child, and the child may not change them.

Passing and receiving

Attributes on the tag arrive as a single object. Destructure it in the parameter list.

function CourseCard({ title, modules, free }) {
  return (
    <article>
      <h2>{title}</h2>
      <p>{modules} modules{free && ", free"}</p>
    </article>
  );
}

<CourseCard title="React.js" modules={17} free />
  • Text goes in quotes. Everything else goes in braces, including numbers.
  • A bare attribute means true, so `free` and `free={true}` are the same.
  • Without destructuring you receive one object: function CourseCard(props) { props.title }

Default values

Set them in the destructuring, so a missing prop has a sensible value.

function Avatar({ src, size = 48, alt = "" }) {
  return <img src={src} width={size} height={size} alt={alt} />;
}
  • The default applies when the prop is undefined, not when it is null or 0.
  • An empty alt is the correct default for a decorative image, and must be deliberate.

Props are read-only

A component may never assign to its own props. React does not stop you; it simply will not work and will confuse you later.

function Bad({ count }) {
  count = count + 1;      // changes nothing outside, and is lost next render
  return <p>{count}</p>;
}

// If a value needs to change, it is state, not a prop.
  • Data flows down. To change a parent's value, the parent passes a function down.
  • This one-way rule is what makes it possible to work out where a value came from.

Passing functions down

The standard way for a child to tell its parent that something happened.

function Parent() {
  const [query, setQuery] = useState("");
  return <SearchBox value={query} onChange={setQuery} />;
}

function SearchBox({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
      aria-label="Search courses"
    />
  );
}
  • The child does not know what happens next, which is what makes it reusable.
  • Name these props for the event: onChange, onSelect, onDismiss.
  • The state lives in the parent because the parent is what needs to know.

Spreading props

Forwards a whole object of props, useful for wrapper components.

function Field({ label, ...rest }) {
  return (
    <label>
      {label}
      <input {...rest} />
    </label>
  );
}

<Field label="Email" type="email" required maxLength={60} />
  • Everything not named explicitly is collected into rest.
  • Convenient, but it hides which props a component actually accepts. Use it sparingly.

State

State is data a component remembers between renders. Changing it is what causes a re-render, and that is the only thing that does.

useState

Declares one piece of state. Returns the current value and a function to replace it.

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}
  • The argument is the initial value, used on the first render only.
  • The array destructuring is a convention; the names are entirely yours.
  • Calling the setter schedules a render. It does not change count on the line below.

State updates are not immediate

The setter queues a change. The variable you are holding belongs to the render you are in and never changes.

const [count, setCount] = useState(0);

function handleClick() {
  setCount(count + 1);
  console.log(count);      // still the old value, always
}

// Three calls, one increment: each reads the same stale count
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
  • This is the single most common source of confusion in React.
  • The fix is the updater form below, not a timeout or a second effect.

The updater form

Pass a function to the setter when the new value depends on the old one.

setCount((c) => c + 1);

// Now three calls really do add three
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
  • React calls your function with the latest queued value, not the render's stale copy.
  • Use it whenever the update reads the current value. It is never wrong to prefer it.

State must be replaced, not mutated

React decides whether to re-render by comparing the old value with the new one by identity. Editing an object in place leaves the identity unchanged, so nothing happens.

// Wrong: same array, React sees no change
items.push(newItem);
setItems(items);

// Right: a new array
setItems([...items, newItem]);

// Removing
setItems(items.filter((i) => i.id !== id));

// Updating one entry
setItems(items.map((i) => (i.id === id ? { ...i, done: true } : i)));

// Objects
setUser({ ...user, name: "Amara" });
  • push, splice, sort and reverse all mutate. concat, filter, map and slice return new arrays.
  • sort mutates, so copy first: [...items].sort(...)
  • Nested updates need a new object at every level you changed.

Choosing where state lives

Put it in the closest component that needs it. When two siblings need the same value, move it to their nearest shared parent.

// Both the filter box and the list need the query,
// so it lives in the parent that renders both.
function Explorer() {
  const [query, setQuery] = useState("");
  return (
    <>
      <SearchBox value={query} onChange={setQuery} />
      <Results query={query} />
    </>
  );
}
  • This is called lifting state up.
  • Lift only as far as necessary. State at the top of the app re-renders everything below it.
  • Do not copy a prop into state; you will end up with two versions that disagree.

What should not be state

Anything you can calculate from existing state or props during the render.

// Unnecessary: a second value that can fall out of step
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);

// Better: derive it
const [items, setItems] = useState([]);
const count = items.length;
const completed = items.filter((i) => i.done);
  • Derived values cannot go stale, because they are recalculated every render.
  • Fewer pieces of state means fewer ways for the screen to contradict itself.

Events

Handling an event

camelCase attribute, a function as the value. Not a string.

<button onClick={handleClick}>Save</button>

<input onChange={(e) => setQuery(e.target.value)} />

<form onSubmit={handleSubmit}>
<div onKeyDown={handleKey}>
  • Pass the function, do not call it. onClick={handleClick()} runs it during render.
  • Use an inline arrow when you need to pass an argument: onClick={() => remove(id)}
  • React attaches one listener at the root and dispatches from there, which is why there is nothing to remove.

The event object

A wrapper over the browser event with the same interface.

function handleSubmit(e) {
  e.preventDefault();          // stop the page reloading
  save(query);
}

function handleChange(e) {
  e.target.value               // what the field now contains
}
  • preventDefault on a form submit is nearly always what you want.
  • stopPropagation stops it reaching parent handlers.
  • Returning false does nothing here, unlike in old inline HTML handlers.

Events and accessibility

A click handler on a div is invisible to keyboard and screen reader users. Use the element that already has the behaviour.

// Wrong: unreachable without a mouse
<div onClick={open}>Open</div>

// Right: focusable, activates on Enter and Space, announced as a button
<button onClick={open}>Open</button>
  • A button gives you focus, keyboard activation and the correct role for free.
  • If it navigates, it is a link. If it acts, it is a button.
  • Recreating that on a div takes tabIndex, role, and two key handlers, and is still worse.

Conditional rendering

Ternary

The usual choice when there are two outcomes.

return signedIn ? <Dashboard /> : <SignInPrompt />;

<p>{count === 1 ? "1 module" : `${count} modules`}</p>
  • Readable for two branches. Nesting them three deep is not; use a variable or an early return.

Logical AND

Render something or nothing.

{error && <p role="alert">{error}</p>}

{items.length > 0 && <ResultsList items={items} />}
  • Careful with numbers: {items.length && <List />} renders a literal 0 when the array is empty.
  • Compare explicitly, as above, and the problem disappears.

Early return

The clearest option when a whole branch of the component differs.

function Results({ loading, error, items }) {
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (items.length === 0) return <EmptyState />;

  return <ul>{items.map((i) => <Item key={i.id} {...i} />)}</ul>;
}
  • Handles loading, error and empty explicitly, which is what a real interface needs.
  • Every one of those four states is a real thing a user will see. Design all four.

Lists and keys

Rendering a list

map over the array and return an element for each entry.

<ul>
  {courses.map((course) => (
    <li key={course.slug}>
      <a href={`/learn/${course.slug}`}>{course.title}</a>
    </li>
  ))}
</ul>
  • map returns a new array, and React renders arrays of elements happily.
  • forEach returns nothing, which renders nothing. This catches people out.
  • The arrow must return the element: use parentheses, or braces plus an explicit return.

Keys

A stable identity for each entry, so React can tell which item moved rather than assuming positions are meaningful.

// Good: an id that belongs to the data
{items.map((i) => <Row key={i.id} item={i} />)}

// Risky: index changes when the list is sorted or filtered
{items.map((i, index) => <Row key={index} item={i} />)}
  • Unique among siblings. It does not need to be globally unique.
  • Index keys corrupt state: delete the first row and every row below inherits the wrong input value.
  • Index is acceptable only when the list never reorders, never filters and never has entries removed.
  • Never use Math.random(). A new key every render destroys and rebuilds the whole list.

Filtering and sorting

Do it before rendering, and copy before sorting.

const visible = courses
  .filter((c) => c.title.toLowerCase().includes(query.toLowerCase()))
  .slice()                                  // copy, because sort mutates
  .sort((a, b) => a.title.localeCompare(b.title));

return <CourseList courses={visible} />;
  • Deriving the visible list during render means it can never disagree with the query.
  • localeCompare sorts text correctly. A plain minus works only on numbers.

Forms

Controlled inputs

React state holds the value and the input displays it. One source of truth, which is what makes validation and resetting straightforward.

const [email, setEmail] = useState("");

<label htmlFor="email">Email</label>
<input
  id="email"
  type="email"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
/>
  • value plus onChange together. value alone makes the field read-only and React will warn.
  • Initialise with an empty string, never undefined, or the input switches from uncontrolled and warns.

Several fields

One state object, keyed by the input name.

const [form, setForm] = useState({ name: "", email: "" });

function update(e) {
  const { name, value } = e.target;
  setForm((f) => ({ ...f, [name]: value }));
}

<input name="name" value={form.name} onChange={update} />
<input name="email" value={form.email} onChange={update} />
  • The square brackets are a computed key: the property named by the variable.
  • Spread first, then override, or the change is discarded.

Other input types

Checkboxes and selects bind slightly differently.

<input type="checkbox" checked={agreed}
       onChange={(e) => setAgreed(e.target.checked)} />

<select value={track} onChange={(e) => setTrack(e.target.value)}>
  <option value="foundations">Foundations</option>
  <option value="web">Web Development</option>
</select>

<textarea value={notes} onChange={(e) => setNotes(e.target.value)} />
  • Checkboxes use checked and e.target.checked, not value.
  • A select uses value on the select itself, not selected on an option.
  • A textarea takes value as a prop rather than text between its tags.

Submitting

Handle onSubmit on the form, not onClick on the button, so Enter works too.

function handleSubmit(e) {
  e.preventDefault();
  if (!email.includes("@")) {
    setError("Enter a valid email address.");
    return;
  }
  setError("");
  send(email);
}

<form onSubmit={handleSubmit} noValidate>
  {/* fields */}
  {error && <p role="alert">{error}</p>}
  <button type="submit">Send</button>
</form>
  • onSubmit catches the Enter key, which onClick does not. Keyboard users rely on it.
  • role="alert" makes a screen reader announce the error when it appears.
  • Associate every input with a label. A placeholder is not a label.

Effects

An effect synchronises your component with something outside React: a network request, a subscription, the document title. It is not a general-purpose place to put code that runs after a render.

useEffect

Runs after the render is on screen. The second argument controls when it runs again.

import { useEffect } from "react";

useEffect(() => {
  document.title = `${count} complete`;
}, [count]);          // again whenever count changes

useEffect(() => { ... }, []);    // once, after the first render
useEffect(() => { ... });        // after every render, rarely what you want
  • The array is a list of values the effect reads. Omit one and it will use a stale copy.
  • An empty array means the effect depends on nothing and runs once.
  • Do not fight the dependency warning. It is almost always describing a real bug.

Cleanup

Return a function and React runs it before the next effect and when the component unmounts. Anything you started, stop here.

useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
}, []);

useEffect(() => {
  window.addEventListener("resize", onResize);
  return () => window.removeEventListener("resize", onResize);
}, []);
  • Without cleanup, timers and listeners accumulate every time the component mounts.
  • In development React mounts twice on purpose, so a missing cleanup shows up immediately.
  • That double run is not a bug to work around. It is the check working.

When not to use an effect

If you can calculate it during the render, do that instead. Effects that only transform data cause an extra render and can display the wrong thing first.

// Unnecessary: an effect to compute a value
const [full, setFull] = useState("");
useEffect(() => { setFull(first + " " + last); }, [first, last]);

// Just calculate it
const full = first + " " + last;

// Also unnecessary: responding to a click
// Put that logic in the click handler, not in an effect watching state.
  • Ask what outside system this synchronises with. If the answer is none, it is not an effect.
  • An effect that sets state which triggers the same effect is an infinite loop.

Fetching data

The four states

Every request has four possible outcomes on screen, and an interface that only handles the happy one will look broken most of the time it matters.

const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} onRetry={reload} />;
if (items.length === 0) return <EmptyState />;
return <List items={items} />;
  • Loading, error, empty, and results. All four are real and all four need a design.
  • Empty is not an error. Say so plainly and suggest what to do next.
  • An error state without a retry leaves the user stuck.

Fetching in an effect

Request in the effect, guard against a response arriving after the component has moved on.

useEffect(() => {
  let cancelled = false;
  setLoading(true);
  setError(null);

  fetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`)
    .then((res) => {
      if (!res.ok) throw new Error(`Request failed: ${res.status}`);
      return res.json();
    })
    .then((data) => { if (!cancelled) setItems(data.results); })
    .catch((err) => { if (!cancelled) setError(err.message); })
    .finally(() => { if (!cancelled) setLoading(false); })

  return () => { cancelled = true; };
}, [query]);
  • fetch does not reject on 404 or 500. Check res.ok yourself or you will parse an error page as data.
  • The cancelled flag prevents a slow first response overwriting a fast second one.
  • encodeURIComponent on anything a user typed, or spaces and ampersands break the URL.
  • AbortController is the fuller solution: it also stops the request rather than ignoring it.

Debouncing

Wait until typing stops before requesting, so one search is not thirty requests.

const [query, setQuery] = useState("");
const [debounced, setDebounced] = useState("");

useEffect(() => {
  const id = setTimeout(() => setDebounced(query), 300);
  return () => clearTimeout(id);
}, [query]);

// then fetch on [debounced], not [query]
  • The cleanup cancels the previous timer, so only the last keystroke survives.
  • Around 300ms feels immediate while removing most of the requests.

Keys and secrets

Anything in frontend code is public. There is no way to hide a key in a React application.

  • A key in your bundle is readable by every visitor, whatever the variable is called.
  • Use APIs that permit public keys with a domain restriction, or route through a server you control.
  • Committing a real key to a public repository means rotating it, not deleting the commit.

Hooks

Hooks are functions that let a component use React features. They all begin with `use`, and they all follow the same two rules.

The rules of hooks

Call them at the top level of a component, in the same order every render. React matches state to hooks by call order, so a conditional call desynchronises everything after it.

// Wrong: this hook sometimes does not run
if (signedIn) {
  const [name, setName] = useState("");
}

// Right: always called, the condition moves inside
const [name, setName] = useState("");
  • No hooks inside conditions, loops, or nested functions.
  • Only in components and in other hooks. Not in plain helper functions.
  • The eslint plugin catches every violation. Install it and trust it.

useRef

A box holding a value that survives renders but does not cause one when it changes. Also how you reach a real DOM node.

const inputRef = useRef(null);

useEffect(() => { inputRef.current?.focus(); }, []);

return <input ref={inputRef} />;
  • The value lives on .current, and changing it re-renders nothing.
  • Use it for timer ids, previous values, and focus management.
  • If the screen should update when it changes, that is state, not a ref.
  • Managing focus after a route change or a dialog opens is a genuine accessibility requirement.

useMemo and useCallback

Cache a computed value or a function between renders. Both are optimisations, and both cost something.

const sorted = useMemo(
  () => [...items].sort((a, b) => a.title.localeCompare(b.title)),
  [items]
);

const handleSelect = useCallback((id) => setSelected(id), []);
  • Reach for these after measuring, not before. Most components are fast enough without them.
  • useMemo caches a value. useCallback caches a function.
  • They matter most when passing to a memoised child, where a new function every render defeats the point.

useContext

Reads a value provided higher in the tree, for things genuinely global like a theme or the signed-in account.

const ThemeContext = createContext("light");

// provide, near the top
<ThemeContext.Provider value={theme}>
  <App />
</ThemeContext.Provider>

// consume, at any depth
const theme = useContext(ThemeContext);
  • Solves passing a prop through six components that do not use it.
  • Not a state manager. Every consumer re-renders when the value changes.
  • Two or three contexts for genuinely app-wide values is healthy. Twelve is a design problem.

useReducer

For state with several fields that change together according to rules.

function reducer(state, action) {
  switch (action.type) {
    case "start":   return { ...state, loading: true, error: null };
    case "success": return { loading: false, error: null, items: action.items };
    case "failure": return { ...state, loading: false, error: action.error };
    default:        return state;
  }
}

const [state, dispatch] = useReducer(reducer, { loading: false, error: null, items: [] });
dispatch({ type: "start" });
  • Makes impossible combinations impossible: loading and error can never both be set.
  • The reducer is a plain function, so it is easy to test on its own.
  • Worth it once three or more pieces of state always change together.

Custom hooks

A function starting with `use` that calls other hooks. This is how logic is shared between components.

function useDebounced(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

// in a component
const query = useDebounced(input);
  • The name must start with use, or the rules of hooks cannot be checked.
  • Each component calling it gets its own separate state. Nothing is shared but the logic.
  • This is the answer to duplicated useEffect blocks across components.

Routing

React itself has no router. React Router is the common choice, and it maps the URL to which components render.

Setting up routes

Declare the map once, near the top of the application.

import { BrowserRouter, Routes, Route } from "react-router-dom";

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/courses" element={<Courses />} />
    <Route path="/courses/:slug" element={<Course />} />
    <Route path="*" element={<NotFound />} />
  </Routes>
</BrowserRouter>
  • The colon marks a parameter. :slug matches any single segment.
  • The asterisk route catches everything unmatched. Always include one.

Routing and accessibility

A client-side navigation changes the screen without telling assistive technology anything happened. You have to say so.

  • Move focus to the new page's heading after a route change.
  • Update the document title so the tab and history are meaningful.
  • This is the most commonly skipped requirement in single page applications.

Performance and accessibility

Reading the render

Before optimising, find out what is actually re-rendering and why.

  • React Developer Tools has a profiler that shows exactly this. Use it before guessing.
  • A component re-renders when its state changes, its props change, or its parent re-renders.
  • Most performance problems are one large piece of state high in the tree, not slow components.

React.memo

Skips re-rendering a component when its props are unchanged.

const Row = React.memo(function Row({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.title}</li>;
});
  • Props are compared shallowly, so a new object or function every render defeats it.
  • Pairs with useCallback for exactly that reason.
  • Wrapping everything in memo makes an application slower, not faster.

Accessibility checklist

The requirements most easily lost when markup is generated by components.

  • Use semantic elements. A component named Button should render a button.
  • Every input has a label tied by htmlFor and id.
  • Every image has alt, empty when decorative.
  • Errors and status messages use role="alert" or aria-live so they are announced.
  • Manage focus when content appears, disappears, or the route changes.
  • The whole interface must work from the keyboard alone. Test by unplugging the mouse.

Common errors, and what they mean

React's messages are more helpful than most, once you know which mistake each one describes.

Objects are not valid as a React child

You put an object between braces. React can render strings, numbers and elements, not objects.

{user}                  // object, throws
{user.name}             // string, fine
{JSON.stringify(user)}  // useful while debugging

Each child in a list should have a unique key prop

A map without a key, or with keys that repeat among siblings.

{items.map((i) => <Row key={i.id} item={i} />)}
  • Duplicate keys produce the same warning and cause genuinely wrong behaviour.

Too many re-renders

State is being set during the render rather than in response to an event.

// Wrong: calls setOpen immediately, every render, forever
<button onClick={setOpen(true)}>Open</button>

// Right: pass a function
<button onClick={() => setOpen(true)}>Open</button>

Cannot read properties of undefined

Data has not arrived yet. The first render happens before the fetch resolves.

{user?.name}                      // optional chaining
{items?.length ?? 0}
const [user, setUser] = useState(null);   // then handle null explicitly
  • This is why the loading state exists. Render it rather than guarding every field.

A component is changing an uncontrolled input to be controlled

The value started as undefined and later became a string.

useState("")          // right
useState(undefined)   // causes the warning

Invalid hook call

A hook called outside a component, inside a condition, or from two copies of React.

  • Check it is at the top level of a component or a custom hook.
  • Two copies of React in node_modules produces this too, and is worth checking second.

Want to learn how to build with this?

A reference tells you what exists. The course teaches you when to reach for it, and has you build something real while you learn.