Subscribe on changes!

computed does'nt invalidate cache if setter assigns value

avatar
Jun 17th 2022

Vue version

3.2.37

Link to minimal reproduction

https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IGNvbXB1dGVkIH0gZnJvbSAndnVlJ1xuXG5sZXQgcmF3VmFsdWUgPSBcIkhlbGxvIFdvcmxkIVwiO1xuY29uc3QgY29tcHV0ZWRQcm9wZXJ0eSA9IGNvbXB1dGVkKHtcbiAgICBnZXQ6ICgpID0+IHJhd1ZhbHVlLFxuICAgIHNldDogKG5ld1ZhbHVlKSA9PiB7XG4gICAgICAgIHJhd1ZhbHVlID0gbmV3VmFsdWU7XG4gICAgICAgIGNvbnNvbGUuZGVidWcoYFNFVFRFRDogbmV3IHZhbHVlOiBcIiR7cmF3VmFsdWV9XCJgKTtcbiAgICB9LFxufSk7XG5cblxuZnVuY3Rpb24gc2V0TmV3VmFsdWUoKSB7XG4gICAgY29tcHV0ZWRQcm9wZXJ0eS52YWx1ZSA9IFwibmV3IHZhbHVlXCI7XG59XG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8aDE+e3sgY29tcHV0ZWRQcm9wZXJ0eSB9fTwvaDE+XG4gIDxidXR0b24gQGNsaWNrPVwic2V0TmV3VmFsdWUoKVwiPlxuICAgIHNldCAnbmV3IHZhbHVlJ1xuICA8L2J1dHRvbj5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSJ9

Steps to reproduce

  1. create a computed property with a getter+setter
  2. read its value
  3. assign a value to the computed property
  4. read the computed property's value again -> it is still the initial value

The following unit test shows the behaviour:

describe("computed():", () => {
    it("Computed property is cached and won't change although altered with setter", () => {
        const initialValue = "initial Value";
        const newValue = "new value";

        let rawValue = initialValue;
        const computedProperty = computed({
            get: function () {
                return rawValue;
            },
            set: function (newValue) {
                rawValue = newValue;
            },
        });

        expect(computedProperty.value).toBeTypeOf("string");
        expect(computedProperty.value).toEqual(initialValue);

        // the new value is applied to the storage "rawValue", thus the setter is executed
        computedProperty.value = newValue;
        expect(rawValue).toEqual(newValue);

        // FAIL HERE! The computed property is cached, so its value will not change.
        expect(computedProperty.value).toEqual(newValue);
    });
});

What is expected?

The new value should be returned when reading the computed property's value after setting it. The setter should set the _dirty flag to true

What is actually happening?

Reading the value from a computed property after altering its value via the setter still returns the initial value. The internal cache is not invalidated.

System Info

System:
    OS: macOS 12.4
    CPU: (16) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 28.19 GB / 64.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 18.3.0 - /usr/local/bin/node
    Yarn: 1.22.17 - /usr/local/bin/yarn
    npm: 8.11.0 - /usr/local/bin/npm
  Browsers:
    Firefox: 101.0.1
    Safari: 15.5
  npmPackages:
    vue: >=3.0.0 => 3.2.37

Any additional comments?

No response

avatar
Jun 17th 2022

If you hit this issue you may mitigate it by tweaking your setter like this (triggerRef is exported by vue). The setter is executed with this set to the computed reference implementation. Of course, an arrow function would set a different this, so must not be used as a setter in this case.

    set: function (newValue) {
        rawValue = newValue;
        this._dirty = true;
        triggerRef(this);
    },

