computed does'nt invalidate cache if setter assigns value
Vue version
3.2.37
Link to minimal reproduction
Steps to reproduce
- create a computed property with a getter+setter
- read its value
- assign a value to the computed property
- 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
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);
},
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);
});
});
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.
Maybe I have not understood the concept of a writable computed value. The documentation is not very clear about it. Its example uses non reactive storing of the data as well. What is the use case then?
https://vuejs.org/guide/essentials/computed.html#writable-computed
I can use a ref as storage to make it reactive but why this distinction? From a component perspective a computed-ref seems just like a variant of a basic ref.
I would be grateful if someone can clarify the intention here or in the docs.
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
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.
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 ...