Emmanuel Akinfulubi

Errors

The errors and warnings you are actually going to hit, in React's own words. 27 entries across 32 errors and warnings. M counts distinct React messages, not error classes.

Every one of these is real. Each snippet declares what React must say, either an error, like TypeError with a fragment of its message, or the console warnings it must print. The build runs it against the vendored React and checks: if it does not die or warn exactly as declared, the build fails and this page is not written. So what you see under each snippet is what React printed for exactly that code. The directory each snippet ran in is shown as /app; the names, messages and line numbers are untouched.

Rules of hooks

Hooks are found by the order they are called in. These are what React says when that order changes, or when a hook has no component to belong to.

Error A hook that runs only some of the time

A hook that runs only some of the time

dies with Error · in happy-dom 20.11.6 · React dev

conditional.jsx

import { useState, useEffect, act } from 'react';
import { createRoot } from 'react-dom/client';

function Toggled({ extra }) {
  const [name] = useState('Ada');
  useEffect(() => {}, []);
  if (extra) {
    const [note] = useState('hi');
  }
  return <p>{name}</p>;
}

const root = createRoot(stage);
await act(async () => root.render(<Toggled extra={false} />));
check('the first render was fine', stage.innerHTML, '<p>Ada</p>');

await act(async () => root.render(<Toggled extra={true} />));

What node printed as it died (React 19.2.7, development build), when the build ran it

node conditional.jsx

ok   the first render was fine
React has detected a change in the order of Hooks called by Toggled. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://react.dev/link/rules-of-hooks

   Previous render            Next render
   ------------------------------------------------------
1. useState                   useState
2. useEffect                  useEffect
3. undefined                  useState
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

/app/node_modules/react-dom/cjs/react-dom-client.development.js:7848
          throw Error("Rendered more hooks than during the previous render.");
                ^

Error: Rendered more hooks than during the previous render.
    at updateWorkInProgressHook (/app/node_modules/react-dom/cjs/react-dom-client.development.js:7848:17)
    at updateReducer (/app/node_modules/react-dom/cjs/react-dom-client.development.js:7967:18)
    at Object.useState (/app/node_modules/react-dom/cjs/react-dom-client.development.js:26511:18)
    at process.env.NODE_ENV.exports.useState (/app/node_modules/react/cjs/react.development.js:1263:34)
    at Toggled (/app/conditional.jsx:8:20)
    at Object.react_stack_bottom_frame (/app/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20)
    at renderWithHooks (/app/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22)
    at updateFunctionComponent (/app/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19)
    at beginWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18)
    at runWithFiberInDEV (/app/node_modules/react-dom/cjs/react-dom-client.development.js:874:13)

Node.js v26.7.0

What it means. React finds a component's state by the order of its hook calls. On the second render a hook appeared that was not there the first time, so every slot after it is off by one. React prints a table of the two orders, then stops.

The fix. Call every hook on every render, at the top of the component, and put the condition inside or after the hook. Lesson 21 breaks the rule each way and shows the fixed twin.

Error A hook after an early return

A hook after an early return

dies with Error · in happy-dom 20.11.6 · React dev

early-return.jsx

import { useState, act } from 'react';
import { createRoot } from 'react-dom/client';

function Greeting({ done }) {
  const [name] = useState('Ada');
  if (done) return <p>finished</p>;
  const [greeting] = useState('Hello');
  return <p>{greeting}, {name}</p>;
}

const root = createRoot(stage);
await act(async () => root.render(<Greeting done={false} />));
check('the first render was fine', stage.innerHTML, '<p>Hello, Ada</p>');

await act(async () => root.render(<Greeting done={true} />));

What node printed as it died (React 19.2.7, development build), when the build ran it

node early-return.jsx

ok   the first render was fine
/app/node_modules/react-dom/cjs/react-dom-client.development.js:7717
        throw Error(
              ^

Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
    at finishRenderingHooks (/app/node_modules/react-dom/cjs/react-dom-client.development.js:7717:15)
    at renderWithHooks (/app/node_modules/react-dom/cjs/react-dom-client.development.js:7684:7)
    at updateFunctionComponent (/app/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19)
    at beginWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18)
    at runWithFiberInDEV (/app/node_modules/react-dom/cjs/react-dom-client.development.js:874:13)
    at performUnitOfWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22)
    at workLoopSync (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41)
    at renderRootSync (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17450:11)
    at performWorkOnRoot (/app/node_modules/react-dom/cjs/react-dom-client.development.js:16583:35)
    at performWorkOnRootViaSchedulerTask (/app/node_modules/react-dom/cjs/react-dom-client.development.js:18957:7)

