Broken reactivity @ `vite-plugin-solid` with `{dev:false}`, but works fine with `{dev:true}`.

Context - I'm using render from solid-js/web to render content for an Obsidian extension. - Reactivity is breaking for the Icon component from @iconify-icon/solid, but the issue is not present when {dev:true} is set in vite-plugin-solid. - I tried troubleshooting this in a minimal StackBlitz Project, but was not able to reproduce the issue.
24 Replies
enteleform
enteleform3w ago
(left side:{dev:true}, right side:{dev:false}) This is the excerpt of code that's controlling the icon. hasTag and sessionContext are accessors that are managed via xstate actors.
const playButton = (() => (
hasTag("Can_START")
? {
event: {
type: "START",
autoAdvance: sessionContext().autoAdvance,
autoStart: sessionContext().autoStart_Timer,
},
icon: (
sessionContext().autoAdvance
? "mdi:play-speed"
: "iconoir:play-solid"
),
}
: {
event: {type:"PAUSE"},
icon: "iconoir:pause-solid"
}
) satisfies {event:Button["Props"]["event"], icon:string})

return (
<div class="Controls State -Row">
<Button event={playButton().event} {...{actorContext}}> <Icon icon={playButton().icon}/> </Button>
<Button event={{type:"RESET"} } {...{actorContext}}> <Icon icon="fa-solid:undo" /> </Button>
</div>
)
const playButton = (() => (
hasTag("Can_START")
? {
event: {
type: "START",
autoAdvance: sessionContext().autoAdvance,
autoStart: sessionContext().autoStart_Timer,
},
icon: (
sessionContext().autoAdvance
? "mdi:play-speed"
: "iconoir:play-solid"
),
}
: {
event: {type:"PAUSE"},
icon: "iconoir:pause-solid"
}
) satisfies {event:Button["Props"]["event"], icon:string})

return (
<div class="Controls State -Row">
<Button event={playButton().event} {...{actorContext}}> <Icon icon={playButton().icon}/> </Button>
<Button event={{type:"RESET"} } {...{actorContext}}> <Icon icon="fa-solid:undo" /> </Button>
</div>
)
While debugging, I tried rendering the playButton().icon string directly instead of passing it to Icon, and it updates the DOM as expected.
// changed this:
<Icon icon={playButton().icon}/>

// to this:
{playButton().icon}
// changed this:
<Icon icon={playButton().icon}/>

