Revision Tracking

Gotchas and Techinques

Chris Hewell Garrett

Sr. Software Engineer, Client Application Frameworks

aka: "Autotracking"

@tracked

class PersonCard extends Component {
  firstName = 'Liz';
  lastName = 'Hewell';
  
  @computed('firstName', 'lastName')
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  
  @action
  updateName(first, last) {
    set(this, 'firstName', first);
    set(this, 'lastName', last);
  }
}
class PersonCard extends Component {
  @tracked firstName = 'Liz';
  @tracked lastName = 'Hewell';
  
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  
  @action
  updateName(first, last) {
    this.firstName = first;
    this.lastName = last;
  }
}
class PersonCard extends Component {
  firstName = 'Liz';
  lastName = 'Hewell';
  
  @computed('firstName', 'lastName')
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  
  @action
  updateName(first, last) {
    set(this, 'firstName', first);
    set(this, 'lastName', last);
  }
}

Gotchas

2

3

4

Short-circuiting

Infinite invalidation

Weak caching

1

Uncached getters

class Person {
  firstName = 'Liz';
  lastName = 'Fizz';

  @computed('firstName', 'lastName')
  get fullName() {
    console.log('calculating fullName...')
    
    return `${this.firstName} ${this.lastName}`;
  }
} 
class Person {
  firstName = 'Liz';
  lastName = 'Fizz';

  @computed('firstName', 'lastName')
  get fullName() {
    console.log('calculating fullName...')
    
    return `${this.firstName} ${this.lastName}`;
  }
} 

let liz = new Person();

liz.fullName; // 'calculating fullName...' logged
class Person {
  firstName = 'Liz';
  lastName = 'Fizz';

  @computed('firstName', 'lastName')
  get fullName() {
    console.log('calculating fullName...')
    
    return `${this.firstName} ${this.lastName}`;
  }
} 

let liz = new Person();

liz.fullName; // 'calculating fullName...' logged
liz.fullName; // nothing logged

class Person {
  @tracked firstName = 'Liz';
  @tracked lastName = 'Fizz';

  get fullName() {
    console.log('calculating fullName...')
    
    return `${this.firstName} ${this.lastName}`;
  }
} 

let liz = new Person();

liz.fullName; // 'calculating fullName...' logged
liz.fullName; // nothing logged?
class Person {
  @tracked firstName = 'Liz';
  @tracked lastName = 'Fizz';

  get fullName() {
    console.log('calculating fullName...')
    
    return `${this.firstName} ${this.lastName}`;
  }
} 

let liz = new Person();

liz.fullName; // 'calculating fullName...' logged
liz.fullName; // 'calculating fullName...' logged again!

class Person {
  firstName = 'Liz';
  lastName = 'Fizz';

  @computed('firstName', 'lastName')
  get fullName() {
    console.log('calculating fullName...')
    
    return `${this.firstName} ${this.lastName}`;
  }
} 
class MyComponent extends Component {
  @computed('firstName', 'lastName')
  get person() {
    return new Person(this.firstName, this.lastName);
  }
} 
class MyComponent extends Component {
  @tracked firstName;
  @tracked lastName;
  
  get person() {
    return new Person(this.firstName, this.lastName);
  }
} 
class MyComponent extends Component {
  @tracked firstName;
  @tracked lastName;
  
  get person() {
    return new Person(this.firstName, this.lastName);
  }
  
  get fullName() {
    return this.person.fullName;
  }
} 
import { cached } from 'tracked-toolbox';

class Person {
  @tracked firstName = 'Liz';
  @tracked lastName = 'Fizz';

  @cached
  get fullName() {
    console.log('calculating fullName...')
    
    return `${this.firstName} ${this.lastName}`;
  }
} 
import { cached } from 'tracked-toolbox';

class Person {
  @tracked firstName = 'Liz';
  @tracked lastName = 'Fizz';