Node.js v26.7.0

What it means. The second render returned before it reached a hook the first render had called, so there are fewer hooks than React remembers. There is no console warning first: React just stops.

The fix. Move every hook above the first return. A component may return early, but only after it has called all of its hooks.

TypeError A hook called from an ordinary function

A hook called from an ordinary function

dies with TypeError · server (no DOM) · React dev

outside.mjs

import { useState } from 'react';

function loadName() {
  const [name] = useState('Ada');
  return name;
}

check('the import itself is fine', typeof useState, 'function');
console.log(loadName());

What node printed as it died (React 19.2.7, development build), when the build ran it

node outside.mjs

ok   the import itself is fine
Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.
/app/node_modules/react/cjs/react.development.js:1263
      return resolveDispatcher().useState(initialState);
                                ^

TypeError: Cannot read properties of null (reading 'useState')
    at process.env.NODE_ENV.exports.useState (/app/node_modules/react/cjs/react.development.js:1263:33)
    at loadName (file:///app/outside.mjs:4:18)
    at file:///app/outside.mjs:9:13
    at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
    at async node:internal/modules/esm/loader:650:26
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)

Node.js v26.7.0

What it means. A hook needs a component being rendered to belong to. Called from a plain function there is none, so React warns first, and then the call fails because the hook reaches for a renderer that is not there. That is why the last error is a TypeError about null, not a React message.

The fix. Call the hook from a component or from another hook (a function whose name starts with use). A plain function that needs the value takes it as an argument.

Rendering

What React says when what you returned is not something it can draw: a list without keys, an object where a child belongs, a component that is not one.

Error An object as a child (server, development)

An object as a child (server, development)

dies with Error · server (no DOM) · React dev

object.jsx

import { renderToString } from 'react-dom/server';

const user = { name: 'Ada' };

console.log(renderToString(<p>{user}</p>));

What node printed as it died (React 19.2.7, development build), when the build ran it

node object.jsx