// to this:
{playButton().icon}
enteleform
enteleform3w ago
enteleform
enteleform3w ago
Is this a Solid issue, or an Iconify issue?
REEEEE
REEEEE3w ago
looks like an issue with iconify because the props are destructured https://github.com/iconify/iconify/blob/978ba328940e7749e60a69556b885addfedbb74b/iconify-icon/solid/src/iconify.tsx#L90 although, they are spreading the props later
enteleform
enteleform3w ago
Just forked it, gonna see if changing that resolves the issue. Any idea why it would have the issue in {dev:false} mode but not {dev:true} ?
REEEEE
REEEEE3w ago
Not too sure tbh, it could be that {dev:true} force rerenders
enteleform
enteleform3w ago
No luck, got rid of the destructuring and the issue is still occurring. Updated Source Code
export function Icon(props: IconifyIconProps): JSX.Element {
const icon = () =>
typeof props.icon === 'object'
? JSON.stringify(props.icon)
: props.icon

return (
<iconify-icon
{...props}
attr:icon={icon()}
attr:mode={props.mode}
attr:inline={props.inline}
attr:rotate={props.rotate}
attr:flip={props.flip}
attr:width={props.width}
attr:height={props.height}
attr:preserveAspectRatio={props.preserveAspectRatio}
attr:noobserver={props.noobserver}
/>
);
}
export function Icon(props: IconifyIconProps): JSX.Element {
const icon = () =>
typeof props.icon === 'object'
? JSON.stringify(props.icon)
: props.icon

return (
<iconify-icon
{...props}
attr:icon={icon()}
attr:mode={props.mode}
attr:inline={props.inline}
attr:rotate={props.rotate}
attr:flip={props.flip}
attr:width={props.width}
attr:height={props.height}
attr:preserveAspectRatio={props.preserveAspectRatio}
attr:noobserver={props.noobserver}
/>
);
}
Bundled Output
function Icon(props) {
const icon = () => typeof props.icon === "object" ? JSON.stringify(props.icon) : props.icon;
return (
(() => {
var _el$ = _tmpl$$b();
spread$1(_el$, mergeProps$1(props, {
get icon() {
return icon();
}
}), false);
_el$._$owner = getOwner();
createRenderEffect$1((_p$) => {
var _v$ = icon(), _v$2 = props.mode, _v$3 = props.inline, _v$4 = props.rotate, _v$5 = props.flip, _v$6 = props.width, _v$7 = props.height, _v$8 = props.preserveAspectRatio, _v$9 = props.noobserver;
_v$ !== _p$.e && setAttribute$1(_el$, "icon", _p$.e = _v$);
_v$2 !== _p$.t && setAttribute$1(_el$, "mode", _p$.t = _v$2);
_v$3 !== _p$.a && setAttribute$1(_el$, "inline", _p$.a = _v$3);
_v$4 !== _p$.o && setAttribute$1(_el$, "rotate", _p$.o = _v$4);
_v$5 !== _p$.i && setAttribute$1(_el$, "flip", _p$.i = _v$5);
_v$6 !== _p$.n && setAttribute$1(_el$, "width", _p$.n = _v$6);
_v$7 !== _p$.s && setAttribute$1(_el$, "height", _p$.s = _v$7);
_v$8 !== _p$.h && setAttribute$1(_el$, "preserveaspectratio", _p$.h = _v$8);
_v$9 !== _p$.r && setAttribute$1(_el$, "noobserver", _p$.r = _v$9);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0,
i: void 0,
n: void 0,
s: void 0,
h: void 0,
r: void 0
});
return _el$;
})()
);
}
function Icon(props) {
const icon = () => typeof props.icon === "object" ? JSON.stringify(props.icon) : props.icon;
return (
(() => {
var _el$ = _tmpl$$b();
spread$1(_el$, mergeProps$1(props, {
get icon() {
return icon();
}
}), false);
_el$._$owner = getOwner();
createRenderEffect$1((_p$) => {
var _v$ = icon(), _v$2 = props.mode, _v$3 = props.inline, _v$4 = props.rotate, _v$5 = props.flip, _v$6 = props.width, _v$7 = props.height, _v$8 = props.preserveAspectRatio, _v$9 = props.noobserver;
_v$ !== _p$.e && setAttribute$1(_el$, "icon", _p$.e = _v$);
_v$2 !== _p$.t && setAttribute$1(_el$, "mode", _p$.t = _v$2);
_v$3 !== _p$.a && setAttribute$1(_el$, "inline", _p$.a = _v$3);
_v$4 !== _p$.o && setAttribute$1(_el$, "rotate", _p$.o = _v$4);
_v$5 !== _p$.i && setAttribute$1(_el$, "flip", _p$.i = _v$5);
_v$6 !== _p$.n && setAttribute$1(_el$, "width", _p$.n = _v$6);
_v$7 !== _p$.s && setAttribute$1(_el$, "height", _p$.s = _v$7);
_v$8 !== _p$.h && setAttribute$1(_el$, "preserveaspectratio", _p$.h = _v$8);
_v$9 !== _p$.r && setAttribute$1(_el$, "noobserver", _p$.r = _v$9);
return _p$;
}, {
e: void 0,
t: void 0,
a: void 0,
o: void 0,
i: void 0,
n: void 0,
s: void 0,
h: void 0,
r: void 0
});
return _el$;
})()
);
}
REEEEE
REEEEE3w ago
The issue is only for the Icon component specifically right?
enteleform
enteleform3w ago
I think so, I haven't noticed it anywhere else. It works fine in the StackBlitz example though, so I'm not sure what aspects of [Obsidian, Solid, Vite, Iconify, etc.] are playing into the issue in the actual production environment.
REEEEE
REEEEE3w ago
Could be an issue with the component being a web component and not playing well with the setup
mdynnl
mdynnl3w ago
not sure what's your exact setup but it seems to be working in Obsidian 1.6.3 can't figure out dev setup though 😆
enteleform
enteleform2w ago
Thanks for the demo. I just replicated it and can confirm that a barebones extension implementation works as expected. However, running the exact same code in my actual extension does not work. Seems like I might have to debug my Vite config... can't think of what sort of thing might cause that issue off the top of my head though 🤔.
enteleform
enteleform2w ago
enteleform
enteleform2w ago
Also, just learned about the Iconify IntelliSense VS Code plugin from your setup 🔥. what do you mean @ can't figure out dev setup? like hot reloading?
mdynnl
mdynnl2w ago
(left side:{dev:true}, right side:{dev:false})
i assumed you were using solid plugin's hmr somehow i do have this plugin also here interesting the attribute doesn't even update though
enteleform
enteleform2w ago
my current dev workflow is to just manually run a full build whenever I'm ready to test some changes, haven't been using a build-watch setup. i ended up finding that {dev:true} resolved the issue either through trial and error or might have seen a recommendation about it while researching the issue. before that, I didn't have that option in my config at all.
mdynnl
mdynnl2w ago
could you try with directly call the icon component like Icon({ icon: playButton().icon })?
enteleform
enteleform2w ago
that does work, although it also results in flashing every time something in the dependency chain updates rather than only when the final value is changed. e.g. on every timer tick.
enteleform
enteleform2w ago
mdynnl
mdynnl2w ago
yeah, that would happen it's really weird this doesn't work also if you'd like to try
import 'iconify-icon';
import type { IconifyIconProperties } from 'iconify-icon';
type BaseElementProps = JSX.IntrinsicElements['span'];
export interface IconifyIconProps
extends BaseElementProps,
IconifyIconProperties {
rotate?: string | number;
}

