Skip to content

Instantly share code, notes, and snippets.

@mwpastore
Last active April 5, 2023 17:29
Show Gist options
  • Save mwpastore/6347c7ad9d8576541e58d6d99a96cf74 to your computer and use it in GitHub Desktop.
Save mwpastore/6347c7ad9d8576541e58d6d99a96cf74 to your computer and use it in GitHub Desktop.
Ember/Glimmer computed getter with access to previous value
import { resource } from 'ember-resources';
const withState =
({ ...state }, fn) =>
resource(() => Object.assign(state, fn({ ...state })));
export default { withState };

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!

@mwpastore
Copy link
Author

TBD: Can the update function benefit from caching, a la @cached?

import { tracked } from '@glimmer/tracking';
import { createCache, getValue } from '@glimmer/tracking/primitives/cache';
import { resource, use } from 'ember-resources';

class {
  @tracked
  foo = 1;

  @use bar = resource(() => {
    let value = 0;

    const fn = createCache(() => {
      value += this.foo * 2;
      
      return value;
    });

    return () => getValue(fn);
  });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment