#createEffect vs createEffect + on

30 messages · Page 1 of 1 (latest)

boreal patrol
#

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);

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}

The prop is a property of a store if that matters:

const [store, setStore] = createStore<GlobalStoreType>({
...
nearestPointConfig: undefined,
});
harsh cliff
#

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.

boreal patrol
#

Thank you for setting that up for me, @harsh cliff. 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!

boreal patrol
#

@harsh cliff 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.

#
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.

sage pawn
#

console.log does more than a prop access

boreal patrol
#

@zulu Wow, thanks! For me it seems totally counterintuitive to do {...props.nearestPointConfig}, how did you come up with it?

sage pawn
# boreal patrol @zulu Wow, thanks! For me it seems totally counterintuitive to do `{...props.nea...

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

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.

#

also console.log in effects might need a warning in docs it is a source of discrepancies in the learning process

#general message

boreal patrol
#

@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 : )

sage pawn
boreal patrol
#

Yes! Thank you for your help : )

sage pawn
boreal patrol
#

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 : )

harsh cliff
# boreal patrol Thanks for the example @zulu! I now understand why using `console.log` on a stru...

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>;
}

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

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.

harsh cliff
# boreal patrol What is the takeaway here? That when I use a store, I should always track things...

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.

sage pawn
# boreal patrol Thanks for the example @zulu! I now understand why using `console.log` on a stru...

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}})
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
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

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)
[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 

boreal patrol
# harsh cliff The thing that tends to be overlooked by newcomers is *fine-grained reactivity*....

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 : )

boreal patrol
#

@sage pawn 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 ?? {}) };

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) => { <-- !

Since { ...undefined } gives me {}, I get an object with all members optional, which is not exactly what i want.

boreal patrol
#

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} />

and then

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.