export function Icon(props: IconifyIconProps) {
return (
// @ts-ignore
<iconify-icon {...props} />
);
}
import 'iconify-icon';
import type { IconifyIconProperties } from 'iconify-icon';
type BaseElementProps = JSX.IntrinsicElements['span'];
export interface IconifyIconProps
extends BaseElementProps,
IconifyIconProperties {
rotate?: string | number;
}

export function Icon(props: IconifyIconProps) {
return (
// @ts-ignore
<iconify-icon {...props} />
);
}
or even this
import 'iconify-icon';

import type { IconifyIconProperties } from 'iconify-icon';
type BaseElementProps = JSX.IntrinsicElements['span'];
export interface IconifyIconProps
extends BaseElementProps,
IconifyIconProperties {
rotate?: string | number;
}

declare module 'solid-js' {
namespace JSX {
interface IntrinsicElements {
'iconify-icon': IconifyIconProps;
}
}
}

<iconify-icon width={20} icon="iconoir:pause-solid" />;
import 'iconify-icon';

import type { IconifyIconProperties } from 'iconify-icon';
type BaseElementProps = JSX.IntrinsicElements['span'];
export interface IconifyIconProps
extends BaseElementProps,
IconifyIconProperties {
rotate?: string | number;
}

declare module 'solid-js' {
namespace JSX {
interface IntrinsicElements {
'iconify-icon': IconifyIconProps;
}
}
}

<iconify-icon width={20} icon="iconoir:pause-solid" />;
enteleform
enteleform2w ago
both of those work 👍 yeh I guess I'll include that as a lib wrapper in my app, thanks for your help with the workarounds! kinda bugs me that I couldn't track down the root of the issue though... just ran safe-stringify on my final vite config (it's built dynamically with a few modules I have for Vite, Solid, Obsidian, etc.) and don't see any glaring issues.
mdynnl
mdynnl2w ago
feel free to provide extra code, best if it can be copy/pasted to test 😆
enteleform
enteleform2w ago
could be tough to assemble a 1:1 repro, kinda have like a personal framework of modules that are all contributing a bit to the final output. might take a shot at it when I get a chance. i did just run into something when trying to implement your Iconify wrapper though. it worked fine when it was in the source code of the Obsidian extension where I'm running into the issue. then I tried moving it to an external module so that I can use it across all of my projects, and the same issue started occurring as with @iconify-icon/solid. that at least narrows it down to something to do with module-sourced components and/or the build process of said modules or the extension itself...