  @cached
  get fullName() {
    console.log('calculating fullName...')
    
    return `${this.firstName} ${this.lastName}`;
  }
} 

let liz = new Person();

liz.fullName; // 'calculating fullName...' logged
liz.fullName; // nothing logged!

Gotchas

2

3

4

Short-circuiting

Infinite invalidation

Weak caching

1

Uncached getters

class MyComponent extends Component {
  inViewport = false;
  containerWidth = 1000;
  width = 800;

  @computed('inViewport', 'containerWidth', 'width')
  get shouldShow() {
    console.log('calculating shouldShow...');

    if (this.inViewport) {
      return this.containerWidth > this.width;
    }

    return false;
  }
}
class MyComponent extends Component {
  @tracked inViewport = false;
  @tracked containerWidth = 1000;
  @tracked width = 800;

  get shouldShow() {
    console.log('calculating shouldShow...');

    if (this.inViewport) {
      return this.containerWidth > this.width;
    }

    return false
  }
}

Gotchas

2

3

4

Short-circuiting

Infinite invalidation

Weak caching

1

Uncached getters

class MyComponent extends Component {
  url = 'http://example.com';
  
  @computed('url')
  get fetchedValue() {
    return PromiseProxy.create({ 
      promise: fetch(this.url),
    });
  }
}
class MyComponent extends Component {
  @tracked url = 'http://example.com';
  
  get fetchedValue() {
    return PromiseProxy.create({ 
      promise: fetch(this.url),
    });
  }
}
class MyComponent extends Component {
  @tracked url = 'http://example.com';
  
  get fetchedValue() {
    return PromiseProxy.create({ 
      promise: fetch(this.url),
    });
  }
}
class PromiseProxy extends Proxy {
  constructor() {
    super(...arguments);
    
    // entangled content, for some reason
    get(this, 'content');
    
    this.promise.then((result) => {
      set(this, 'content', result);
    });
  }
}

Gotchas

2

3

4

Short-circuiting

Infinite invalidation

Weak caching

1

Uncached getters

class MyComponent extends Component {
  @tracked url = 'http://example.com';
  
  @cached
  get fetchedValue() {
    return PromiseProxy.create({ 
      promise: fetch(this.url),
    });
  }
}
class MyComponent extends Component {
  @tracked baseUrl = 'http://example.com/search';
  
  @tracked foo = true;
  @tracked bar = false;

  get searchUrl() {
    let fooOrBar = this.foo || this.bar;
    
    return `${this.baseUrl}?fooOrBar=${fooOrBar}`;
  }
  
  @cached
  get searchResults() {
    return PromiseProxy.create({ 
      promise: fetch(this.searchUrl),
    });
  }
}
class MyComponent extends Component {
  @tracked baseUrl = 'http://example.com/search';
  
  @tracked foo = true;
  @tracked bar = false;

  get searchUrl() {
    let fooOrBar = this.foo || this.bar;
    
    return `${this.baseUrl}?fooOrBar=${fooOrBar}`;
  }

  _lastSearchUrl;
  _lastSearchResults;  

  get searchResults() {
    let { searchUrl } = this;
    
    if (this._lastSearchUrl === searchUrl) {
      this._lastSearchUrl = searchUrl;
      this._lastSearchRseults = PromiseProxy.create({ 
        promise: fetch(searchUrl),
      });
    }
    
    return this._lastSearchResults;
  }
}

Gotchas

2

3

4

Short-circuiting

Infinite invalidation

Weak caching

1

Uncached getters

Techniques

2

3

Tracking function calls

Libraries

1

Tracking dynamic keys

4

Tracking helpers and modifiers

class Store extends Component {
  groceries = [
    { name: 'Milk', price: 1.23, perUnitPrice: 1.01, },
    { name: 'Eggs', price: 3.00, perUnitPrice: 0.25, },
    { name: 'Cheese', price: 1.34, perUnitPrice: 1.00 },
  ];

  filterAmount = 2.00;
  
