tracked-toolbox
Helpful utilities for writing applications with Ember Octane's revision tracking!
Compatibility
- Ember.js v3.8 or above
- Ember CLI v2.13 or above
- Node.js v8 or above
Installation
ember install tracked-toolbox
Usage
Minimizing updates
@cached
Adds weak-caching to a getter, so that it tracks its execution, and only updates when tracked state that the getter used changes.
import { tracked } from '@glimmer/tracking';
import { cached } from 'tracked-toolbox';
class Person {
@tracked firstName = 'Tom';
@tracked lastName = 'Dale';
@cached
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
When to use it
Sometimes, a given getter has to run an expensive computation, like a complex
math calculation or a data fetch. In those cases, you may want to add a layer of
caching -- but caching by value can also be very costly, especially if your
cache key is an object, not just a simple value. @cached
allows you to have a
cache key that is extremely cheap: simply whether any of the tracked properties
used by the getter have changed.
As with caching in general, you should still only apply this when you know you need it (measure the performance first!). While it is as cheap a kind of caching as possible, it is still not free, requiring both checking whether the tracked properties have updated and the memory cost of their previous values: low but definitely not zero!
@dedupeTracked
Turns a field in a deduped @tracked
property. If you set the field to the same
value as it is currently, it will not notify a property change (thus, deduping
property changes). Otherwise, it is exactly the same as @tracked
.
import { dedupeTracked } from 'tracked-toolbox';
class Counter {
@dedupeTracked count = 0;
}
When to use it
@tracked
updates any time you set a value, even if it's the same value. So,
for example, if a tracked property depends on user input, if they set it to the
same value, it would trigger updates. Most of the time, this doesn't really
matter, but sometimes updates depending on that can be very expensive. Using
@dedupeTracked
solves this problem, while adding the cost of a small cache.
As with the other caching utilities, don't reach for this every time: caching is sometimes more expensive than just recomputing values!
Forking tracked state
In general, you should prefer to derive component state from the arguments to the component, rather than creating new tracked state. However, there are times when a component actually does logically need to generate new tracked state which still needs to update when its arguments change, but which is still local to the component.
@localCopy
Creates a local copy of a remote value. The local copy can be updated locally, but will also update if the remote value ever changes:
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { localCopy } from 'tracked-toolbox';
export default class CustomInput extends Component {
// This defaults to the value of this.args.text
@localCopy('args.text') text;
@action
updateText({ target }) {
// this updates the value of `text`, but does _not_ update
// the value of `this.args.text`
this.text = String(target.value);
if (this.args.onInput) {
// this action could then update the upstream value
this.args.onInput(this.text);
}
}
}
In this example, if args.text
were to ever change externally, then the local
text
property would also update. The local copy is not a clone of the value
passed in, it is the actual value itself, so values like arrays and objects
will still be affected upstream if their values are changed.
An initializer can be provided as the second parameter to the decorator. This
will be used if the remote value is undefined
:
export default class CustomInput extends Component {
@localCopy('args.text', 'placeholder') text;
}
If the initializer is a function, it will be called and its return value will be used as the default value.
You can also provide a getter function instead of a path to the decorator, which receives the object, key, and last as arguments:
export default class CustomInput extends Component {
@localCopy((component) => component.args.text) text;
}
This allows you to get a remote value using more complex logic. You can also use this to do more complex logic for checking the value for changes, and for deriving the local value:
export default class MyComponent extends Component {
@localCopy((component, key, last) => {
let arr = component.args.arr;
let changed = arr.some((value, index) => value !== last[index]);
return changed ? arr.slice() : last;
}) arr;
}
In this example, it allows you to have a local copy of an array. Mutations to the local array will not affect the upstream array, but changes to the upstream array will update the local copy.
@trackedReset
Similar to @localCopy
, but instead of copying the remote value, it will reset
to the class field's default value when another value changes.
export default class CustomSelect extends Component {
@trackedReset('args.selectableValues') selected = null;
}
You can also provide a configuration object with an update
function, which can
be used to provide a different value than the original on updates.
@trackedReset({
memo: 'args.items.length',
update(component, key, last) {
return Math.min(last, this.args.items.length);
}
})
selectedIndex = 0;
memo
must either be a path or a function that returns the value to memoize
against.
When to use it
@trackedReset
is primarily useful in contexts like a component which manages
the state of an overall page, and therefore which needs to update when the page
changes. For example, you might have a component which represents a form for a
given profile, at /profile/:id
. When navigating to edit a different profile,
if you do not reset the internal state of the form component, it could end up
preserving the previous profile's form data, since Ember will reuse the
component instance and simply update its arguments.
Contributing
See the Contributing guide for details.
License
This project is licensed under the MIT License.