Skip to content

[2.0 rc.15 + next] For row lifecycle cleanup inherits the list computation and halts on a shared-state write #4165

Closed

Description

@ethan-huo

What happens

Removing a row from a keyed For runs that row's lifecycle cleanup under the list's owned computation. An ordinary setter from the cleanup throws REACTIVE_WRITE_IN_OWNED_SCOPE, followed by REACTIVITY_HALTED. The row remains mounted and subsequent list updates are ignored. The identical cleanup unmounted by Show succeeds.

This reproduces with the documented component lifecycle form, onSettled(() => () => setCount(n => n + 1)), and with low-level onCleanup. It uses no async source, router, external UI library, SSR or hydration.

Minimal shape

import { render } from '@solidjs/web';
import { createSignal, flush, For, onSettled } from 'solid-js';

const [rows, setRows] = createSignal([1, 2]);
const [count, setCount] = createSignal(0);
function Row() {
  onSettled(() => () => setCount(n => n + 1));
  return <i>row</i>;
}
render(() => <For each={rows()}>{() => <Row />}</For>, document.body);
flush();
setRows([2]);
flush(); // development: owned-write error, then reactivity halted

Expected: one row remains, the release count becomes 1, and a later append works. An internal control-flow computation should not become the lifecycle callback's write context.

Executable case and version evidence

Fixed conformance case 058, including source, desired-behavior assertions and Show/ownedWrite controls.

After its frozen install, run:

TARGET=rc15 bun test ./findings/058-list-cleanup-write/repro.test.ts
  • Published core/web/signals/compiler 2.0.0-rc.15 (4937f0a2bfddd45a65738c189075aff577530073): both For cleanup variants fail in development; Show and ownedWrite controls pass.
  • Source-built current next at 2172a37687e92dd91758476423f08cb4a12bcb0c: the same development failure.
  • Current HEAD production: the disposal/control cases pass because the owned-write guard is development-only. This report does not claim a production crash.
  • Real system Google Chrome; native compiler; plain public JSX with no type bypass.

Raw versioned receipts and logs.

Source boundary and candidate repair

packages/signals/src/map.ts disposes removed row owners inside the list recomputation. core/owner.ts calls registered cleanups and final effect-returned cleanups directly, inheriting that ambient context. The setter guard then treats the cleanup as a computation write.

A local source experiment runs just the actual cleanup callbacks through runWithOwner(null, callback), restoring the caller's context/tracking afterward. It does not relax the setter guard or add a disposal-depth exemption. The original browser repro becomes green; ordinary computation writes and a computation nested inside cleanup remain rejected. Using the same Node/Vitest 4.1.6 runner, the upstream signals suite is 323 files / 5387 passes / 3 expected failures / 2 skips before and after. This is a tested direction, not a claim that the repair is upstream-reviewed.

Dedupe and application impact

Searched open and closed issues in solid, solid-router and solid-start. #4089 is cleanup rejoining an already-landed transaction; #3601 is reentrant double cleanup. This case needs neither a transaction nor reentrant disposal.

The real application trigger was deleting a row from its own open menu: dismissable-layer teardown publishes shared state to still-mounted UI. It currently requires deferred writes or ownedWrite declarations in library internals merely to survive list removal.

Activity

  1. ryansolid commented on Oct 20, 2026

    @ryansolid
    Member

    Fixed in #4168, merged to next as a2743fa. A cleanup's signal, store or refresh write is now allowed while an owner is torn down, so removing a <For> row whose onCleanup or onSettled cleanup calls a setter no longer trips REACTIVE_WRITE_IN_OWNED_SCOPE. Cleanups keep their owner and context. It will ship in the next RC.

    — Claude via Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions