Skip to content

fix(babel-plugin,compiler): assign bare variable refs inside ref arrays (#3285) - #3288

Open
javascript-unsafe wants to merge 1 commit into
solidjs:nextfrom
javascript-unsafe:fix/ref-array-lval-3285
Open

fix(babel-plugin,compiler): assign bare variable refs inside ref arrays (#3285)#3288
javascript-unsafe wants to merge 1 commit into
solidjs:nextfrom
javascript-unsafe:fix/ref-array-lval-3285

Conversation

@javascript-unsafe

Copy link
Copy Markdown

Closes #3285

What was broken

<div ref={[el]} /> compiled without errors and passed type-checking, but el was never assigned the element:

let el;
render(() => <div ref={[el]} />, document.body);
el; // undefined ❌

while the callback form worked:

let el;
render(() => <div ref={[(node) => (el = node)]} />, document.body);
el; // <div> ✅

Why

  • A plain ref={el} works: the compiler emits a callback that assigns the element to el when it's not callable.
  • ref={[el]} took a different branch — the array was passed through untouched (wrapped as _$ref(() => [el], element)), with nothing done to the el inside.
  • At runtime, ref arrays are flattened and each entry is called with the element (f && f(element)). A bare variable evaluates to undefined at mount time, so it gets silently skipped and nothing is ever assigned.

The runtime only sees the evaluated value (undefined), not the binding, so this can't be fixed at runtime — the compiler has to turn the bare variable into something callable.

The fix

Both compilers (the babel plugin and the native Rust compiler) now transform ref array literals element by element:

Element in ref array Behavior
bare let variable ref={[el]} lowered to an assignment callback, same contract as non-array refs: read the target once; if it currently holds a function, call it with the element; otherwise assign the element
member expression ref={[obj.el]} same treatment
nested array ref={[[el]]} recursed
spread ref={[...refs]} left as-is (already a value)
callback ref={[fn]}, const/module bindings, globals (undefined) left untouched — assigning to a global like undefined would throw in strict mode
falsy slots null / false untouched, still short-circuit at runtime

This also preserves the existing let cb = someFn; <div ref={[cb]} /> semantics — someFn gets called with the element rather than cb being overwritten.

Tests

  • 9 new behavioral specs in packages/web/test/ref-array.spec.tsx (the issue's case plus multiple variables, callbacks, mixed arrays, member expressions, nested arrays, falsy slots, mutable/const function bindings) — green with both the native compiler and JSX_COMPILER=babel
  • new refArrays babel-plugin fixture, reused as an oxc fixture — zero parity diff between the two compilers' output
  • full suites green: babel-plugin 257 tests, compiler 5362 JS tests + 55 cargo tests (default / no-default-features / tsrx), clippy -D warnings clean

Changeset

Patch bumps for @solidjs/babel-plugin and @solidjs/compiler.

@changeset-bot

changeset-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0479d25

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

This PR includes changesets to release 11 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

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

)

`ref={[el]}` type-checked and compiled but `el` was never assigned. The
ref-array branch in both compilers passed the array straight through, and
the runtime applyRef only *calls* flattened ref-array entries — a bare
variable evaluates to `undefined` at mount, so it was silently skipped.

Bare identifiers and non-optional member expressions inside a ref array
are now lowered to assignment callbacks that mirror the non-array lval
contract: read the target once, call it with the element when it holds a
function, otherwise assign the element. Nested arrays are recursed,
callback refs and const/module refs pass through untouched, globals
(`undefined`) are not treated as assignment targets, and falsy slots keep
short-circuiting.

- babel-plugin: new transformRefArrayLiteral helper wired into the dom,
  universal, and component ref branches
- compiler (Rust): transform_ref_array_literal in shared/refs, wired into
  dom_ref_statements, universal_ref_statements, universal_component_ref,
  and the shared component ref property; BindingTable::is_declared for
  the globals guard
- tests: babel-plugin refArrays fixture (+ oxc fixture reuse), compiler
  cross-mode parity, packages/web behavioral specs (9 cases)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codspeed-hq

codspeed-hq Bot commented Sep 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 136 untouched benchmarks


Comparing javascript-unsafe:fix/ref-array-lval-3285 (0479d25) with next (ace227e)

Open in CodSpeed

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.

1 participant