Computed getters are great!
import { tracked } from '@glimmer/tracking';
class {
@tracked
foo = 1;
get bar() {
return this.foo * 2;
}
}
...but sometimes you need to incorporate the previous value of the computed getter in the computation. For a completely arbitrary example, let's say we want bar
to be cumulative:
import { tracked } from '@glimmer/tracking';
class {
@tracked
_foo = 1;
get foo() {
return this._foo;
}
set foo(val) {
this._foo = val;
this.bar = this.nextBar;
}
@tracked
bar = 0;
get nextBar() {
return this.bar + this._foo * 2;
}
}
As you can see, it goes off the rails rather quickly. And if your foo
is external (e.g. an @arg
) you might not have full control over when and how it's being mutated, so the solution is even more brittle than it seems at first glance.
Fortunately, ember-resources gives us a better way, by letting us close around a ephemeral variable scope and define a function that has access to the principle object as well as the ephemeral variable scope, with auto-tracking.
import { tracked } from '@glimmer/tracking';
import { resource, use } from 'ember-resources';
class {
@tracked
foo = 1;
@use bar = resource(() => {
// Here is an ephemeral variable scope that will only be accessible by the
// function returned in the next statement below.
let value = 0;
return () => {
value += this.foo * 2;
return value;
};
});
}
Here's a general purpose solution that takes an initial state object and a mutator. The mutator receives a copy of the current state and must return the new state object (or at least an object containing the properties that have changed, i.e. the relevant state changes).
There are a few different ways to write this, but I have found this version to work best with auto-tracking and contain the fewest footguns.
import { resource } from 'ember-resources';
const withState =
({ ...state }, fn) =>
resource(() => Object.assign(state, fn({ ...state })));
And here's how you would use it:
import { tracked } from '@glimmer/tracking';
import { use } from 'ember-resources';
import { withState } from './-with-state';
class {
@tracked
foo = 1;
@use barState = withState({ value: 0 }, (currentState) => {
const newState = {
value: currentState.value + this.foo * 2
};
return newState;
});
get bar() {
return this.barState.value;
}
}
And of course because the argument to the mutator is a copy of the state object, you can mutate it directly...
(state) => {
state.value += this.foo * 2;
return state;
});
...or simply destructure it and return the relevant state changes (the new value
, in this case).
({ value }) => {
value += this.foo * 2;
return { value };
});
I hope someone else finds this useful!
TBD: Can the update function benefit from caching, a la
@cached
?