createEffect vs createEffect + on

What could be the reason that the first createEffect is triggered but not the second one using on? I feel there is something obvious I'm missing. Using solid-js 1.9.5
createEffect(() => {
console.log("effect", props.nearestPointConfig);
});

createEffect(
on(
() => props.nearestPointConfig,
(config) => {
console.log("effect 2", config);
createEffect(() => {
console.log("effect", props.nearestPointConfig);
});

createEffect(
on(
() => props.nearestPointConfig,
(config) => {
console.log("effect 2", config);
This is the output from the first console call:
[Log] effect (Video.tsx, line 95)
Proxy

handler: {get: function, has: function, set: function, deleteProperty: function, ownKeys: function, …}

target: {name: "P1-P2", areas: Array, Symbol(solid-proxy): Proxy, Symbol(store-node): Object}
[Log] effect (Video.tsx, line 95)
Proxy

handler: {get: function, has: function, set: function, deleteProperty: function, ownKeys: function, …}

target: {name: "P1-P2", areas: Array, Symbol(solid-proxy): Proxy, Symbol(store-node): Object}
The prop is a property of a store if that matters:
const [store, setStore] = createStore<GlobalStoreType>({
...
nearestPointConfig: undefined,
});
const [store, setStore] = createStore<GlobalStoreType>({
...
nearestPointConfig: undefined,
});
18 Replies
peerreynders
peerreynders2w ago
Any chance you could replicate it here? https://playground.solidjs.com/anonymous/d72eb037-7569-4225-b447-3faba1f7d58b I'm wondering whether it has something to do with how you modify the store. The first version will subscribe you to changes to anything along the path of the proxy necessary to get access to the final value. The second version will run the dependency function under the same circumstances but I believe that the effect function will only run when the config object reference changes.
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
lemonad
lemonadOP2w ago
Thank you for setting that up for me, @peerreynders. I tried to replicate it by using pretty much the setup I have but both createEffect executes here: https://playground.solidjs.com/anonymous/f6b82b77-944c-4aaa-9ef5-7f8cf3728864 Either I accidentally fixed something when replicating or there is something else in my setup that's causing it. It's a rather complex GUI application with a lot of signals. I'll keep investigating and will keep you posted. Thanks again for the help this far!
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
lemonad
lemonadOP2w ago
@peerreynders Oh, I think I found something now, even though it does not make a lot of sense. I have two different socket.io message handlers where I set the value of nearestPointConfig. If the first one updates the value, I get both "effect" and "effect 2" outputs but if the other one is doing the updating, I get only the "effect" output. The values I set look no different from each other.
lemonad
lemonadOP2w ago
@peerreynders Okay, I managed to replicate it now: https://playground.solidjs.com/anonymous/18509665-99be-4412-8209-3cb410f8a3c5 (updated link)
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
lemonad
lemonadOP2w ago
Console was cleared
effect undefined
effect 2 undefined
Timeout 1
effect {name: 'hello', areas: Array(1)}
effect 2 {name: 'hello', areas: Array(1)}
Timeout 2
effect {name: 'hello', areas: Array(1)}
Console was cleared
effect undefined
effect 2 undefined
Timeout 1
effect {name: 'hello', areas: Array(1)}
effect 2 {name: 'hello', areas: Array(1)}
Timeout 2
effect {name: 'hello', areas: Array(1)}
Hmm, it doesn't seem to happen in your playground so you might be on to something with having to do with how I update the values. If you get the chance, please take a look and see if there's something that is odd with that or the way I set up the context.
zulu
zulu2w ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
zulu
zulu2w ago
console.log does more than a prop access
lemonad
lemonadOP2w ago
@zulu Wow, thanks! For me it seems totally counterintuitive to do {...props.nearestPointConfig}, how did you come up with it?
zulu
zulu2w ago
spread operator in this case the spread is like a short cut for iterating over the object or even array keys/values top level
MDN Web Docs
Spread syntax (...) - JavaScript | MDN
The spread (...) syntax allows an iterable, such as an array or string, to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected. In an object literal, the spread syntax enumerates the properties of an object and adds the key-value pairs to the object being created.
zulu
zulu2w ago
also console.log in effects might need a warning in docs it is a source of discrepancies in the learning process https://discord.com/channels/722131463138705510/722131463889223772/1359995272637452378
lemonad
lemonadOP2w ago
@zulu Sorry, I meant destructuring was counterintuitive and was wondering how you came up with that as a solution Perhaps not the right term here but hopefully you understand what I meant : )
zulu
zulu2w ago
I just know the destruct access/read each key in the object you destruct, so each key is tracked similar to what console.log does. where it access all keys is that what you meant ?
lemonad
lemonadOP2w ago
Yes! Thank you for your help : )
zulu
zulu2w ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
lemonad
lemonadOP2w ago
Thanks for the example @zulu! I now understand why using console.log on a structure is not the best choice for debugging this : ) However, if you don't mind, what makes one update different from the other in my case? It seems I update the store in exactly the same way two times but only one works for both. Given your example, I could see my second createEffect never working but sometimes it does, which confuses me What is the takeaway here? That when I use a store, I should always track things by the leaves of the tree to be certain it works? I've recently implemented this global store to avoid passing props everywhere so in some regards it is different to how I use stores in other places. Perhaps I've just been lucky this far : )
peerreynders
peerreynders2w ago
If you make the following change:
export function SubComp(props: {
nearestPointConfig: NearestPointConfig | undefined;
}) {
createEffect((prev: NearestPointConfig | undefined) => {
const value = props.nearestPointConfig;
console.log('effect', value, value === prev);
return value;
}, undefined);

createEffect(
on(
() => props.nearestPointConfig,
(config) => {
console.log('effect 2', config);
}
)
);
return <div>SubComp</div>;
}
export function SubComp(props: {
nearestPointConfig: NearestPointConfig | undefined;
}) {
createEffect((prev: NearestPointConfig | undefined) => {
const value = props.nearestPointConfig;
console.log('effect', value, value === prev);
return value;
}, undefined);

createEffect(
on(
() => props.nearestPointConfig,
(config) => {
console.log('effect 2', config);
}
)
);
return <div>SubComp</div>;
}
You'll see this:
Timeout 1
effect {name: 'hello', areas: Array(1)} false
effect 2 {name: 'hello', areas: Array(1)}
Timeout 2
effect {name: 'hello', areas: Array(1)} true
Timeout 1
effect {name: 'hello', areas: Array(1)} false
effect 2 {name: 'hello', areas: Array(1)}
Timeout 2
effect {name: 'hello', areas: Array(1)} true
The first timeout replaced undefined with an entirely new object—therefore both effects fired. On the second timeout the first effect doesn't actually see a new object reference; it's identical to the last one—what it is reacting to is that the “guts” of the object have been replaced. The second effect only specifically is subscribed to changes to the object reference. Given that the object reference doesn't change on the second timeout the effect doesn't run. The thing that tends to be overlooked by newcomers is fine-grained reactivity. fine-grain change will not trigger coarse-grained reactivity. The way tracking works, you only subscribe to the exact value that you actually access. “You accessed this value last time so I'll run you again the next time that value changes”. With primitive values it very clear when the value changes. When it comes to objects and arrays however “the value” is the object reference. If you don't explicitly access their content under a tracked context then Solid's tracking doesn't know that you are interested in changes of the content.
zulu
zulu2w ago
key points: 1 when you set an object with another object using the store setter. there is a shallow merge
store, setStore = createStore(
{a:{"value":0}})
store, setStore = createStore(
{a:{"value":0}})
let a_before = store.a // {value:0}
setStore('a', {value:1})
let a_after = store.a // {"value":1}

a_before === a_after // true (same object)
let a_before = store.a // {value:0}
setStore('a', {value:1})
let a_after = store.a // {"value":1}

a_before === a_after // true (same object)
if you create an effect on store.a setting the store.a like we did above will not trigger. ( because the setter preforms a shallow merge which does not replace the value of store.a only a value change in the a key, will trigger a tracking of store.a 2 spreading allow the effect to track not just existing keys, but also new keys that may be added in the future. for example
createStore({a: {}}) // the object has no keys
createStore({a: {}}) // the object has no keys
createEffect(()=>{
({...store.a});
console.log("spread store.a")
});
createEffect(()=>{
({...store.a});
console.log("spread store.a")
});
if we set a new value in store.a now
setStore('a', {value:1}) // the "value" key was added
setStore('a', {value:1}) // the "value" key was added
we will see the effect rerun and print "spread store.a" there is still a shallow merge, but the ... force solid to track changes to the object itself. in other words it tracks all top level changes to the object we destructed/spread 3 setting a key of a store with an alternating types object TO non object {} => "" or non object TO object null => {} will trigger the effect, because there is no shallow merge between object and non object value like ( 1 and {} can not be merged ) so here the value of the key is replaced and the effect will re run
createEffect(()=>store.a)
createEffect(()=>store.a)
[store, _] = createStore({a: null})



// store.a => null

setStore('a', {value:0})

// store.a => {value:0}

// will trigger an effect on store.a
// because the store.a changed
[store, _] = createStore({a: null})



// store.a => null

setStore('a', {value:0})

// store.a => {value:0}

// will trigger an effect on store.a
// because the store.a changed
lemonad
lemonadOP2w ago
Thanks for the answer, it makes sense now. For me, the problem is probably not so much that I don't understand fine-grained reactivity (I've been using solid-js for a long time but still have many knowledge gaps). It is more that I don't know what I don't know in this case. I could very well have been blissfully unaware and lucky so things have just worked – and I've drawn the wrong conclusions from it : ) With signals, I think types such as { names: string, areas: AreaType[] } | undefined trigger effects when set and there is no deep, nested reactivity to consider. I hope : ) What I didn't like using lots of individual signals was to prop pass those around as much so I wanted a context and I chose a store for it. I figured as long as I always replaced e.g. store.a as a whole, I would get the same behavior as the previously used signals. Which does not seem to be the case. I need something in between a signal and a store : ) @zulu Thanks a lot for the detailed reply. It looks like the spread does exactly what i need, i.e. one level of reactivity – no more, no less.
createEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const x = { ...(props.nearestPointConfig ?? {}) };
createEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const x = { ...(props.nearestPointConfig ?? {}) };
Slightly OT: what I haven't yet figured out is how to get reasonable code using on with the same construct:
createEffect(
on(
{ ...props.nearestPointConfig },
(config: unknown) => { <-- !
createEffect(
on(
{ ...props.nearestPointConfig },
(config: unknown) => { <-- !
Since { ...undefined } gives me {}, I get an object with all members optional, which is not exactly what i want. It might seem like a weird design (and it could very well still be : ) The solid-js app speaks directly to an embedded device without internet access so for the most part there is one server and one client. The app provides real-time control for the operator and many of these messages arrive around 30 times a second. [Edit: See next message] Still, what I don't understand is why this only breaks in this specific case. I've got other places where I do the exact same thing and it seems to work fine. Is the underlying problem that I set the value in two different places? That is,
const onReceivedTargets = (msg: unknown, ack: undefined | (() => void)) => {
...
setTargets(result.data);
};
...
<SubComponent targets={globalStore.targets} />
const onReceivedTargets = (msg: unknown, ack: undefined | (() => void)) => {
...
setTargets(result.data);
};
...
<SubComponent targets={globalStore.targets} />
and then
createEffect(
on(
() => props.targets,
(targets) => {
createEffect(
on(
() => props.targets,
(targets) => {
works as expected. I'm still not sure I understand when it works and when it does not work. That is, I think I understand what's going on in the original case and the explanations make a lot of sense. Reading it, I would have thought that the above example should only trigger the effect when going from undefined to an object but not when replacing the object. Oh, targets is an array though. Perhaps that's the thing. I think I need to find all places where I trigger effects with on or similar where the effect is dependent on e.g. store.a and where a is an object. It might be just this single instance and that's why I haven't run into this earlier. Edit: No, it turns out to be different (I intercept it during prop passing and use a different signal downstream) – I think I have this down now. Thanks everyone : ) I have one other case where I have a type { max: number, min: number } | undefined where I update the store as originally posted. Which seems to trigger the following effect every time:
createEffect(
+ on(
+ () => props.trackingDepth,
+ (td) => {
+ console.log(td);
+ },
+ ),
+ );
createEffect(
+ on(
+ () => props.trackingDepth,
+ (td) => {
+ console.log(td);
+ },
+ ),
+ );
so there is something I'm still missing with the shallow merge. Really sorry if you've already answered it and I've missed it or not understood it.

Did you find this page helpful?