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
The runtime style() differ — used whenever the style value is not an inline object literal (a computed object, a constant, a ternary between objects, a prop, or the spread path) — has two defects producing three user-visible symptoms:
Explicit undefined never removes a property.style={boxStyle()} with boxStyle = () => ({ color: on() ? "red" : undefined }) leaves color: red applied forever: the differ calls setProperty(name, undefined), which is a CSSOM no-op. (The inline-literal form style={{ color: on() ? "red" : undefined }} happens to work because the compiler decomposes it into setStyleProperty calls, which do handle removal — so the bug surfaces exactly when the object comes from anywhere else.)
The differ mutates the user's previous style object with delete prev[s] (1.x diffed against an internal copy). Toggling between two constant objects A/B permanently deletes keys from them, so styles are silently lost on flip-back: A = { color: "red", "background-color": "yellow" } loses color on the first flip to B, and the element never gets red back.
Corollary of 2: when the differ re-runs with value === prev — which happens whenever any other dynamic binding is grouped into the same compiled template effect, i.e. the exact setup of Using imported object for style attribute is reevaluated if other attribute uses updated signal #1938 — the delete prev[s] loop deletes every key of the object while iterating it, emptying the user's constant object in one pass without touching the DOM. All its styles are then lost on the next real update.
Related to open #1938: same style() function and same trigger shape (another binding's signal re-runs the shared effect), but in 1.x the consequence was only a spurious re-evaluation — in 2.0 it destroys the user's object (symptom 3).
All three symptoms are regressions from 1.x, verified on 1.9.14: explicit undefined removes the property, and toggling/shared-effect re-runs never mutate the user's objects (1.x style() accumulates the applied declarations in an internal object it returns and threads back as prev; the user's values are only ever read).
import{createSignal,flush}from"solid-js";// constant style objects, defined once (shared/imported styles pattern)constA={color: "red","background-color": "yellow"};constB={color: "green"};// (1) explicit `undefined` value never removes the propertyfunctionBox1(){const[on,setOn]=createSignal(true);const[report,setReport]=createSignal("");letbox!: HTMLDivElement;// computed style object — hits the runtime style() differ// (an inline literal is decomposed by the compiler and unaffected)constboxStyle=()=>({color: on() ? "red" : undefined});return(<section><divref={box}style={boxStyle()}>box1</div><buttononClick={()=>{setOn(!on());flush();setReport(`on=${on()} — inline color: "${box.style.color}"`);}}>
toggle box1 color
</button><pre>{report()}</pre></section>);}// (2) the differ mutates the constant objects; styles lost on flip-backfunctionBox2(){const[useB,setUseB]=createSignal(false);const[report,setReport]=createSignal("");letbox!: HTMLDivElement;return(<section><divref={box}style={useB() ? B : A}>box2</div><buttononClick={()=>{setUseB(!useB());flush();setReport(`using ${useB() ? "B" : "A"} — inline color: "${box.style.color}", `+`background: "${box.style.backgroundColor}"\nA = ${JSON.stringify(A)}`);}}>
toggle box2 A/B
</button><pre>{report()}</pre></section>);}// (3) any other dynamic binding in the same template effect empties a// constant style object without touching the DOMfunctionBox3(){constC={color: "red","background-color": "yellow"};const[n,setN]=createSignal(0);const[report,setReport]=createSignal("");return(<section><divtitle={`count ${n()}`}style={C}>box3</div><buttononClick={()=>{setN(n()+1);flush();setReport(`C is now ${JSON.stringify(C)}`);}}>
bump box3 title attr
</button><pre>{report()}</pre></section>);}exportdefaultfunctionApp(){return(<><Box1/><Box2/><Box3/></>);}
Steps to Reproduce the Bug or Issue
On load, box1 is red (cssText = color: red;), box2 is red on yellow.
Click "toggle box1 color". Actual — the color is never removed:
on=false — inline color: "red"
Click "toggle box2 A/B" (A → B). The DOM looks right at this point, but constant A has already lost its color key:
using B — inline color: "green", background: ""
A = {"background-color":"yellow"}
Click "toggle box2 A/B" again (B → A). Actual — red is gone forever; box2 is now just a yellow box:
using A — inline color: "", background: "yellow"
A = {"background-color":"yellow"}
Click "bump box3 title attr" (only the title attribute's signal changes). Actual — the constant style object is emptied in place:
C is now {}
Expected behavior
Diffing must treat a nullish value as removal and must never write to user-owned objects:
step 2: on=false — inline color: "" (property removed)
step 3: using B — inline color: "green", background: ""
A = {"color":"red","background-color":"yellow"} (A untouched)
step 4: using A — inline color: "red", background: "yellow" (flip-back restores A fully)
step 5: C is now {"color":"red","background-color":"yellow"} (C untouched)
Screenshots or Videos
No response
Platform
OS: macOS
Browser: Chrome
Version: 2.0.0-beta.15 (verified at next HEAD bad66625; 1.x comparison on solid-js 1.9.14)
Additional context
Root cause is style() in node_modules/dom-expressions/src/client.js:247-263 (source repo: ryansolid/dom-expressions, src/client.js); the same function serves the spread() path:
prev is the raw previous value the user passed (the compiled effect threads it through), not an internal copy as in 1.x. When value === prev (shared-effect re-run, symptom 3), delete prev[s] deletes the keys of the object being iterated, emptying it.
Suggested fix direction: diff against a private shallow copy of the previously applied declarations (kept in the effect state or on the element) instead of the user's object; route nullish new values through removeProperty in the first loop; never delete from, or otherwise write to, the incoming objects.
Describe the bug
The runtime
style()differ — used whenever thestylevalue is not an inline object literal (a computed object, a constant, a ternary between objects, a prop, or thespreadpath) — has two defects producing three user-visible symptoms:undefinednever removes a property.style={boxStyle()}withboxStyle = () => ({ color: on() ? "red" : undefined })leavescolor: redapplied forever: the differ callssetProperty(name, undefined), which is a CSSOM no-op. (The inline-literal formstyle={{ color: on() ? "red" : undefined }}happens to work because the compiler decomposes it intosetStylePropertycalls, which do handle removal — so the bug surfaces exactly when the object comes from anywhere else.)delete prev[s](1.x diffed against an internal copy). Toggling between two constant objectsA/Bpermanently deletes keys from them, so styles are silently lost on flip-back:A = { color: "red", "background-color": "yellow" }losescoloron the first flip toB, and the element never gets red back.value === prev— which happens whenever any other dynamic binding is grouped into the same compiled template effect, i.e. the exact setup of Using imported object for style attribute is reevaluated if other attribute uses updated signal #1938 — thedelete prev[s]loop deletes every key of the object while iterating it, emptying the user's constant object in one pass without touching the DOM. All its styles are then lost on the next real update.Related to open #1938: same
style()function and same trigger shape (another binding's signal re-runs the shared effect), but in 1.x the consequence was only a spurious re-evaluation — in 2.0 it destroys the user's object (symptom 3).All three symptoms are regressions from 1.x, verified on 1.9.14: explicit
undefinedremoves the property, and toggling/shared-effect re-runs never mutate the user's objects (1.xstyle()accumulates the applied declarations in an internal object it returns and threads back asprev; the user's values are only ever read).Your Example Website or App
https://stackblitz.com/edit/solidjs-templates-e5b4bufb?file=src%2FApp.tsx
Steps to Reproduce the Bug or Issue
cssText=color: red;), box2 is red on yellow.Ahas already lost itscolorkey:titleattribute's signal changes). Actual — the constant style object is emptied in place:Expected behavior
Diffing must treat a nullish value as removal and must never write to user-owned objects:
Screenshots or Videos
No response
Platform
nextHEADbad66625; 1.x comparison on solid-js 1.9.14)Additional context
Root cause is
style()innode_modules/dom-expressions/src/client.js:247-263(source repo: ryansolid/dom-expressions,src/client.js); the same function serves thespread()path:previs the raw previous value the user passed (the compiled effect threads it through), not an internal copy as in 1.x. Whenvalue === prev(shared-effect re-run, symptom 3),delete prev[s]deletes the keys of the object being iterated, emptying it.Suggested fix direction: diff against a private shallow copy of the previously applied declarations (kept in the effect state or on the element) instead of the user's object; route nullish new values through
removePropertyin the first loop; neverdeletefrom, or otherwise write to, the incoming objects.