Managing State

Examples

  • Taco Bell
  • Farmers
  • P.F. Chang's

Current Approach

Imperative

Event handlers describe explicit transformations to apply to the DOM

Transitions are hard

Summary

  • Imperative - focus on transitions
  • Minimal distinction between state and UI

New Approach

Declarative - UI as a pure function of state

  • Explicit model of state

Questions:

  • How do we change the state
    • list of defined actions
  • How do we update the view when the state changes
    • virtual dom

Component Model

{state, view, actions, update}
  • state: model of information needed to completely describe the component
  • view: (state) => rendered component
  • actions: list of the ways in which state can change
  • update: (action, state) => new state

Demo

Virtual DOM

  • h - create a virtual element
  • patch - diff and update elements

Standard Implementation

class Counter {
  constructor(el) {
    this.count;
    this.counterEl = el.querySelector('.js-count');
    this.addButton = el.querySelector('.js-add');
    this.subButton = el.querySelector('.js-sub');
    this.resetButton = el.querySelector('.js-reset');

    this.setCount(0);

    this.addButton.addEventListener('click', () => {
      this.setCount(this.count + 1);
    });

    this.subButton.addEventListener('click', () => {
      this.setCount(this.count - 1);
    });

    this.resetButton.addEventListener('click', () => {
      this.setCount(0);
    });
  }

  setCount(count) {
    this.count = count;
    this.counterEl.innerText = count;
  }
}

New Approach

State

// Model
// {
//   count: Number
// }

const init = (count) => ({count: count});

View

const view = (state) => h('div.Counter', [
  h('div.Counter-count', state.count),
  h('div.Counter-buttons', [
    h('button.Counter-button', '-'),
    h('button.Counter-button', '+'),
    h('button.Counter-button', 'Reset'),
  ]),
]);
<div class="Counter">
  <div class="Counter-count">${count}</div>
  <div class="Counter-buttons">
    <button class="Counter-button">-</button>
    <button class="Counter-button">+</button>
    <button class="Counter-button">Reset</button>
  </div>
</div>

Actions

const Action = Type({
	Increment: [],
	Decrement: [],
	Set: [Number],
});

Update

const update = Action.caseOn({
	Increment: (state) => ({count: state.count + 1}),
	Decrement: (state) => ({count: state.count - 1}),
	Set: (value, state) => ({count: value}),
});

Putting it Together

const view = (handler, state) => h('div.Counter', [
  h('div.Counter-count', state.count),
  h('div.Counter-buttons', [
    h('button.Counter-button', {on: {click: () => handler(Action.Decrement)}}, '-'),
    h('button.Counter-button', {on: {click: () => handler(Action.Increment)}}, '+'),
    h('button.Counter-button', {on: {click: () => handler(Action.Set(0))}}, 'Reset'),
  ]),
]);
const run = (state, element, Component) => {
  const newElement = Component.view((action) => {
    const newState = Component.update(action, state);
    run(newState, newElement, Component)
  }, state);
  patch(element, newElement);
}

Review