Repository navigation
Description
Describe the bug
A lone reactive JSX spread crashes when its source is undefined or null. This affects valid optional-props patterns such as <input {...props()} />, where props() returns an object or an absent value.
The runtime throws TypeError: Cannot convert undefined or null to object. Without an enclosing error boundary, it reports REACTIVITY_HALTED and stops processing subsequent reactive updates.
Verified against source at a17dfec, whose package manifests identify the prerelease as 2.0.0-rc.6. This report describes that checkout, not a separate test of the published rc.6 bundles.
Minimal reproduction
Vitest/jsdom TSX test using the normal Solid JSX transform:
/** @jsxImportSource @solidjs/web */
import { expect, test } from "vitest";
import { createSignal, flush } from "solid-js";
import { render } from "@solidjs/web";
test.each([undefined, null])("clearing optional spread to %s", absent => {
const host = document.createElement("div");
const [props, setProps] = createSignal<
{ value: string } | undefined | null
>({ value: "draft" });
const dispose = render(() => <input {...props()} />, host);
try {
expect((host.firstElementChild as HTMLInputElement).value).toBe("draft");
setProps(absent);
flush(); // Throws before the assertion below.
expect((host.firstElementChild as HTMLInputElement).value).toBe("");
} finally {
dispose();
}
});Starting with an undefined reactive spread also crashes the initial mount. TypeScript accepts the object-or-nullish accessor type above; this is not a definitely-null spread expression such as {...null}.
Expected behavior
Treat the absent source as empty props: retain the input, clear the previously supplied value, and continue processing application updates without throwing.
Actual behavior
Both nullish cases throw from hasOwnProperty in spread():
[REACTIVITY_HALTED] An uncaught error halted the reactive system.
No further updates will be processed.
TypeError: Cannot convert undefined or null to object
at hasOwnProperty (<anonymous>)
at packages/web/src/client.ts:724:21
Root cause
spread() at client.ts:720-730 resolves the accessor and calls hasOwn.call(source, "children") and hasOwn.call(source, "ref") without normalizing a nullish source.
Both compilers pass a lone reactive spread directly to this helper. In contrast, adding a static attribute, <input id="field" {...props()} />, selects the merged-props path and the same transitions pass.
Impact and verification
- Optional field configuration and conditional props can cause a mount failure or an application-wide reactive halt, rather than simply removing attributes.
- Reproduced with both the native and Babel JSX compilers against source-runtime aliases.
- Reproduced with development and production flags, and when removing the spread after successful hydration.
- Platform: macOS, Vitest 4.1.6, jsdom 25.0.1. These are source-runtime test results, not a browser compatibility claim.
- This differs from 2.0.0-beta.28:
inputprops spread causes value to be `"undefined" when value is undefined #2957: the entire spread source is absent here, rather than an existing props object containingvalue: undefined.
Reported with OpenCode (GPT-6 Astra).
Activity
fix(web): a nullish lone spread source is an empty spread (#3297)
ryansolid commentedon Sep 7, 2026
ryansolid commentedon Sep 7, 2026