  @computed('filterAmount', 'groceries.@each.price')
  get filteredGroceries() {
    let { filterAmount } = this;
    
    return this.groceries.filter((item) => {
      return item.price < filterAmount;
    });
  }
}
class Store extends Component {
  groceries = [
    { name: 'Milk', price: 1.23, perUnitPrice: 1.01, },
    { name: 'Eggs', price: 3.00, perUnitPrice: 0.25, },
    { name: 'Cheese', price: 1.34, perUnitPrice: 1.00 },
  ];

  filterAmount = 2.00;
  filterKey = 'price';
  
  @computed('filterAmount', 'filterKey', 'groceries.@each.???')
  get filteredGroceries() {
    let { filterAmount, filterKey } = this;
    
    return this.groceries.filter((item) => {
      return item[filterKey] < filterAmount;
    });
  }
}

ember-macro-helpers

createClassComputed()
class GroceryItem {
  @tracked name;
  @tracked price;
  @tracked perUnitPrice;
  
  constructor(name, price, perUnitPrice) {
    this.name = name;
    this.price = price;
    this.perUnitPrice = perUnitPrice;
  }
}
class Store extends Component {
  groceries = [
    new GroceryItem('Milk', 1.23, 1.01),
    new GroceryItem('Eggs', 3.00, 0.25),
    new GroceryItem('Cheese', 1.34, 1.00),
  ];

  @tracked filterAmount = 2.00;
  @tracked filterKey = 'price';
  
  get filteredGroceries() {
    let { filterAmount, filterKey } = this;
    
    return this.groceries.filter((item) => {
      return item[filterKey] < filterAmount;
    });
  }
}

Techniques

2

3

Tracking function calls

Libraries

1

Tracking dynamic keys

4

Tracking helpers and modifiers

class Store extends Component {
  groceries = [
    new GroceryItem('Milk', 1.23, 1.01),
    new GroceryItem('Eggs', 3.00, 0.25),
    new GroceryItem('Cheese', 1.34, 1.00),
  ];

  @tracked priceFilter = 2.00;
  @tracked perUnitPriceFilter = 1.00;
  
  filter(filterKey, filterAmount) {
    return this.groceries.filter((item) => {
      return item[filterKey] < filterAmount;
    });
  }

  get filteredByPrice() {
    return this.filter('price', this.priceFilter);
  }

  get filteredByPerUnitPrice() {
    return this.filter('perUnitPrice', this.perUnitPriceFilter);
  }
}

Techniques

2

3

Tracking function calls

Libraries

1

Tracking dynamic keys

4

Tracking helpers and modifiers

import { observer } from '@ember/object';
import { inject as service } from '@ember/service'; 
import Helper from '@ember/component/helper'; 
 
export default Helper.extend({ 
  i18n: service(), 
 
  compute() { 
    return this.i18n.isRtlLanguage; 
  }, 
 
  _recomputeOnRtlChange: observer('i18n.isRtlLanguage', function () {
    this.recompute(); 
  }),
}); 
import { inject as service } from '@ember/service'; 
import Helper from '@ember/component/helper'; 
 
export default Helper.extend({ 
  i18n: service(), 
 
  compute() { 
    return this.i18n.isRtlLanguage; 
  }, 
}); 
import Modifier from 'ember-modifier'

export default class ScrollModifier extends Modifier {
  @service scrollTop;
  
  didInstall() {
    this.element.scrollTop = this.scrollTop.value;
  }
  
  didUpdateAttrs() {
    this.element.ScrollTop = this.scrollTop.value;
  }
}

Techniques

2

3

Tracking function calls

Libraries

1

Tracking dynamic keys

4

Tracking helpers and modifiers

  • tracked-built-ins
    • Arrays, objects, maps, and sets
  • tracked-maps-and-sets
  • tracked-toolbox
    • @cached
    • @dedupeTracked
    • @localCopy
  • tracked-redux

Thank you!