Skip to content

docs: document ref callback ownership change in migration guide - #3001

Closed
waterWang wants to merge 1 commit into
solidjs:nextfrom
waterWang:docs/ref-ownership-migration
Closed

docs: document ref callback ownership change in migration guide#3001
waterWang wants to merge 1 commit into
solidjs:nextfrom
waterWang:docs/ref-ownership-migration

Conversation

@waterWang

Copy link
Copy Markdown

docs: document ref callback ownership change in migration guide

Description

In Solid 2.0, ref callbacks are no longer owned — getOwner() returns null inside a ref callback, so 1.x-style cleanup registration (using onCleanup() inside the ref callback) no longer works.

This change is documented but not mentioned in the migration guide, which can be confusing for developers migrating from 1.x who relied on the old behavior.

Changes

  • Quick checklist: Added a line about ref callback ownership change
  • DOM section: Added a new ### Ref callbacks: no longer owned subsection with before/after code examples showing how to handle cleanup in 2.0

Fixes solidjs/solid-docs#1630

@changeset-bot

changeset-bot Bot commented Aug 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: a04c9de

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@ryansolid ryansolid left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this fills a real gap, and the technical claims all check out against the implementation: the runtime applies refs with runWithOwner(null, ...) (deliberately, to match the unowned apply phase of directive factories), and onSettled from a component body does accept a returned cleanup.

Two suggestions on the examples before this lands:

  1. The 1.x "before" registers a cleanup for a listener that's never added, and the 2.0 "after" splits the teardown from setup that isn't shown. Pairing setup and teardown makes the migration self-contained and matches onSettled's documented replaces-onMount+onCleanup form:
// 1.x — ref callback ran owned, so cleanup could live inside it
<div ref={(el) => {
  el.addEventListener("pointerdown", onDown);
  onCleanup(() => el.removeEventListener("pointerdown", onDown));
}} />
// 2.0 — the ref callback is unowned; keep setup + teardown in the owned
// component scope and let the ref only capture the element
let el;
onSettled(() => {
  el.addEventListener("pointerdown", onDown);
  return () => el.removeEventListener("pointerdown", onDown);
});

<div ref={el} />
  1. Worth one closing sentence that for reusable element-attached behavior, the directive-factory pattern in the next section is the fuller answer — setup phase owned (primitives, onCleanup), apply phase unowned — so readers don't reach for module-level onSettled plumbing everywhere.

The checklist line and placement are good as-is.

@ryansolid

Copy link
Copy Markdown
Member

To make point 2 concrete rather than a pointer — the section would be complete if it showed the directive-factory form of the same advice, with the cleanup in the owned top half:

// Reusable form: the factory's setup half IS the owned scope —
// primitives and onCleanup live there; the returned apply callback
// (the actual ref) is unowned and only touches the element.
function tooltip(options) {
  let el;
  const instance = createTooltipInstance();
  createEffect(
    () => options.content,
    content => el && instance.setContent(content)
  );
  onCleanup(() => instance.destroy());

  return nextEl => {
    el = nextEl;
    instance.attach(nextEl);
  };
}

<button ref={tooltip({ content: "Save" })} />

Same principle as the onSettled example — owned lifecycle outside, element capture inside — just the reusable packaging. The two together (inline onSettled for one-offs, factory for reusable behavior) cover the whole 1.x onCleanup-inside-ref migration.

ryansolid added a commit that referenced this pull request Aug 17, 2026
Ref callbacks in 2.0 run unowned (getOwner() is null), so the 1.x
pattern of registering onCleanup inside the callback no longer works.
Documents both owned-scope packagings: inline onSettled for one-off
component-local behavior, and the directive factory's setup half for
reusable behavior — plus the timing difference between them. Supersedes
PR #3001.

Co-authored-by: waterWang <waterWang@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid

Copy link
Copy Markdown
Member

Thanks for identifying this gap and taking a first pass — the core claim was exactly right. Rather than iterate on the examples across review rounds while the guide is moving with the beta, we've landed the section directly in 525073c, incorporating your checklist line and subsection structure with the reworked examples from the review (paired setup/teardown in onSettled, plus the directive-factory form with onCleanup in the owned setup half). You're credited as co-author on the commit.

Note the docs-site side of this is tracked separately as solidjs/solid-docs#1630 — this repo's MIGRATION.md is the internal migration reference, so that issue still needs its own PR against the docs repo if you'd like to carry it there.

@ryansolid ryansolid closed this Aug 17, 2026
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.

2 participants