Skip to content

fix(compiler): nullish checkbox/radio value removes the attribute in non-hydratable builds (#4017) - #4021

Merged
ryansolid merged 2 commits into
nextfrom
claude/project-thread-w8qjvi-4017
Oct 13, 2026
Merged

ryansolid merged 2 commits into
nextfrom
claude/project-thread-w8qjvi-4017

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Requested by Ryan · project thread

Summary

Fixes #4017 (follow-up to #4009).

Before: in non-hydratable client builds, both compilers inlined el.value = v ?? "" for every input, so <input type="checkbox" value={v()} checked /> submitted "" when v() was nullish, unlike hydratable builds and spreads.

After: when an input may be a checkbox or radio, a dynamic value/defaultValue compiles to setProperty(el, name, v), the same runtime path #4009 fixed, so a nullish value removes the attribute and the box submits "on". Text inputs, untyped inputs and textarea keep the #2957 ?? "" clearing.

How: both compilers use the same rule, without constant folding, so they can't diverge.

  • A static type of checkbox/radio (case-insensitive): setProperty.
  • No type, or any other static string: unchanged inline ?? "".
  • A dynamic type (expression, identifier, prop:type, attr:type): setProperty, which checks the live type at runtime.
  • Literal values and spreads are unchanged.
Input Before After
<input type="checkbox" value={v()}/> el.value = v ?? "" setProperty(el, "value", v)
<input type="radio" defaultValue={v()}/> el.defaultValue = v ?? "" setProperty(el, "defaultValue", v)
<input type={kind()} value={v()}/> el.value = v ?? "" setProperty(el, "value", v)
<input type="text" value={v()}/>, <input value={v()}/>, <textarea value={v()}/> inline ?? "" unchanged

Public API changes

None. setProperty is an existing runtime import compiled output already uses (hydratable builds, and <select value> since #3928).

How did you test this change?

  • New packages/compiler/__tests__/dom-nullish-input-value.test.js (jsdom): compiles with Babel and Oxc, hydratable on and off, and mirrors the fix(web): preserve default checkbox and radio submission values #4009 runtime test. 8 cases fail with the source changes reverted (both compilers, non-hydratable).
  • Fixtures template100–template107 in attributeExpressions, updated via vitest -u and UPDATE_OXC_FIXTURES=1; parity diffs regenerated with UPDATE_PARITY=1 (only line/numbering shifts, no new divergence).
  • Rust: test:rust (all three feature configs), cargo fmt --check, clippy clean. Compiler vitest 6212 passed. Babel plugin 299 passed. @solidjs/web input-nullish-value.spec.ts 52/52, client 1389, hydrate 493.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w


Generated by Claude Code

…non-hydratable builds (#4017)

Both compilers inlined the #2957 text-field fallback (`el.value = v ?? ""`)
for value/defaultValue on every input, so a client-only build never reached
the #4009 runtime path and a checked checkbox with a nullish value submitted
"" instead of "on".

When an <input>'s static `type` is checkbox or radio (case-insensitive), or
its type cannot be resolved at compile time (expression, prop:type,
attr:type), a non-literal value/defaultValue now compiles to the existing
`setProperty` runtime helper, which already tests the live type. Inputs with
no type or another static type, and textareas, keep the inline `?? ""`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcUAu8BQ6Xsf8XRBhr9V1w
@changeset-bot

changeset-bot Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6c62af6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
test-integration Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch
@solidjs/web Patch
board-example Patch
todos-server-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ryansolid ryansolid self-assigned this Oct 13, 2026
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.44 KB 0 B 0 B +31 B 7.45 KB ✅
signals: + createStore 14.88 KB 0 B 0 B 0 B 14.89 KB ✅
signals: + isPending/latest 9.71 KB 0 B 0 B −72 B 9.73 KB ✅
app: render + one signal (the simple-app floor) 9.92 KB 0 B 0 B −39 B 9.94 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.84 KB 0 B 0 B −166 B 17.93 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 29.45 KB 0 B 0 B +28 B 29.50 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.01 KB 0 B 0 B 0 B 13.03 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.62 KB 0 B 0 B 0 B 14.59 KB ⚠️ over by 31 B, 20 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.93 KB 0 B 0 B 0 B 28.96 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.13 KB 0 B 0 B −57 B 10.13 KB ⚠️ over by 1 B, 77 B minified headroom
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.64 KB 0 B 0 B +70 B 25.65 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 31.56 KB 0 B 0 B 0 B 31.58 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.43 KB 0 B 0 B 0 B 11.43 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 34.08 KB 0 B 0 B −1715 B 34.33 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 8.99 KB, wire.js 0.93 KB
page: live server components (base + live/GET + action + isPending/latest) 37.86 KB 0 B 0 B −1803 B 38.15 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, trace.js 9.00 KB, wire.js 0.93 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 34.97 KB 0 B 0 B −2793 B 35.66 KB ✅ assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.80 KB, sc-comments.js 0.20 KB, trace.js 8.97 KB, wire.js 0.93 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 40.51 KB 0 B 0 B −2835 B 41.21 KB ✅ eager (counted): web.js 21.47 KB; assets.js 0.78 KB, bind.js 1.83 KB, decode.js 6.23 KB, regions.js 0.79 KB, sc-comments.js 0.19 KB, trace.js 8.95 KB, wire.js 0.93 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 41.37 KB 0 B 0 B −1702 B 41.79 KB ✅ assets.js 0.78 KB, bind.js 1.85 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.80 KB, server.js 1.02 KB, serverForms.js 3.58 KB, trace.js 8.97 KB, wire.js 0.94 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.19 KB 0 B 0 B −1751 B 47.49 KB ✅ eager (counted): client.js 27.69 KB; assets.js 0.78 KB, bind.js 1.85 KB, decode.js 6.23 KB, lazy-page.js 0.04 KB, regions.js 0.81 KB, server.js 1.02 KB, serverForms.js 3.32 KB, trace.js 8.95 KB, wire.js 0.94 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.47 KB 0 B 0 B 0 B 20.48 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • app: CSR, observe tier (same app on the observe artifacts): over brotli cap by 31 B; minified 41,460 B vs 41,460 B recorded with the cap (+0 B) — 20 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled floor (one template, one text hole, one delegated click): over brotli cap by 1 B; minified 28,386 B vs 28,443 B recorded with the cap (−57 B) — 77 B of the 20 B minified allowance left; +0 B minified over this PR's base

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

Copy link
Copy Markdown
Member Author

The red Size / check here is "app: compiled hydrating", over its brotli cap by 31 B with a 0 B delta against the base. It isn't from this PR: every scenario is unchanged against the base, and the same scenario fails on next itself (the Size runs for the last three pushes to next, through d8ba741, are red). No fix exists yet; it needs either a trim or a recorded cap change on next.

— Claude via Claude Code


Generated by Claude Code

@coveralls

coveralls commented Oct 13, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38433179373

Coverage remained the same at 77.429%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1321
Covered Lines: 1079
Line Coverage: 81.68%
Relevant Branches: 1036
Covered Branches: 746
Branch Coverage: 72.01%
Branches in Coverage %: Yes
Coverage Strength: 27.2 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ 2 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 193 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ memo + sync render effect only (reference) 28.6 ms 32.5 ms -11.92%
⚡ createStore setter: delete + set one root key (#3044 overlay) 2.1 ms 1.8 ms +16.44%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/project-thread-w8qjvi-4017 (6c62af6) with next (be845cb)

Open in CodSpeed

Copy link
Copy Markdown
Member Author

CodSpeed flags two regressed benchmarks here. This PR only changes the two compilers and their fixtures; no runtime package source changed, so the signals benchmarks run identical code to next. CodSpeed's report also notes that the compared runs used different runtime environments, and #4020 shows the same pattern on code these benches don't reach. I'm re-running the CodSpeed workflow once to confirm.

— Claude via Claude Code


Generated by Claude Code

Copy link
Copy Markdown
Member Author

CodSpeed flags dbmon shallow full tick at −7.4% after the merge of next. The same benchmark reads −7% on every PR measured against this base (be845cb), #4020 and #4027 included, and CodSpeed warns that the comparison crossed runtime environments. This PR only changes compiler output for checkbox/radio value, which a signals store benchmark doesn't exercise. Size and tests are green. I've re-run the benchmarks once.

— Claude via Claude Code


Generated by Claude Code

@ryansolid
ryansolid merged commit ca15970 into next Oct 13, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants