[🐞] Function `ref` runs before the element is connected, has its later attributes, or has children

Open
#9,351 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
30/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
javascript, typescript, vite
Domain
frontend, web-dev

Research direction

The ref call happens in createNewElement() in packages/qwik/src/core/client/vnode-diff.ts, inside the loop that applies props, via applyRef() calling value(element) (around lines 1099 and 1340). Start by reading that loop and comparing it with the v1 ref timing. A fix would defer function and signal refs until the element is inserted and its children exist, and a test should check isConnected and attributes inside the callback. The maintainer has not picked a design yet, so confirm the approach first.

Written by the indexing model from the issue text.

Description

Which component is affected?

Qwik Runtime

Describe the bug

A function ref on an element that mounts on the client runs while the element is still only half built. At that point the element:

  • is not in the document (el.isConnected === false),
  • does not have the attributes that come after ref in the JSX yet,
  • has no children yet.
const Probe = component$(() => {
  const atRef = useSignal('pending');
  return (
    <div
      ref={(el: Element) => {
        atRef.value = `isConnected=${el.isConnected}, hasAttribute('data-after')=${el.hasAttribute('data-after')}, childNodes=${el.childNodes.length}`;
      }}
      data-after="set"
    >
      <p>In ref callback: {atRef.value}</p>
    </div>
  );
});

export default component$(() => {
  const open = useSignal(false);
  return (
    <div>
      <button onClick$={() => (open.value = true)}>Mount Probe</button>
      {open.value && <Probe />}
    </div>
  );
});

Expected: the ref callback receives the finished element, already in the document: isConnected=true, hasAttribute('data-after')=true, with its children present. This is how React handles ref callbacks: it calls them after the element is attached.
Actual: isConnected=false, hasAttribute('data-after')=false, childNodes=0

This is not a v2 regression. v1 calls the ref at the same point. The behaviour then differs after one microtask (Promise.resolve().then(...)):

in ref callback one microtask later
v2 (rc.4 and main) isConnected=false isConnected=true
v1 (1.20) isConnected=false isConnected=false

In v2, createNewElement() calls applyRef() → value(element) from inside the loop that applies the element's props. That loop runs right after the element is created, before the element is inserted and before its children are created (vnode-diff.ts#L1099, and #L1340 for the var-props path). Any code that needs a usable element in the ref therefore fails. For example, el.showPopover() throws because the element is disconnected and popover is not set yet. el.focus() and layout measurements also fail.

Would it be possible to call function refs (and set signal refs) only after the element and its subtree are inserted?

Tested with vite dev and with a production build (vite build + vite preview), on 2.0.0-rc.4 and on current main (pkg.pr.new/QwikDev/qwik/@qwik.dev/core@main, 2026-11-01).

Reproduction

v2 playground: next.qwik.dev/playground

v1 playground (for comparison, same behaviour inside the callback): qwik.dev/playground

The two playgrounds use the same code. Only the import source differs (@qwik.dev/core vs @builder.io/qwik). They also show the one-microtask-later check.

Steps to reproduce
  1. Open the v2 playground link.
  2. Click "Mount Probe".
  3. "In ref callback" shows isConnected=false, hasAttribute('data-after')=false, childNodes=0.
System Info
  System:
    OS: macOS 27.0.1
    CPU: (14) arm64 Apple M4 Max
  Binaries:
    Node: 22.22.0
    pnpm: 10.11.1
  Browsers:
    Chrome: 156.0.8078.74
  npmPackages:
    @qwik.dev/core: https://pkg.pr.new/QwikDev/qwik/@qwik.dev/core@main => 2.0.0-rc (also 2.0.0-rc.4)
    @qwik.dev/router: https://pkg.pr.new/QwikDev/qwik/@qwik.dev/router@main => 2.0.0-rc (also 2.0.0-rc.4)
    typescript: 5.9.3
    vite: 8.3.5
Additional Information

I hit this while porting an overlay host. It renders a popover="manual" element and uses a function ref to call el.showPopover() as soon as possible. A useVisibleTask$ is not an option there, because a closed popover has no layout box and the task never runs. showPopover() throws when called inside the ref, because the element isn't connected and the popover attribute isn't set yet.

Workaround: set the popover attribute imperatively, then check el.isConnected on each requestAnimationFrame and call showPopover() once it is true.

Dominant language
TypeScript
Stars
22.1k
Forks
1.4k
Avg merge
2d 17h
Merged PRs (30d)
101

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from QwikDev/qwik

All issues in QwikDev/qwik

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.