Coder Social home page Coder Social logo

Comments (14)

maciek50322 avatar maciek50322 commented on June 16, 2024 1

Your statement

I would expect that if we were to set a value inside of object this value would propagate under all circumstances
into the store, but there is one where it doesn't.

holds true only within reactive context.

Let me explain

Outside reactive context the values don't "propagate" on change, might seem like it, but it is just reading what reference points to, e.g.

let object = { prop: "value" };
function logProperty() {
  console.log(object.prop);
}
logProperty(); // logs "value"
object.prop = "new value";
logProperty(); // logs "new value"

Here function logProperty didn't track changes for object.prop and didn't get notified that object.prop has changed, but still logged newest value.

The reactive context allows us to trigger different behaviors when the tracked value changes. This means, that we don't need to manually call logProperty, we can define behaviour that reacts to changing a value.

const [object, setObject] = createStore({ prop: "value" });
createEffect(function logProperty() {
  console.log(object.prop);
}); // logs "value" (in case we wouldn't call setObject immediately after)
setObject("prop", "new value"); // logs "new value"

Here the createEffect does track changes for object.prop and gets called when it changes. This is possible, because now object is defined as a trackable value and through provided interface (setObject) we notify the effect that we changed the value. This notification wouldn't happen if we didn't use the store interface. I mean the ❌ object.prop = "new value" ❌ wouldn't trigger anything (except maybe eslint 😉, but that's good, because we didn't create trackable to not trigger changes).

Back to your issue

In your example

let object = {};
const [store, setStore] = createStore(object)
setStore('fake item', 'fake id');
object['fake item'] = createUniqueId();

console.log(store["fake item"] === object["fake item"]) // logs false

Changing the property of object, the object['fake item'] = createUniqueId();, doesn't trigger anything, because it isn't trackable value. Even though the store is dependent on it, this change doesn't cause store to change. It might seem like it changed something after you unwrapped store, but unwrapping returns the reference to original object, so it reads original object at given time. It might seem like it changed something also when you don't unwrap, but at the same time don't use setStore, because then the store uses direct reference to original object, only after using setStore it becomes more deeply independent.

Other way around if you used only setStore and not object['fake item'] = createUniqueId();, the original object can change (and it does), because store holds reference to original object and it can change its' properties.

let object = {};
const [store, setStore] = createStore(object)
setStore('fake item', 'fake id');

console.log(store["fake item"] === object["fake item"]) // logs true

Then also the properties of unwrapped store wouldn't differ from properties of store, because with setStore you change original object and the store.

Changing only original object doesn't change the store.

Solution