See it in action in the playground: https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IGNvbXB1dGVkLCB0cmlnZ2VyUmVmIH0gZnJvbSAndnVlJztcblxubGV0IHJhd1ZhbHVlID0gXCJIZWxsbyBXb3JsZCFcIjtcbmNvbnN0IGNvbXB1dGVkUHJvcGVydHkgPSBjb21wdXRlZCh7XG4gICAgZ2V0OiAoKSA9PiByYXdWYWx1ZSxcbiAgICBzZXQ6IGZ1bmN0aW9uIChuZXdWYWx1ZSkge1xuICAgICAgICByYXdWYWx1ZSA9IG5ld1ZhbHVlO1xuICAgICAgICB0aGlzLl9kaXJ0eSA9IHRydWU7XG4gICAgICAgIHRyaWdnZXJSZWYodGhpcyk7XG4gICAgfSxcbn0pO1xuXG5cbmZ1bmN0aW9uIHNldE5ld1ZhbHVlKCkge1xuICAgIGNvbXB1dGVkUHJvcGVydHkudmFsdWUgPSBcIm5ldyB2YWx1ZVwiO1xufVxuPC9zY3JpcHQ+XG5cbjx0ZW1wbGF0ZT5cbiAgPGgxPnt7IGNvbXB1dGVkUHJvcGVydHkgfX08L2gxPlxuICA8YnV0dG9uIEBjbGljaz1cInNldE5ld1ZhbHVlKClcIj5cbiAgICBzZXQgJ25ldyB2YWx1ZSdcbiAgPC9idXR0b24+XG48L3RlbXBsYXRlPiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHtcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiLFxuICAgIFwidnVlL3NlcnZlci1yZW5kZXJlclwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy9zZXJ2ZXItcmVuZGVyZXIuZXNtLWJyb3dzZXIuanNcIlxuICB9XG59In0=

The adapted unit test will work as expected:

describe("computed():", () => {
    it("MITIGATED: Computed property is cached and will change if altered with setter", () => {
        const initialValue = "initial Value";
        const newValue = "new value";

        let rawValue = initialValue;
        const computedProperty = computed({
            get: function () {
                return rawValue;
            },
            set: function (newValue) {
                rawValue = newValue;
                (this as unknown as {_dirty: boolean})._dirty = true;
                triggerRef(this);
            },
        });

        expect(computedProperty.value).toBeTypeOf("string");
        expect(computedProperty.value).toEqual(initialValue);

        computedProperty.value = newValue;
        expect(rawValue).toEqual(newValue);

        // Now works as expected - nota bene
        expect(computedProperty.value).toEqual(newValue);
    });
});
avatar
Jun 17th 2022

This is not a bug and not in need of a fix.

The getter will update when a reactive dependency has changed. Since all You do it change a let variable (which is a nonreacrive operation), nothing will updated, and this is expected.

You seem to have a misconcsption about the nature and intended use of computed setters.

avatar
Jun 17th 2022

You might be correct that notifying all the deps is not following the concept of computed-refs.

Nevertheless I think you have missed the consequence of the current implementation, that the following fundamental concept of computer languages does not hold true anymore? Maybe I am missing your point, but please re-consider my point and tell me, where I fail if I do.

This does NOT work with writable computed refs at the moment in all cases. It is not guaranteed to work.

    comp.value = 'some new value'
    comp.value === 'some new value'  <--- this is FALSE but should be TRUE
avatar
Jun 17th 2022

Nevertheless I think you have missed the consequence of the current implementation, that the following fundamental concept of computer languages does not hold true anymore?

Even when using a plain getter and setter on a class, you can write the setter (or the getter) in a way that it doesn't indeed change the value that the getter would return:

class MyClass {
  private _name = 'Tom'
  get name() {
    return this._name
  } 
  set name(name) {
      if (name !==  'Jerry') {
        this.name = name
      }
  }
}

Countless other situations where setting a value will not directly change the getter's return value are also thinkable. So it's not as fundamental as you make it out to be.

A computed with a getter and setter behaves much like normal getters and setters, with the exception that the getter's return value only updates once a reactive dependency changes. Without that behavior, a computed property would just be a normal plain JS getter on an object.

So the setter should make sure that it - directly or indirectly - makes a change to a reactive dependency of the getter.

avatar
Jun 17th 2022

Thank you very much that you have taken your time to answer. I am really grateful! I thought there might be none.

I am sorry to have bothered you. Now I see the way to use it is with underlying 'ref()' storage, which I have missed previously. Therfore, I can see your point that there is not realy a need to change the code. Agreed. Still I do not believe it is a reasonable thing to do: not to reflect any assigned change in the getter.

I will trying to wrap my class/instances based "storage" into a reactive objects, hoping there will be no unintended side effects. Otherwise there will be more boilerplate code ...