luisherranz / deepsignal

DeepSignal 🧶 - Preact signals, but using regular JavaScript objects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Bug?] an array nested in an object added to an observed object is not properly observed

rozek opened this issue · comments

It took me quite a while to figure out, but when trying the following code

  let hugo = deepSignal({ })
  let anna = hugo.anna = {
    _list:[1,2,3],
    get list () { return this._list.slice() }
  }

  effect(() => { console.log('> anna.list',anna.list) })

  console.log('assign')
    anna._list = anna._list.slice(0,2).concat(2.5,anna._list.slice(2))
    anna._list = anna._list.slice(0,2).concat(anna._list.slice(3))

you will see that changes to anna.list will not be recognized.

There is, however, a simple workaround: just observe anna itself

  let anna = hugo.anna = deepSignal({
    _list:[1,2,3],
    get list () { return this._list.slice() }
  })

and everything works as intended

Perhaps, this issue is not actually a bug in the code, but an unclear documentation:

  • deepSignal(...) seems to (recursively) convert an object/array and its contents into signals at the time of invocation
  • objects/array which are later added to such a converted object/array will have to be converted "manually" (it is with its own explicit deepSignal(...) invocation)

Unfortunately, things are even worse:

  • whenever a copy of an observed object/array has been created (e.g., list.slice()) which could be part of an effect, this copy also has to be explicitly converted into a deepSignal(...) - otherwise, you'll get a plain (unobserved) object/array (albeit with potentially still observed entries/elements)

Ok, things seem to be more complicated:

  • first of all, since slice() returns a copy (and copies are, by definition, independent of their originals), its result is not observed
  • if, however, you want to follow "good programming practices" and not return references to your internal data structures to your clients, you will have to change your programming style and "seal" your internal structures instead - now you can return references rather than copies. This is not a real issue as you cannot change arrays/objects "in-situ" but have to re-assign new versions to observed properties in order for deepSignal to fire

This works fine with a few helper functions:

/**** sealed ****/

  function sealed (Candidate) {
    Object.seal(Candidate)
    return Candidate
  }

/**** spliced ****/

  Array.prototype.spliced = function () {
    this.splice.apply(this,Array.prototype.slice.call(arguments))
    return this
  }

  let anna = hugo.anna = deepSignal({
    _list:sealed([1,2,3]),
    get list () { return this._list }
  })

  anna._list = sealed(anna._list.slice().spliced(2,0,2.5))
  anna._list = sealed(anna._list.slice().spliced(2,1))

Well, at least until you decide to use classes:

  class Berta {
    constructor () {
      this._list = sealed([1,2,3])
    }
    get list () { return this._list }
  }

  let anna = deepSignal(new Berta())

  effect(() => { console.log('> anna.list',anna.list) })

  console.log('assign')
    anna._list = sealed(anna._list.slice().spliced(2,0,2.5))
  console.log('anna.list and anna._list',anna.list,anna._list)

While the effect is still invoked upon every change, it always logs an old version of anna.list rather than the current one...(just look at the last output line - it's strange)

Just do it like this and it'll work fine:

hugo.anna = {
  _list: [1, 2, 3],
  get list() {
    return this._list.slice();
  },
};

let anna = hugo.anna;

https://stackblitz.com/edit/vitejs-vite-j7pkwr?file=main.js

The problem is that the proxy is created when it is accessed (= hugo.anna), and without the proxy, tracking is not enabled. When you do let anna = ... = {} you are referencing the object (target), not the proxy.