Fix: Objects Are Not Valid as a React Child
The 2015 bug reproduced today, the exact modern error text, what the production build shows instead, and the one-line fix for every cause.
Most of the time this crash has nothing to do with React's rendering engine and everything to do with one JavaScript value that quietly stopped being a string. React throws the moment it tries to insert that value into the page, and the fix is almost always a one-line change to what you pass as a child.
Uncaught Error: Invariant Violation: Objects are not valid as a React
child (found: object with keys {dispatchConfig, dispatchMarker,
nativeEvent, target, currentTarget, type, eventPhase, bubbles,
cancelable, timeStamp, defaultPrevented, isTrusted, view, detail,
screenX, screenY, clientX, clientY, ctrlKey, shiftKey, altKey,
metaKey, getModifierState, button, buttons, relatedTarget, pageX,
pageY, isDefaultPrevented, isPropagationStopped, _dispatchListeners,
_dispatchIDs}). If you meant to render a collection of children, use
an array instead or wrap the object using createFragment(object) from
the React add-ons. Check the render method of `Welcome`.That is the exact text from the original Stack Overflow question, asked in October 2015 and still read almost a million times. On a current React install the same mistake prints a shorter message — reproduced below, on react-dom 19.3.1 — because the message template itself changed, not the cause.
TL;DR#
React can render a string, a number, a boolean, null/undefined, a React element, or an array
or other iterable of those — not a plain object, a Date, or anything else with no React-specific
meaning. The usual culprits are: a plain object where a string was expected, a Date that needs
.toString(), or {{ }} double braces that are actually a JavaScript object literal, not special
template syntax. Render a property, a string conversion, or the array itself — never the object.
Why it happens#
React's own reconciler decides whether a value is a renderable child before it ever touches the
DOM. The function that makes that call ships, readable, inside react-dom's development build —
this is the exact code from the installed react-dom@19.3.1 package, not a paraphrase:
function throwOnInvalidObjectTypeImpl(returnFiber, newChild) {
if (newChild.$$typeof === REACT_LEGACY_ELEMENT_TYPE)
throw Error(
'A React Element from an older version of React was rendered. This is not supported. It can happen if:\n- Multiple copies of the "react" package is used.\n- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n- A compiler tries to "inline" JSX instead of using the runtime.'
);
returnFiber = Object.prototype.toString.call(newChild);
throw Error(
"Objects are not valid as a React child (found: " +
("[object Object]" === returnFiber
? "object with keys {" + Object.keys(newChild).join(", ") + "}"
: returnFiber) +
"). If you meant to render a collection of children, use an array instead."
);
}Only a plain object gets the "with keys" list. Object.prototype.toString.call(value) tags a
plain object as [object Object], so React lists its own keys to help you spot it. Anything else
— a Date, for instance — keeps its own type tag. Reproduced on react-dom 19.3.1: a plain
{ id: 1, name: 'Tunisia' } throws found: object with keys {id, name}, while new Date() throws
found: [object Date], with no key list at all.
The function's first branch fires for a specific, verifiable case: a React element created by an
older React than the one rendering it. Reproduced: an element created with an actual
react@18.3.1 install, mounted inside a react@19.3.1 tree, throws that exact branch's message —
"A React Element from an older version of React was rendered" — not the generic "object with keys"
one. The two installs tag their elements with different symbols (19.3.1 uses a newer, non-colliding
one), which is how the check tells them apart. Which exact release range on the other side of that
boundary also trips it is not established here — only 18.3.1 was tested.
The "Invariant Violation" wording is the other half of the confusion. React 0.14.0 — the release
actually current on 2015-10-14, the day the question above was asked (checked against the npm
registry's publish dates; React 15 did not exist until April 2016) — still prints it. Reproduced,
on the same { id: 1, name: 'Tunisia' } object used earlier, err.message reads:
err.message: Invariant Violation: Objects are not valid as a React child (found: object with keys {id, name}). If you meant to render a collection of children, use an array instead or wrap the object using createFragment(object) from the React add-ons.React 19.3.1 drops both: the message starts straight at "Objects", and it only ever suggests "use
an array instead" — the createFragment alternative is gone from the text. Which exact release in
between made that change is not asserted here — only these two installed versions were checked —
but the cause has not moved in a decade: a non-renderable value reached a JSX child slot.
Fix#
-
Read the error's own hint.
found: object with keys {...}names the exact object;found: [object Date]names the type instead when it is not a plain object. Trace that value back to where it is rendered. -
Render a property or a conversion, not the value itself. Before, throwing
found: object with keys {id, name}:jsxconst user = { id: 1, name: 'Tunisia' }; const label = <p>{user}</p>;After — pick the field you meant:
jsxconst label = <p>{user.name}</p>; -
A
Dateneeds.toString()(or a formatter), not a direct render. Before, throwingfound: [object Date]:jsxconst label = <p>{task.createdAt}</p>;After:
jsxconst label = <p>{task.createdAt.toString()}</p>;.toString()fixes this error, but it has its own trap in a server-rendered app: the string it produces depends on the time zone of whoever calls it. Reproduced on the sameDate, run three times with a differentTZ:textTZ env: (unset) d.toString(): Thu Jan 01 2026 00:00:00 GMT+0000 (Coordinated Universal Time) TZ env: Europe/London d.toString(): Thu Jan 01 2026 00:00:00 GMT+0000 (Greenwich Mean Time) TZ env: America/New_York d.toString(): Wed Dec 31 2025 19:00:00 GMT-0500 (Eastern Standard Time)A Next.js server running in UTC and a reader's browser in another zone can render that string differently for the very same
Date— which is a hydration mismatch, not this error. If fixing this sends you straight into Minified React error #425 instead, that is the next error, and.toISOString()or a fixed-zone formatter avoids both. -
{{ }}is not a template tag — it is an object literal inside a JSX expression. React's own docs put it plainly: "The next time you see{{and}}in JSX, know that it's nothing more than an object inside the JSX curlies!"<p>{{count}}</p>is<p>{ {count: count} }</p>— an object child, not the number. Reproduced: it throwsfound: object with keys {count}. Drop the outer braces:<p>{count}</p>. -
If the bad value is a browser event, check the argument order of
.bind(). The original question's bug is this one. The JSX form of the class mounted in the reproduction (written withReact.createElementthere, since the plain Node script has no JSX transform):jsxclass Welcome extends React.Component { constructor(props) { super(props); this.state = { lang: 'EN' }; } onItemClick(e, item) { this.setState({ lang: item }); // item is not what you think it is } render() { const items = ['EN', 'IT', 'FR', 'GR', 'RU'].map((item) => <li onClick={this.onItemClick.bind(this, item)} key={item}>{item}</li> ); return ( <div> <ul>{items}</ul> <p id="out">{this.state.lang}</p> </div> ); } }Function.prototype.bindprepends its bound arguments before whatever the caller passes — so clicking the<li>callsonItemClick('EN', clickEvent), notonItemClick(clickEvent, 'EN').itemends up holding the click'sSyntheticEvent, and the nextsetStateputs that event instate.lang. Mounted withreact-dom/clientin jsdom and clicked for real, the reproduction'sonUncaughtErrorhandler logged:textObjects are not valid as a React child (found: object with keys {_reactName, _targetInst, type, nativeEvent, target, currentTarget, eventPhase, bubbles, cancelable, timeStamp, defaultPrevented, isTrusted, view, detail, screenX, screenY, clientX, clientY, pageX, pageY, ctrlKey, shiftKey, altKey, metaKey, getModifierState, button, buttons, relatedTarget, movementX, movementY, isDefaultPrevented, isPropagationStopped}). If you meant to render a collection of children, use an array instead.Note the key list itself: React 19.3.1's
SyntheticEventcarries_reactNameand_targetInst, not the 2015 question'sdispatchConfigand_dispatchListeners— the shape of the event object changed, the mistake did not. Swap the handler for an arrow function so the event and the item keep their own slots, and the crash disappears:jsx<li onClick={(e) => this.onItemClick(e, item)}>{item}</li>
Verify the fix#
Re-render after the change: the error should not fire, and the value that used to crash the page
now shows as text. In a production build, the same click gives one opaque line instead of the
readable message above — the reproduction's onUncaughtError handler logged it as:
uncaught error message: Minified React error #31; visit https://react.dev/errors/31?args[]=object%20with%20keys%20%7B_reactName%2C%20_targetInst%2C%20type%2C%20nativeEvent%2C%20target%2C%20currentTarget%2C%20eventPhase%2C%20bubbles%2C%20cancelable%2C%20timeStamp%2C%20defaultPrevented%2C%20isTrusted%2C%20view%2C%20detail%2C%20screenX%2C%20screenY%2C%20clientX%2C%20clientY%2C%20pageX%2C%20pageY%2C%20ctrlKey%2C%20shiftKey%2C%20altKey%2C%20metaKey%2C%20getModifierState%2C%20button%2C%20buttons%2C%20relatedTarget%2C%20movementX%2C%20movementY%2C%20isDefaultPrevented%2C%20isPropagationStopped%7D for the full message or use the non-minified dev environment for full errors and additional helpful warnings.— that is the same error in a client-side production bundle: React replaces the readable text
with a numbered Minified React error #<code> to save bytes, and encodes the original arguments
into the react.dev/errors/<code> URL. Reproduced here: the exact same click, under
NODE_ENV=production, prints Minified React error #31 with the object's keys URL-encoded in the
args[] query string — open that link, or re-run the page in development, to get the full
"found: …" text back. The Node and Edge server bundles are the exception: grepping the full
phrase in react-dom 19.3.1's react-dom-server.node.production.js and
react-dom-server.edge.production.js — the files behind renderToPipeableStream and
renderToReadableStream — finds it in both, while the same search on
react-dom-client.production.js finds nothing. Only the browser build minifies it.
If you are on TypeScript, you can stop this at the type checker instead of the console. Typing a
function's return value as ReactNode — the type that covers everything React can
render — turns the same mistake into a compile-time
error. Reproduced with typescript@7.0.2 and @types/react@19.3.0:
check.tsx(7,3): error TS2322: Type 'User' is not assignable to type 'ReactNode'.One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Shapes that look like this error, and aren't#
Four near-misses are worth knowing before you over-correct — three never throw at all, the fourth throws a different, more specific message:
- An array of valid children is fine — React expects it.
{items.map(i => <li key={i.id}>{i.text}</li>)}never trips this error, because every item is already an element. The same.map()returning raw data instead of JSX ({items.map(i => i)}) throws on the first non-renderable item it reaches, not on all of them — reproduced: an array holding two plain objects only ever reported the first object's keys. - A
Mapor aSetchild renders, with only a warning. Reproduced: a<div>whose only child isnew Map([['a', 1]])rendersa1and logs "Using Maps as children is not supported. Use an array of keyed ReactElements instead." — a warning, not a crash, because Map and Set are iterated the same way arrays are. - A real React element is technically an object too, and it is always fine — from the same
React. A
<div>whose only child is a<span>element never throws — React's check never sees it, because an element from the same React install is recognised and rendered before this check ever runs. An element from an older React install is the next bullet. - A React element from an older React is a fourth shape, with its own message.
Rendering an actual
react@18.3.1element through areact@19.3.1tree throws, but not the "object with keys" message — reproduced, it throws the more specific "A React Element from an older version of React was rendered" instead, because 18.3.1's element tag is exactly the one that check looks for.npm ls reactis how you would spot two Reacts in the first place; see Material UI's ERESOLVE error on React 18 or 19 for a real case ofnpm lsflagging a forced React install as invalid.
A Promise passed as a child is a fifth case worth a warning rather than a fix here: reproduced
with renderToString on React 19.3.1, it does not hit this error at all — it throws a different
message about a component suspending outside a transition, which belongs in its own article.
Whichever shape got you here, the fix is the same move: stop handing React the container and hand it the value inside.
Further reading: React Hook Form: Fix Confirm Password Validation, react-chartjs-2 fill property not working: register Filler.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix localStorage is not defined after 'use client' (Next.js)
The cookie banner from the Stack Overflow question, rebuilt on Next.js 16.3.8: why 'use client' does not stop the crash, the useEffect fix, and the other errors Node 25 and 26 print.
react-chartjs-2 fill property not working: register Filler
Why a line chart ignores fill: true, the console warning that names the cause, the per-chart and chart.js/auto alternatives, and what each one costs in bundle size.
Fix: error while hydrating, root switched to client render
Why this error takes down the whole page instead of one component, and the two separate causes behind it: broken DOM nesting and no Suspense boundary above the failure.


