You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
createRoot's dispose function keeps fn alive, so <For>/<Index> rows retain removed list items #4095
That scope also holds fn, because updateFn closes over it. So anything that keeps the dispose function also keeps fn, and everything fn closes over.
mapArray and indexArray keep every row's dispose function in disposers. Every row's fn is mapper, and mapper closes over newItems, the list that was current when the row was created. So a mounted row keeps that whole list reachable, including items that were removed from the list later.
We found this in production. A long-lived <For> row kept a list it was created from, and with it a replaced document of a few hundred MB.
// npm i solid-js@1.9.17// node --expose-gc --conditions=browser repro.mjsimport{createRoot,createSignal,createMemo,mapArray,indexArray}from"solid-js";constcollect=async()=>{// WeakRef targets stay alive until the current job ends.awaitnewPromise(r=>setTimeout(r,0));globalThis.gc();};// A row created from [kept, removed] stays mounted after the list becomes [kept].asyncfunctioncheck(name,map){constkept={name: "kept"};letremoved,setList;constdispose=createRoot(dispose=>{const[list,set]=createSignal((()=>{constitem={name: "removed"};removed=newWeakRef(item);return[kept,item];})());setList=set;createMemo(map(list))();returndispose;});setList([kept]);awaitcollect();console.log(`${name}: removed item still reachable:`,removed.deref()!==undefined);dispose();}awaitcheck("mapArray (<For>)",list=>mapArray(list,item=>item.name));awaitcheck("indexArray (<Index>)",list=>indexArray(list,item=>item().name));
Output:
mapArray (<For>): removed item still reachable: true
indexArray (<Index>): removed item still reachable: true
The dev build (dist/dev.js) gives the same output, and so does Bun (JavaScriptCore) with Bun.gc(true).
Expected behavior
Once the row for removed is disposed, nothing should keep removed reachable, so both lines should print false. They do once the dispose function is created outside createRoot's scope, in both V8 and JavaScriptCore.
Platform
solid-js: 1.9.17 (main)
Node 22.23 (V8), Bun 1.4.2 (JavaScriptCore)
Additional context
I haven't checked next (2.0) yet. The fix for 1.x is in the PR linked below.
Activity
fix(signals): createRoot's dispose function no longer retains init (#…
fix(signals): createRoot's dispose function no longer retains init (#…
fix(signals): createRoot's dispose function no longer retains init (#…