Reactivity
Autotracking
Updating
Let's build an app!
<h1>todos</h1>
<main>
<input
type="text"
placeholder="What needs to be done?"
{{on "keyup" this.addItem}}
/>
{{#each this.displayingTodos as |todo|}}
<div class="todo {{if todo.completed "completed"}}">
<input type="checkbox" {{on "change" this.toggle todo}}/>
{{todo.title}}
</div>
{{/each}}
<footer>
<span class="todos-left">
{{this.todosLeft}} items left
</span>
<button {{on "click" (fn this.show "all"}}>
All
</button>
<button {{on "click" (fn this.show "active")}}>
Active
</button>
<button {{on "click" (fn this.show "completed")}}>
Completed
</button>
</footer>
</main>
class TodoList extends Component {
allTodos = [
{
title: 'Code things',
completed: false,
},
{
title: 'Plan EmberConf talk',
completed: true,
}
];
displaying = 'all';
get activeTodos() {
return this.allTodos.filter(todo => !todo.completed);
}
get completedTodos() {
return this.allTodos.filter(todo => todo.completed);
}
get displayingTodos() {
return this[`${this.displaying}Todos`];
}
get todosLeft() {
return this.uncompletedTodos.length;
}
@action addItem({ key, target }) {
if (key !== 'Enter' || !target.value) return;
this.allTodos.push({
text: target.value,
completed: false,
});
target.value = '';
}
@action toggle(todo) {
todo.completed = !todo.completed;
}
@action show(type) {
this.displaying = type;
}
}
<h1>todos</h1>
<main>
<input
type="text"
placeholder="What needs to be done?"
{{on "keyup" this.addItem}}
/>
{{#each this.displayingTodos as |todo|}}
<div class="todo {{if todo.completed "completed"}}">
<input type="checkbox" {{on "change" this.toggle todo}}/>
{{todo.title}}
</div>
{{/each}}
<footer>
<span class="todos-left">
{{this.todosLeft}} items left
</span>
<button {{on "click" (fn this.show "all"}}>
All
</button>
<button {{on "click" (fn this.show "active")}}>
Active
</button>
<button {{on "click" (fn this.show "completed")}}>
Completed
</button>
</footer>
</main>
class TodoList extends Component {
allTodos = [
{
title: 'Code things',
completed: false,
},
{
title: 'Plan EmberConf talk',
completed: true,
}
];
displaying = 'all';
get activeTodos() {
return this.allTodos.filter(todo => !todo.completed);
}
get completedTodos() {
return this.allTodos.filter(todo => todo.completed);
}
get displayingTodos() {
return this[`${this.displaying}Todos`];
}
get todosLeft() {
return this.activeTodos.length;
}
@action addItem({ key, target }) {
if (key !== 'Enter' || !target.value) return;
this.allTodos.push({
text: target.value,
completed: false,
});
target.value = '';
}
@action toggle(todo) {
todo.completed = !todo.completed;
}
@action show(type) {
this.displaying = type;
}
}
class TodoList extends Component {
allTodos = [
{
title: 'Code things',
completed: false,
},
{
title: 'Plan EmberConf talk',
completed: true,
}
];
displaying = 'all';
get activeTodos() {
return this.allTodos.filter(todo => !todo.completed);
}
get completedTodos() {
return this.allTodos.filter(todo => todo.completed);
}
get displayingTodos() {
return this[`${this.displaying}Todos`];
}
get todosLeft() {
return this.activeTodos.length;
}
@action addItem({ key, target }) {
if (key !== 'Enter' || !target.value) return;
this.allTodos.push({
text: target.value,
completed: false,
});
target.value = '';
this.rerender();
}
@action toggle(todo) {
todo.completed = !todo.completed;
this.rerender();
}
@action show(type) {
this.displaying = type;
this.rerender();
}
}
class TodoList extends Component {
allTodos = [
{
title: 'Code things',
completed: false,
},
{
title: 'Plan EmberConf talk',
completed: true,
}
];
displaying = 'all';
get activeTodos() {
return this.allTodos.filter(todo => !todo.completed);
}
get completedTodos() {
return this.allTodos.filter(todo => todo.completed);
}
get displayingTodos() {
return this[`${this.displaying}Todos`];
}
get todosLeft() {
return this.activeTodos.length;
}
@action addItem({ key, target }) {
if (key !== 'Enter' || !target.value) return;
this.allTodos.push({
text: target.value,
completed: false,
});
target.value = '';
this.rerenderList();
this.rerenderItemsLeft();
}
@action toggle(todo) {
todo.completed = !todo.completed;
this.rerenderItem(todo);
this.rerenderItemsLeft();
}
@action show(type) {
this.displaying = type;
this.rerenderList();
}
}
Annotation Overhead
Reactivity: Declarative programming model for updating based on changes to state
Reactivity:
Declarative is about "what, not how"
<h1>Log In</h1>
<form>
<label>
Email
<input name="email" type="email" />
</label>
<label>
Password
<input name="password" type="password" />
</label>
</form>
HTML
Reactivity: Declarative programming model for updating based on changes to state
All the things that can change in your app
Root State vs. Derived State
class Person extends Component {
@tracked firstName = 'Liz';
@tracked lastName = 'Hewell';
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
Root State
Derived State
Root State
HTML
(Derived State)
Reactivity: Declarative programming model for updating based on changes to state
Events
Transformed
Events
class TodoList extends Component {
allTodos = Observable.of([
{
title: 'Code things',
completed: false,
},
{
title: 'Plan EmberConf talk',
completed: true,
}
]);
displaying = Observable.of('all');
activeTodos = allTodos.map(arr =>
arr.filter(todo => !todo.completed)
);
completedTodos = allTodos.map(arr =>
arr.filter(todo => todo.completed)
);
displayingList = Observable.forkJoin({
displaying: this.displaying,
all: this.allTodos,
active: this.activeTodos,
completed: this.completedTodos,
}).map(
values => values[values.displaying]
);
todosLeft = activeTodos.map(arr => arr.length);
@action addItem({ key, target }) {
if (key !== 'Enter' || !target.value) return;
let lastTodos = this.allTodos.last();
this.allTodos.next([...lastTodos, {
text: this.input,
completed: false,
}]);
target.input = '';
}
@action toggle(todo) {
let todos = this.allTodos.last().slice();
let index = todos.indexOf(todo);
todos[index] = {
...todo,
todo: !todo.completed
};
this.allTodos.next(todos);
}
@action show(type) {
this.displaying.next(type);
}
}
class TodoList extends Component {
allTodos = A([
{
title: 'Code things',
completed: false,
},
{
title: 'Plan EmberConf talk',
completed: true,
}
]);
displaying = 'all';
@computed('allTodos.[]')
get activeTodos() {
return this.allTodos.filter(todo => !todo.completed);
}
@computed('allTodos.[]')
get completedTodos() {
return this.allTodos.filter(todo => todo.completed);
}
@computed('displaying')
get displayingTodos() {
return this[`${this.displaying}Todos`];
}
@computed('completedTodos.[]')
get todosLeft() {
return this.activeTodos.length;
}
@action addItem({ key, target }) {
if (key !== 'Enter' || !target.value) return;
this.allTodos.pushObject({
text: this.input,
completed: false,
});
target.value = '';
}
@action toggle(todo) {
set(todo, 'completed', !todo.completed);
}
@action show(type) {
set(this, 'displaying', type);
}
}
class TodoList extends Component {
state = {
allTodos: [
{
title: 'Code things',
completed: false,
},
{
title: 'Plan EmberConf talk',
completed: true,
}
],
displaying: 'all',
};
get activeTodos() {
return this.state.allTodos.filter(
todo => !todo.completed
);
}
get completedTodos() {
return this.state.allTodos.filter(
todo => todo.completed
);
}
get displayingTodos() {
return this[`${this.state.displaying}Todos`];
}
get todosLeft() {
return this.activeTodos.length;
}
@bound addItem({ key, target }) {
if (key !== 'Enter' || !target.value) return;
let allTodos = [
...this.state.allTodos,
{
text: target.value,
completed: false,
}
];
target.value = '';
this.setState({ allTodos });
}
@bound toggle(todo) {
let allTodos = this.state.allTodos.last().slice();
let index = todos.indexOf(todo);
allTodos[index] = {
...todo,
todo: !todo.completed
};
this.setState({ allTodos });
}
@bound show(type) {
this.setState({ displaying: type });
}
}
function TodoList() {
let [allTodos, setTodos] = useState([
{
title: 'Code things',
completed: false,
},
{
title: 'Plan EmberConf talk',
completed: true,
}
]);
let [displaying, setDisplaying] = useState('all');
let activeTodos = allTodos.filter(todo => !todo.completed);
let completedTodos = allTodos.filter(todo => todo.completed);
let displayingTodos =
displaying === 'all' ? allTodos :
displaying === 'active' ? activeTodos :
displaying === 'completed' ? completedTodos : [];
let todosLeft = activeTodos.length;
// ...
useMemo
shouldComponentUpdate
Use the JavaScript State Model
class Todo {
@tracked title;
@tracked completed;
constructor(title, completed) {
this.title = title;
this.completed = completed;
}
}
class TodoList extends Component {
allTodos = new TrackedArray([
new Todo('Code things', false),
new Todo('Plan EmberConf talk', true),
]);
@tracked displaying = 'all';
get activeTodos() {
return this.allTodos.filter(todo => !todo.completed);
}
get completedTodos() {
return this.allTodos.filter(todo => todo.completed);
}
get displayingTodos() {
return this[`${this.displaying}Todos`];
}
get todosLeft() {
return this.activeTodos.length;
}
@action addItem({ key, target }) {
if (key !== 'Enter' || !target.value) return;
this.allTodos.push({
text: target.value,
completed: false,
});
target.value = '';
}
@action toggle(todo) {
todo.completed = !todo.completed;
}
@action show(type) {
this.displaying = type;
}
}
Root State
Output
// Basic memoization in JS
let lastArgs;
let lastResult;
function memoizedRender(...args) {
if (deepEqual(lastArgs, args)) {
return lastResult;
}
lastResult = render(...args);
lastArgs = args;
return lastResult;
}
class Item {
@tracked name;
constructor(name) {
this.name = name;
}
}
class State {
@tracked showItems = false;
@tracked selectedType = 'Fruits';
@tracked itemTypes = [
'Fruits',
'Vegetables',
]
@tracked fruits = [
new Item('Banana'),
new Item('Orange'),
];
@tracked vegetables = [
new Item('Celery'),
new Item('Broccoli'),
];
}
let state = new State();
const InventoryComponent = memoize(() => {
if (!state.showItems) return '';
let { selectedType } = state;
let typeOptions = state.itemTypes.map(type =>
OptionComponent(type);
);
let items = state[selectedType.toLowerCase()];
let listItems = items.map(item =>
ListItemComponent(item.name);
);
return `
<select>${typeOptions}</select>
<ul>${listItems}</ul>
`;
});
let output = InventoryComponent(state);
class Item {
@tracked name;
constructor(name) {
this.name = name;
}
}
class State {
@tracked showItems = false;
@tracked selectedType = 'Fruits';
@tracked itemTypes = [
'Fruits',
'Vegetables',
]
@tracked fruits = [
new Item('Banana'),
new Item('Orange'),
];
@tracked vegetables = [
new Item('Celery'),
new Item('Broccoli'),
];
}
let CURRENT_REVISION = 0;
dirty() {
CURRENT_REVISION++;
}
0
Clock
0
Clock
state v0
State Timeline
1
Clock
state v0
state v1
State Timeline
2
Clock
state v2
state v0
state v1
State Timeline
15156
Clock
state v2
state v15156
...
state v0
state v1
State Timeline
Tag
myClass.trackedProp
Tag
8034
Tag
15156
Clock
myClass.trackedProp
8034
Tag
15156
Clock
myClass.trackedProp++
8034
Tag
15157
15156
Clock
myClass.trackedProp++
8034
Tag
15157
15157
15156
Clock
myClass.trackedProp++
8034
let state = new State();
const InventoryComponent = memoize(() => {
if (!state.showItems) return '';
let { selectedType } = state;
let typeOptions = state.itemTypes.map(type =>
OptionComponent(type);
);
let items = state[selectedType.toLowerCase()];
let listItems = items.map(item =>
ListItemComponent(item.name);
);
return `
<select>${typeOptions}</select>
<ul>${listItems}</ul>
`;
});
let output = InventoryComponent(state);
showItems
selectedType
itemTypes
fruits
banana.name
orange.name
class Item {
@tracked name;
constructor(name) {
this.name = name;
}
}
class State {
@tracked showItems = false;
@tracked selectedType = 'Fruits';
@tracked itemTypes = [
'Fruits',
'Vegetables',
]
@tracked fruits = [
new Item('Banana'),
new Item('Orange'),
];
@tracked vegetables = [
new Item('Celery'),
new Item('Broccoli'),
];
}
showItems
selectedType
itemTypes
fruits
banana.name
orange.name
2
1
3
6
5
4
6
6
showItems
selectedType
itemTypes
fruits
banana.name
orange.name
2
1
3
6
5
4
6
7
showItems
selectedType
itemTypes
fruits
banana.name
orange.name
2
1
3
6
5
7
6
7
7
showItems
selectedType
itemTypes
fruits
banana.name
orange.name
2
1
3
6
5
7
7
7
some.other
4
showItems
selectedType
itemTypes
fruits
banana.name
orange.name
2
1
3
6
5
7
8
7
some.other
4
showItems
selectedType
itemTypes
fruits
banana.name
orange.name
2
1
3
6
5
7
8
7
some.other
8
Only need annotate root state!
This is what bending the curve looks like.
What's next?
Libraries, patterns, common abstractions
import {
TrackedArray,
TrackedMap,
TrackedSet,
} from 'tracked-built-ins';
let arr = new TrackedArray([1, 2, 3]);
let map = new TrackedMap();
let set = new TrackedSet();
Tooling!
Autotracking > Ember
Read More: www.pzuraq.com/tag/autotracking