I am not sure why you need to control original object outside store AND the store separately, and also not sure why you need reactivity here (your sandbox example doesn't show anything reactive), so

  • if you don't need to control original object outside store, then you don't need to hold it
    const [store, setStore] = createStore({});
    setStore("prop", 123);
    unwrap(store); // when you need to read original for some reason
  • if you don't need reactivity, you can just use plain object
    let object = {};
    object.prop = 123;
  • if you need to control moreless same object with 2 interfaces with reactivity, you can define original object as trackable (I would treat it as a fun fact, I think this always can be restructured to one of the solutions above)
    const [object, setObject] = createStore({});
    const [store, setStore] = createStore(object);
    setStore("prop", 123); // changes object and store
    setObject("prop", 123); // changes object and store

from solid.

maciek50322 avatar maciek50322 commented on June 16, 2024 1

And for the other issue do you mean something like this?
https://playground.solidjs.com/anonymous/4d348a1d-5ee3-45d4-90f6-c9bc72c35e5f
createComputed sets component color when instance color changes, but when component color changes instance color stays the same

Yeah that could also be done through an effect, the thing is that this gets really unmaintanable once the objects get big and there are many objects. But I actually meant for it to be done the other way around, so the instance changes when the component changes. But also, if the instance changes and then the component changes the instance shouldn't change anymore for only the changed property.

Well it does get harder to maintain if you need to take care of each property separately, but there's always a way if you want to treat all of them the same way, so something like iterating through all properties should do the work. Also there's something like createReaction that can trigger only once dependency changes.

There are actually much more handy utilities in docs, take your time to get to know them. Even though your case is VERY specific it's possible to do. I managed to do something you described, but there are many ways to implement this. Here's one example
https://playground.solidjs.com/anonymous/494d6068-6e82-4615-81c0-db40ebe9b49d
You can change instance as many times as you want, but once you change component, you can change instance only with component.

Back to main topic, what you described originally is probably not a bug. If you have problems with understanding library or need some tips official discord might be a better place for this disccusion.

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

Feeling like this might be the culprit:

export function setProperty(
state: StoreNode,
property: PropertyKey,
value: any,
deleting: boolean = false
): void {
if (!deleting && state[property] === value) return;
const prev = state[property],
len = state.length;
if ("_SOLID_DEV_")
DevHooks.onStoreNodeUpdate && DevHooks.onStoreNodeUpdate(state, property, value, prev);
if (value === undefined) {
delete state[property];
if (state[$HAS] && state[$HAS][property] && prev !== undefined) state[$HAS][property].$();
} else {
state[property] = value;
if (state[$HAS] && state[$HAS][property] && prev === undefined) state[$HAS][property].$();
}
let nodes = getNodes(state, $NODE),
node: DataNode | undefined;
if ((node = getNode(nodes, property, prev))) node.$(() => value);
if (Array.isArray(state) && state.length !== len) {
for (let i = state.length; i < len; i++) (node = nodes[i]) && node.$();
(node = getNode(nodes, "length", len)) && node.$(state.length);
}
(node = nodes[$SELF]) && node.$();
}

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

Hey @maciek50322, thanks for taking the time to look into this!

Changing the property of object, the object['fake item'] = createUniqueId();, doesn't trigger anything, because it isn't trackable value. Even though the store is dependent on it, this change doesn't cause store to change. It might seem like it changed something after you unwrapped store, but unwrapping returns the reference to original object, so it reads original object at given time. It might seem like it changed something also when you don't unwrap, but at the same time don't use setStore, because then the store uses direct reference to original object, only after using setStore it becomes more deeply independent.

I don't exactly want it to trigger anything, the way I have things setup is that I want to defineProperty into the target object with a getter
that returns a reactive value (i.e. a signal or a store), so that it would be reactive to that value, kind of like what Solid already does from your doubly wrapped store solution. The problem
is that even with the defineProperty in the original object it won't update, but only after the set on the store.

So, this example of me setting just the value in the object is just to get a more minimal reproduction, not necessarily my use case.

Changing only original object doesn't change the store.

I've noticed it hasn't but it only does in this case, the thing is that this behavior doesn't seem to be necessarily intended, just what is right now as it's never been an issue, so
my thought is that this could be probably circumvented here in the code. Thinking here that, as the set for the store
will change the target object, it could also keep the state of the object in sync if necessary.

From your solution:

if you need to control moreless same object with 2 interfaces with reactivity, you can define original object as trackable (I would treat it as a fun fact, I think this always can be restructured to one of the solutions above)

The problem with that is that the getters from the original store can't really be reset back again. Only if I were able to Object.defineProperty into the store again then it would work. My use case demands that I'm able to have the value
and then go back to the getter if I intend to switching back-and-forth.


A solution would probably work completely if I could programmatically add a getter into the store, I could just set the value,
but I do need a getter, so not sure if something like the following would work.

const [store, setStore] = createStore({  });
setStore('key', '1234');
setStore(produce(value => Object.defineProperty(value, 'key', { get: () => {...}, enumerable: true, configurable: true; })

This illustrates a bit more on my problem here as AFAIK I can only set this getter programmatically in the original object,
and if I do on the store not sure how the Proxy wrapped object would behave.

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

Another problem with the last solution you provided is that both of them are in sync, whereas I want only the second store to be in sync with the first while the first is independent of the second. Does that make sense? Just a one-way data flow so that things are immutable if I need to, and mutable, by only changing, on the second store if I need to as well.

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

I'm still a long way from understanding the code for the setter in the store, but as far as I understand, once I set the value Solid starts (no pun intended) returning the value of a signal on the property's getter, and then, that would be the reason the signal gets out of sync with the original object's property. The only reason that the target object updates when I do set is that the target object is in sync after only being set to the store is because Solid also updates it when setting.

This would be fine if I could either, define a getter programatically, or delete the signal manaully so that it just returns the value of the target when I know I need to. These would also probably be better solutions than what I'm trying right now.

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

Another problem with the last solution you provided is that both of them are in sync, whereas I want only the second store to be in sync with the first while the first is independent of the second. Does that make sense? Just a one-way data flow so that things are immutable if I need to, and mutable, by only changing, on the second store if I need to as well.

@maciek50322 I also made a playground with a minimal version of my use case if you want to try out any solutions

https://playground.solidjs.com/anonymous/97674bdd-bcfe-49de-8cf5-83144de079fb

Thanks again for taking the time to help me out on this :)

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

This would be fine if I could either, define a getter programmatically

Tried many things for this, but none of them worked. Here are two of them:

  • const [signal, setSignal] = createSignal('this is not John');
    const [store, setStore] = createStore({});
    setStore('name', 'John');
    Object.defineProperty(store, 'name', {
      get() {
        return signal();
      },
      configurable: true,
      enumerable: true
    });
    expect(store.name).toBe('this is not John') // fails
  • const [signal, setSignal] = createSignal('this is not John');
    const [store, setStore] = createStore({});
    setStore('name', 'John');
    setStore(reconcile({
      get name() {
        return signal();
      }
    }, { merge: true }));
    expect(store.name).toBe('this is not John') // seems to work
    setSignal("maybe it is John");
    expect(store.name).toBe('maybe it is John') // fails

from solid.

maciek50322 avatar maciek50322 commented on June 16, 2024

Yeah so custom getter usually will get evaluated to assign the value to store property, but I don't think you need to define custom getter if you want to just pass signal as property. If you pass signal as property, you pass accessor, which return newest value.

const [signal, setSignal] = createSignal("this is not John");
const [store, setStore] = createStore({ name: signal });

console.log(store.name()); // this is not John
setSignal("another name");
console.log(store.name()); // another name

Small caveat there if you want to change store property to different signal you do

setStore("name", () => signal)

And for the other issue do you mean something like this?
https://playground.solidjs.com/anonymous/4d348a1d-5ee3-45d4-90f6-c9bc72c35e5f
createComputed sets component color when instance color changes, but when component color changes instance color stays the same

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

And for the other issue do you mean something like this?
https://playground.solidjs.com/anonymous/4d348a1d-5ee3-45d4-90f6-c9bc72c35e5f
createComputed sets component color when instance color changes, but when component color changes instance color stays the same

Yeah that could also be done through an effect, the thing is that this gets really unmaintanable once the objects get big and there are many objects. But I actually meant for it to be done the other way around, so the instance changes when the component changes. But also, if the instance changes and then the component changes the instance shouldn't change anymore for only the changed property.

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

Back to main topic, what you described originally is probably not a bug. If you have problems with understanding library or need some tips official discord might be a better place for this disccusion.

I'm not sure though, might be something to consider about the behavior of createStore. Regardless I think I'm going to end up doing something like someone from Discord described inside of a primitive for this, might end up being better.
I'll leave this open for now as Ryan might consider it a bug.

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

Another thing I experimented with is patching Solid to export the underlying $NODE symbol so as to delete the signal for the property that is being set again. This actually is feasible for my use case, the primitive ended up being too complex to write but it is possible still.

from solid.

gabrielmfern avatar gabrielmfern commented on June 16, 2024

If anyone is interested in a solution here, I ended up using a proxy OVER a store that contains the overwritten values,
then I only override the getters and the set for Solid stays the same along with all its behavior. Could not do it without this awesome library

from solid.

ryansolid avatar ryansolid commented on June 16, 2024

Nice. Yeah these sort of considerations are something that I do pay attention to. I will say the current behavior is very much by design and would be difficult to change in a performant way. It is sort of the nature of the beast but understanding the intent and effort that goes into working around this for specific cases is incredibly invaluable. Thank you posting your efforts on our github so that we have a record of it.

from solid.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.