/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6474
          throw Error(
                ^

Error: Objects are not valid as a React child (found: object with keys {name}). If you meant to render a collection of children, use an array instead.
    at retryNode (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6474:17)
    at renderNodeDestructive (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6367:11)
    at renderNode (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6905:18)
    at renderElement (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5619:18)
    at retryNode (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6428:21)
    at performWork (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:7435:17)
    at startWork (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:8270:7)
    at renderToStringImpl (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:8380:7)
    at process.env.NODE_ENV.exports.renderToString (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:9869:14)
    at <anonymous> (/app/object.jsx:5:13)

Node.js v26.7.0

What it means. JSX can show text, numbers, elements and arrays of them. A plain object is none of those, and React names the keys it found so you can tell which object it was.

The fix. Show a field of the object, {user.name}, or map it to elements. If you meant a list, pass an array.

warning The same mistake in the production build

The same mistake in the production build

warns, on purpose · in happy-dom 20.11.6 · React prod

minified.jsx

import { createRoot } from 'react-dom/client';

const user = { name: 'Ada' };

const root = createRoot(stage);
root.render(<p>{user}</p>);
await new Promise((resolve) => setTimeout(resolve, 50));

check('what the stage holds', stage.innerHTML, '');
root.unmount();

What node printed (React 19.2.7, production build), when the build ran it

NODE_ENV=production node minified.jsx

Error: Minified React error #31; visit https://react.dev/errors/31?args[]=object%20with%20keys%20%7Bname%7D for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at throwOnInvalidObjectTypeImpl (/app/node_modules/react-dom/cjs/react-dom-client.production.js:3294:9)
    at reconcileChildFibersImpl (/app/node_modules/react-dom/cjs/react-dom-client.production.js:3943:7)
    at /app/node_modules/react-dom/cjs/react-dom-client.production.js:3964:29
    at reconcileChildren (/app/node_modules/react-dom/cjs/react-dom-client.production.js:6118:9)
    at beginWork (/app/node_modules/react-dom/cjs/react-dom-client.production.js:7689:7)
    at performUnitOfWork (/app/node_modules/react-dom/cjs/react-dom-client.production.js:11305:14)
    at workLoopSync (/app/node_modules/react-dom/cjs/react-dom-client.production.js:11182:37)
    at renderRootSync (/app/node_modules/react-dom/cjs/react-dom-client.production.js:11163:7)
    at performWorkOnRoot (/app/node_modules/react-dom/cjs/react-dom-client.production.js:10728:40)
    at performWorkOnRootViaSchedulerTask (/app/node_modules/react-dom/cjs/react-dom-client.production.js:12078:3)
ok   what the stage holds

What it means. The production build drops the long messages to keep the download small. What is left is a number and a link. The link is shown here as text only: this course does not go online. The number is the key to the full message, and error 31 is the object-as-a-child message above.

The fix. Reproduce the problem in development, where the full text is printed, then fix it the same way.

Error A component that is undefined

A component that is undefined

dies with Error · server (no DOM) · React dev

widgets.jsx

export function Widget() {
  return <p>widget</p>;
}

main.jsx

import * as widgets from './widgets.jsx';
import { renderToString } from 'react-dom/server';

const Widgit = widgets.Widgit;

console.log(renderToString(<Widgit />));

What node printed as it died (React 19.2.7, development build), when the build ran it

node main.jsx

/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6119
        throw Error(
              ^

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
    at renderElement (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6119:15)
    at retryNode (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6428:21)
    at performWork (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:7435:17)
    at startWork (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:8270:7)
    at renderToStringImpl (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:8380:7)
    at process.env.NODE_ENV.exports.renderToString (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:9869:14)
    at <anonymous> (/app/main.jsx:6:13)
    at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
    at async node:internal/modules/esm/loader:650:26
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)

Node.js v26.7.0

What it means. The name in the JSX looks up to undefined, usually because of a misspelled name or a default export imported as a named one (or the reverse). React cannot render undefined and says what it expected.

The fix. Check the import against the export: spelling, braces for a named export, none for a default one. The message also names the file of the component that rendered it.

warning A list without keys

A list without keys

warns, on purpose · in happy-dom 20.11.6 · React dev

App.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

function List({ names }) {
  return <ul>{names.map((name) => <li>{name}</li>)}</ul>;
}

const root = createRoot(stage);
await act(async () => root.render(<List names={['Ada', 'Grace']} />));
check('how many items rendered', stage.querySelectorAll('li').length, 2);
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node App.jsx

Each child in a list should have a unique "key" prop.

Check the render method of `List`. See https://react.dev/link/warning-keys for more information.
ok   how many items rendered

What it means. React matches the items of a list from one render to the next by their key. Without one it falls back to position, which is wrong the moment the list is reordered, filtered or inserted into. It still renders: the warning is about what will go wrong later.

The fix. Give each item a key that is stable and unique among its siblings, such as an id from your data: <li key={name}>. Lesson 7 covers keys, and lesson 22 shows what a wrong key does to state.

warning Two items with the same key

Two items with the same key

warns, on purpose · in happy-dom 20.11.6 · React dev

App.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

function List({ names }) {
  return <ul>{names.map((name) => <li key={name}>{name}</li>)}</ul>;
}

const root = createRoot(stage);
await act(async () => root.render(<List names={['Ada', 'Ada']} />));
check('how many items rendered', stage.querySelectorAll('li').length, 2);
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node App.jsx

Encountered two children with the same key, `Ada`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
ok   how many items rendered

What it means. A key only works if it is unique among its siblings. With two the same, React cannot tell the items apart, and an update may duplicate or drop one of them.

The fix. Key by something that is unique in your data, such as an id. Do not use a value that can repeat, like a name, and avoid the index if the list can be reordered.

warning A key that arrives through a spread

A key that arrives through a spread

warns, on purpose · in happy-dom 20.11.6 · React dev

App.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

function Label({ label }) {
  return <p>{label}</p>;
}

const props = { key: 'greeting', label: 'hello' };

const root = createRoot(stage);
await act(async () => root.render(<Label {...props} />));
check('what the label shows', stage.textContent, 'hello');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node App.jsx

A props object containing a "key" prop is being spread into JSX:
  let props = {key: someKey, label: ...};
  <Label {...props} />
React keys must be passed directly to JSX without using spread:
  let props = {label: ...};
  <Label key={someKey} {...props} />
ok   what the label shows

What it means. key is not a prop: React reads it off the element and keeps it. Spreading an object that has one hides the key from the compiler, which React now warns about because the key will stop being applied correctly in future versions.

The fix. Pass the key by itself and spread the rest: <Label key={props.key} {...rest} />.

Error An input with children

An input with children

dies with Error · in happy-dom 20.11.6 · React dev

void.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(stage);
await act(async () => root.render(<input>text</input>));

What node printed as it died (React 19.2.7, development build), when the build ran it

node void.jsx

/app/node_modules/react-dom/cjs/react-dom-client.development.js:20565
                      throw Error(
                            ^

Error: input is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`.
    at setInitialProperties (/app/node_modules/react-dom/cjs/react-dom-client.development.js:20565:29)
    at completeWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:12662:18)
    at runWithFiberInDEV (/app/node_modules/react-dom/cjs/react-dom-client.development.js:874:13)
    at completeUnitOfWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17777:19)
    at performUnitOfWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17658:11)
    at workLoopSync (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41)
    at renderRootSync (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17450:11)
    at performWorkOnRoot (/app/node_modules/react-dom/cjs/react-dom-client.development.js:16583:35)
    at performWorkOnRootViaSchedulerTask (/app/node_modules/react-dom/cjs/react-dom-client.development.js:18957:7)
    at flushActQueue (/app/node_modules/react/cjs/react.development.js:590:34)

Node.js v26.7.0

What it means. Some HTML elements, such as input, img and br, cannot contain anything. React refuses to render one that is given children.

The fix. Close the element yourself, <input />, and put the text in a label next to it, or in the value or placeholder prop.

DOM props

HTML has its own names and nesting rules, and React tells you when JSX does not follow them.

warning A camelCase prop HTML does not have

A camelCase prop HTML does not have

warns, on purpose · in happy-dom 20.11.6 · React dev

App.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(stage);
await act(async () => root.render(<div fooBar="x">hi</div>));
check('the element React rendered', stage.firstChild.tagName, 'DIV');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node App.jsx

React does not recognize the `fooBar` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `foobar` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
ok   the element React rendered

What it means. React passes unknown lowercase attributes through, but a camelCase name that is not an HTML property is almost always a mistake, so it warns and still draws the element.

The fix. Use the real name (tabIndex, htmlFor), or write a custom attribute in lowercase, such as data-foo-bar.

warning class instead of className

class instead of className

warns, on purpose · in happy-dom 20.11.6 · React dev

App.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(stage);
await act(async () => root.render(<div class="box">hi</div>));
check('what the DOM got for class', stage.firstChild.getAttribute('class'), 'box');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node App.jsx

Invalid DOM property `class`. Did you mean `className`?
ok   what the DOM got for class

What it means. JSX attributes follow the DOM property names, not the HTML attribute names, and the DOM property for class is className. React warns, but still writes the attribute.

The fix. Write className. The same goes for htmlFor instead of for.

warning A div inside a p

A div inside a p

warns, on purpose · in happy-dom 20.11.6 · React dev

App.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(stage);
await act(async () => root.render(<p><div>hi</div></p>));
check('the HTML React wrote', stage.innerHTML, '<p><div>hi</div></p>');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node App.jsx

In HTML, <div> cannot be a descendant of <p>.
This will cause a hydration error.

> <p>
>   <div>

ok   the HTML React wrote

What it means. A browser that reads this HTML closes the paragraph before the div, so the markup it builds is not the markup you wrote. React builds the nodes directly and keeps your shape, but server HTML would be rewritten and fail to hydrate, so it warns now.

The fix. Use an element that is allowed inside a paragraph, such as span, or change the p to a div.

Infinite loops

A component that sets state while it renders, or an effect that sets the thing it depends on. React stops each one, and says how.

Error setState during render (server)

setState during render (server)

dies with Error · server (no DOM) · React dev

loop.jsx

import { useState } from 'react';
import { renderToString } from 'react-dom/server';

function Counter() {
  const [n, setN] = useState(0);
  setN(n + 1);
  return <p>{n}</p>;
}

console.log(renderToString(<Counter />));

What node printed as it died (React 19.2.7, development build), when the build ran it

node loop.jsx

/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:3973
        throw Error(
              ^

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
    at dispatchAction (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:3973:15)
    at Counter (/app/loop.jsx:6:3)
    at renderWithHooks (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5072:22)
    at renderElement (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5497:23)
    at retryNode (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6428:21)
    at performWork (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:7435:17)
    at startWork (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:8270:7)
    at renderToStringImpl (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:8380:7)
    at process.env.NODE_ENV.exports.renderToString (/app/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:9869:14)
    at <anonymous> (/app/loop.jsx:10:13)

Node.js v26.7.0

What it means. Setting state in the body of a component makes React render it again straight away, which sets state again. After about 25 rounds React stops and throws rather than hang the program.

The fix. Do not call a setter while rendering. Set state in an event handler or an effect, or compute the value during render instead of storing it.

warning setState during render (client)

setState during render (client)

warns, on purpose · in happy-dom 20.11.6 · React dev

client-loop.jsx

import { useState } from 'react';
import { flushSync } from 'react-dom';
import { createRoot } from 'react-dom/client';

function F() {
  const [n, setN] = useState(0);
  setN(n + 1);
  return <p>{n}</p>;
}

const root = createRoot(stage);
flushSync(() => root.render(<F />));
check('what the stage holds', stage.innerHTML, '');

What node printed (React 19.2.7, development build), when the build ran it

node client-loop.jsx

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
    at renderWithHooksAgain (/app/node_modules/react-dom/cjs/react-dom-client.development.js:7747:17)
    at renderWithHooks (/app/node_modules/react-dom/cjs/react-dom-client.development.js:7665:21)
    at updateFunctionComponent (/app/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19)
    at beginWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18)
    at runWithFiberInDEV (/app/node_modules/react-dom/cjs/react-dom-client.development.js:874:13)
    at performUnitOfWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22)
    at workLoopSync (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41)
    at renderRootSync (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17450:11)
    at performWorkOnRoot (/app/node_modules/react-dom/cjs/react-dom-client.development.js:16583:35)
    at performSyncWorkOnRoot (/app/node_modules/react-dom/cjs/react-dom-client.development.js:18972:7)
An error occurred in the <F> component.

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

ok   what the stage holds

What it means. The same loop on the client. The error is reported to the console instead of thrown at your code, together with the name of the component, and nothing is drawn.

The fix. The same as on the server: leave the setter out of the render body. An error boundary (lesson 27) is how an app shows something useful when this happens anyway.

warning A layout effect that sets its own dependency

A layout effect that sets its own dependency

warns, on purpose · in happy-dom 20.11.6 · React dev

layout-loop.jsx

import { useState, useLayoutEffect } from 'react';
import { createRoot } from 'react-dom/client';

let renders = 0;

function F() {
  const [n, setN] = useState(0);
  renders++;
  useLayoutEffect(() => {
    setN(n + 1);
  }, [n]);
  return <p>{n}</p>;
}

const root = createRoot(stage);
root.render(<F />);
await new Promise((resolve) => setTimeout(resolve, 100));
root.unmount();
check('React stopped the loop', renders > 50 && renders < 1000, true);

What node printed (React 19.2.7, development build), when the build ran it

node layout-loop.jsx

Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
    at getRootForUpdatedFiber (/app/node_modules/react-dom/cjs/react-dom-client.development.js:4624:11)
    at enqueueConcurrentHookUpdate (/app/node_modules/react-dom/cjs/react-dom-client.development.js:4584:14)
    at dispatchSetStateInternal (/app/node_modules/react-dom/cjs/react-dom-client.development.js:9167:18)
    at dispatchSetState (/app/node_modules/react-dom/cjs/react-dom-client.development.js:9127:7)
    at <anonymous> (/app/layout-loop.jsx:10:5)
    at Object.react_stack_bottom_frame (/app/node_modules/react-dom/cjs/react-dom-client.development.js:25989:20)
    at runWithFiberInDEV (/app/node_modules/react-dom/cjs/react-dom-client.development.js:874:13)
    at commitHookEffectListMount (/app/node_modules/react-dom/cjs/react-dom-client.development.js:13249:29)
    at commitHookLayoutEffects (/app/node_modules/react-dom/cjs/react-dom-client.development.js:13213:11)
    at commitLayoutEffectOnFiber (/app/node_modules/react-dom/cjs/react-dom-client.development.js:13951:13)
An error occurred in the <F> component.

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

ok   React stopped the loop

What it means. The effect sets the very value it depends on, so each run causes the next. For effects that run before the browser paints, React counts the nested updates and, past a limit, stops and reports it.

The fix. Do not set state from an effect that reads the same state unless something stops the chain: add a condition, or derive the value during render. Lesson 14 covers dependencies.

warning An effect that sets its own dependency

An effect that sets its own dependency

warns, on purpose · in happy-dom 20.11.6 · React dev

effect-loop.jsx

import { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';

let renders = 0;

function Counter() {
  const [n, setN] = useState(0);
  renders++;
  useEffect(() => {
    if (n < 60) setN(n + 1);
  });
  return <p>{n}</p>;
}

const root = createRoot(stage);
root.render(<Counter />);
await new Promise((resolve) => setTimeout(resolve, 500));
root.unmount();
console.log('renders > 50: ' + (renders > 50));
check('renders > 50', renders > 50, true);

What node printed (React 19.2.7, development build), when the build ran it

node effect-loop.jsx

Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
renders > 50: true
ok   renders > 50

What it means. An effect with no dependency array runs after every render, and this one sets state, so it causes another render, and so on. React warns each time it sees the pattern. Left alone it would not stop, so the panel stops it at 60 renders (and unmounts after half a second); the page prints only that it went past 50, because the real speed depends on the machine.

The fix. Give the effect a dependency array that does not contain what it sets, or remove the effect and compute the value in render. Lesson 14 and lesson 16 cover both.

warning Updating another component while rendering

Updating another component while rendering

warns, on purpose · in happy-dom 20.11.6 · React dev

other.jsx

import { useState, act } from 'react';
import { createRoot } from 'react-dom/client';

let setA;

function A() {
  const [n, setN] = useState(0);
  setA = setN;
  return <span>A{n}</span>;
}

function B() {
  setA(1);
  return <span>B</span>;
}

const root = createRoot(stage);
await act(async () => root.render(<><A /><B /></>));
check('what both components show', stage.textContent, 'A1B');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node other.jsx

Cannot update a component (`A`) while rendering a different component (`B`). To locate the bad setState() call inside `B`, follow the stack trace as described in https://react.dev/link/setstate-in-render
ok   what both components show

What it means. Rendering is meant to be a pure calculation. Here B, while it renders, tells A to change, which React cannot allow: A may already have been drawn this pass.

The fix. Move the call out of render, into an event handler or an effect, or lift the state up to the parent that renders both (lesson 12).

Forms

Controlled and uncontrolled inputs, and the warnings for mixing them up.

warning An input that changes from uncontrolled to controlled

An input that changes from uncontrolled to controlled

warns, on purpose · in happy-dom 20.11.6 · React dev

switch.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

function Field({ value }) {
  return <input value={value} onChange={() => {}} />;
}

const root = createRoot(stage);
await act(async () => root.render(<Field value={undefined} />));
await act(async () => root.render(<Field value="Ada" />));
check('the value after the switch', stage.querySelector('input').value, 'Ada');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node switch.jsx

A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components
ok   the value after the switch

What it means. An input with no value is controlled by the browser; one with a value is controlled by React. This one started as the first and became the second, usually because the state it reads started as undefined.

The fix. Start the state as a string, useState(''), so the input is controlled from the first render. Lesson 11 covers controlled inputs.

warning A value with no onChange

A value with no onChange

warns, on purpose · in happy-dom 20.11.6 · React dev

readonly.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(stage);
await act(async () => root.render(<input value="hi" />));

const input = stage.querySelector('input');
await act(async () => {
  const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
  setter.call(input, 'hello');
  input.dispatchEvent(new Event('input', { bubbles: true }));
});
check('the value after typing', input.value, 'hi');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node readonly.jsx

You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
ok   the value after typing

What it means. React owns the value, and nothing tells it what to do when someone types. So it puts the old value back, and the field cannot be edited.

The fix. Add an onChange that updates state, readOnly if it should not change, or defaultValue if the browser should keep the value.

Effects and async

Effects that return the wrong thing, updates that React was not told to expect, and components that cannot be async on the client.

TypeError An async function as an effect

An async function as an effect

dies with TypeError · in happy-dom 20.11.6 · React dev

async-effect.jsx

import { useEffect, act } from 'react';
import { createRoot } from 'react-dom/client';

function Loader() {
  useEffect(async () => {
    await Promise.resolve();
  }, []);
  return <p>loading</p>;
}

const root = createRoot(stage);
await act(async () => root.render(<Loader />));
check('the first render was fine', stage.innerHTML, '<p>loading</p>');

await act(async () => root.unmount());

What node printed as it died (React 19.2.7, development build), when the build ran it

node async-effect.jsx

useEffect must not return anything besides a function, which is used for clean-up.

It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately:

useEffect(() => {
  async function fetchData() {
    // You can await here
    const response = await MyAPI.getData(someId);
    // ...
  }
  fetchData();
}, [someId]); // Or [] if effect doesn't need props or state

Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching
ok   the first render was fine
/app/node_modules/react-dom/cjs/react-dom-client.development.js:26001
            destroy();
            ^

TypeError: destroy is not a function
    at Object.react_stack_bottom_frame (/app/node_modules/react-dom/cjs/react-dom-client.development.js:26001:13)
    at runWithFiberInDEV (/app/node_modules/react-dom/cjs/react-dom-client.development.js:874:13)
    at commitHookEffectListUnmount (/app/node_modules/react-dom/cjs/react-dom-client.development.js:13316:17)
    at commitHookPassiveUnmountEffects (/app/node_modules/react-dom/cjs/react-dom-client.development.js:13347:11)
    at commitPassiveUnmountEffectsInsideOfDeletedTree_begin (/app/node_modules/react-dom/cjs/react-dom-client.development.js:16264:13)
    at recursivelyTraversePassiveUnmountEffects (/app/node_modules/react-dom/cjs/react-dom-client.development.js:16085:13)
    at commitPassiveUnmountOnFiber (/app/node_modules/react-dom/cjs/react-dom-client.development.js:16127:11)
    at flushPassiveEffects (/app/node_modules/react-dom/cjs/react-dom-client.development.js:18428:9)
    at flushPendingEffects (/app/node_modules/react-dom/cjs/react-dom-client.development.js:18358:14)
    at flushSpawnedWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:18323:44)

Node.js v26.7.0

What it means. An async function always returns a promise, and React reads what an effect returns as its clean-up function. It warns when the effect runs, and when the component unmounts it tries to call the promise as a function.

The fix. Keep the effect itself synchronous and start the async work inside it: define an async function in the effect and call it. Lesson 13 shows the pattern.

warning flushSync inside an effect

flushSync inside an effect

warns, on purpose · in happy-dom 20.11.6 · React dev

flush.jsx

import { useEffect, act } from 'react';
import { flushSync } from 'react-dom';
import { createRoot } from 'react-dom/client';

let ran = false;

function F() {
  useEffect(() => {
    flushSync(() => {});
    ran = true;
  }, []);
  return <p>hi</p>;
}

const root = createRoot(stage);
await act(async () => root.render(<F />));
check('whether the effect ran', ran, true);
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node flush.jsx

flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.
ok   whether the effect ran

What it means. flushSync forces React to update the page right now, which it cannot do while it is already in the middle of an update. The call is ignored for that moment, and React says so.

The fix. Take the call out of the effect. If it must run after a render, schedule it with a setTimeout or a microtask.

warning An update outside act in a test

An update outside act in a test

warns, on purpose · in happy-dom 20.11.6 · React dev

no-act.jsx

import { act } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(stage);
await act(async () => root.render(<p>one</p>));

root.render(<p>two</p>);
await new Promise((resolve) => setTimeout(resolve, 50));

check('the text after the update', stage.textContent, 'two');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node no-act.jsx

An update to Root inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
ok   the text after the update

What it means. In a test, React is told it is running under act, which collects every update so the test can wait for them. An update made outside it may not be finished when the test looks.

The fix. Wrap the update in await act(async () => ...). Lesson 34 shows it in a test file.

warning An async component on the client

An async component on the client

warns, on purpose · in happy-dom 20.11.6 · React dev

async-component.jsx

import { act, startTransition, Suspense } from 'react';
import { createRoot } from 'react-dom/client';

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function later() {
  const ready = wait(100);
  return async function A() {
    await ready;
    return <p>async</p>;
  };
}

const Slow = later();
const root = createRoot(stage);
startTransition(() => root.render(<Suspense fallback={<p>loading</p>}><Slow /></Suspense>));
await wait(500);
check('it rendered anyway', stage.innerHTML, '<p>async</p>');
await act(async () => root.unmount());

const Other = later();
const again = createRoot(document.createElement('div'));
again.render(<Suspense fallback={<p>loading</p>}><Other /></Suspense>);
await wait(500);
await act(async () => again.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node async-component.jsx

An update to Root inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
<A> is an async Client Component. Only Server Components can be async at the moment. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server.
A suspended resource finished loading inside a test, but the event was not wrapped in act(...).

When testing, code that resolves suspended data should be wrapped into act(...):

act(() => {
  /* finish loading suspended data */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework.
ok   it rendered anyway
An update to Root inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
A suspended resource finished loading inside a test, but the event was not wrapped in act(...).

When testing, code that resolves suspended data should be wrapped into act(...):

act(() => {
  /* finish loading suspended data */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
A suspended resource finished loading inside a test, but the event was not wrapped in act(...).

When testing, code that resolves suspended data should be wrapped into act(...):

act(() => {
  /* finish loading suspended data */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act
A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework.

What it means. A component that is an async function returns a promise. Only a server component may do that. On the client React warns about the component and about the promise it suspended on, although the small one in the first half of the panel still draws. The second half renders outside act, as in the missing-act entry, which adds the warning about a resource that finished loading unnoticed.

The fix. Load the data in an effect, or read a promise with use (lesson 26), or fetch on the server and pass the result down.

Server rendering

What React says when the HTML from the server and the first render in the browser disagree.

warning Hydration text mismatch

Hydration text mismatch

warns, on purpose · in happy-dom 20.11.6 · React dev

mismatch.jsx

import { act } from 'react';
import { hydrateRoot } from 'react-dom/client';

stage.innerHTML = '<h1>hi server</h1>';

let root;
await act(async () => {
  root = hydrateRoot(stage, <h1>hi client</h1>);
});
check('the client text won', stage.innerHTML, '<h1>hi client</h1>');
await act(async () => root.unmount());

What node printed (React 19.2.7, development build), when the build ran it

node mismatch.jsx

Error: Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

https://react.dev/link/hydration-mismatch

  <h1>
+   hi client
-   hi server

    at throwOnHydrationMismatch (/app/node_modules/react-dom/cjs/react-dom-client.development.js:5238:11)
    at prepareToHydrateHostInstance (/app/node_modules/react-dom/cjs/react-dom-client.development.js:5334:21)
    at completeWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:12553:15)
    at runWithFiberInDEV (/app/node_modules/react-dom/cjs/react-dom-client.development.js:874:13)
    at completeUnitOfWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17777:19)
    at performUnitOfWork (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17658:11)
    at workLoopSync (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41)
    at renderRootSync (/app/node_modules/react-dom/cjs/react-dom-client.development.js:17450:11)
    at performWorkOnRoot (/app/node_modules/react-dom/cjs/react-dom-client.development.js:16504:11)
    at performWorkOnRootViaSchedulerTask (/app/node_modules/react-dom/cjs/react-dom-client.development.js:18957:7)
ok   the client text won

What it means. The HTML from the server and the first render in the browser must match. They did not, so React cannot reuse the HTML, reports the difference and renders the client's version over it. It is a recoverable error, not a crash.

The fix. Render the same thing on both sides. Values that change between the two, such as dates, random numbers and typeof window, belong in an effect. Lesson 32 covers hydration.

Server/client boundary

Code that belongs on one side of the server/client line, imported on the other.

SyntaxError A client-only hook in a server component

A client-only hook in a server component

dies with SyntaxError · server (no DOM) · React dev

Counter.jsx

import { useState } from 'react';

export function Counter() {
  const [count] = useState(0);
  return <p>{count}</p>;
}

server.jsx

import { Counter } from './Counter.jsx';
import { renderToString } from 'react-dom/server';

console.log(renderToString(<Counter />));

What node printed as it died (React 19.2.7, development build), when the build ran it

node --conditions=react-server server.jsx

/app/Counter.jsx:1
import { useState } from 'react';
         ^
SyntaxError: The requested module 'react' does not provide an export named 'useState'
    at #asyncInstantiate (node:internal/modules/esm/module_job:463:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:561:5)
    at async node:internal/modules/esm/loader:650:26
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)

Node.js v26.7.0

What it means. A server component gets a build of React with the client hooks removed (--conditions=react-server). A component that imports useState asks for something that is not there. This is the module system speaking, not React. No RSC payload is produced here; the Next.js course covers the framework side.

The fix. Move the stateful part into its own file that begins with 'use client' and render it from the server component. Lesson 33 shows the split.

Error server-only code imported from client code

server-only code imported from client code

dies with Error · server (no DOM) · React dev

secret.js

import 'server-only';

export const secret = 'only on the server';

client.mjs

import { secret } from './secret.js';

console.log(secret);

What node printed as it died (React 19.2.7, development build), when the build ran it

node client.mjs

/app/node_modules/server-only/index.js:1
throw new Error(
      ^

Error: This module cannot be imported from a Client Component module. It should only be used from a Server Component.
    at Object.<anonymous> (/app/node_modules/server-only/index.js:1:7)
    at Module._compile (node:internal/modules/cjs/loader:1934:14)
    at Object..js (node:internal/modules/cjs/loader:2074:10)
    at Module.load (node:internal/modules/cjs/loader:1656:32)
    at Module._load (node:internal/modules/cjs/loader:1448:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)
    at loadCJSModuleWithModuleLoad (node:internal/modules/esm/translators:373:15)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:245:9)
    at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
    at async node:internal/modules/esm/loader:650:26

Node.js v26.7.0

What it means. The server-only package is empty under the react-server condition and throws under every other. Importing it from code that is not on the server fails at once, before the secret can end up in a bundle the browser downloads.

The fix. Keep the file out of the client code: import it only from server components, and pass the client a plain value it needs.

How to read any of them. In development React writes a sentence that says what is wrong, usually names the component, and often says what to do. A warning is printed to the console and the program carries on; an error stops the render. The text of a production error is a code and a link, and the link is shown as text only: this course does not go online.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.