with STate Machines

Crafting Stateful Styles

@davidkpiano Β· Finch Front-End 2019

Fetch data

Fetch data

Fetch data

Fetch data

Normal

Hover

Active

Disabled

Fetched data

Success

Failed to fetch

Error

Fetching data...

Loading

Fetch data

πŸ“„

GET api/users

Pending

200 OK

Fetch data

πŸ“„

GET api/users

Pending

200 OK

Fetch data

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

Fetch data

πŸ“„

GET api/users

Pending

200 OK

Fetch data

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

Fetch data

I wasn't always good at JavaScript.

I'm still quite rubbish

input[type="checkbox"] {
  /* ... */
}

input[type="checkbox"] ~ #app {
  /* ... */
}

input[type="checkbox"] ~ #app .box {
  background-color: white;
}

input[type="checkbox"]:checked
  ~ #app .box {
  background-color: blue;
}

~

checkbox

#app

.box

~

Adjacent sibling selector

label for="..."

label for="..."

checkboxes

radios

Zero or more

One at a time

Fetching data...

<button class="loading">
  Fetching data...
</button>

<button class="success">
  Fetched data!
</button>

<button class="loading success">
  Fetched data...?
</button>

Fetched data!

Fetched data...?

Is there a better way to model
state for dynamic UIs?

User flows are transitions

between UI states

due to events

Finite state machines

and statecharts

A state is a dynamic property expressing characteristics of an object that may change in response to user action or automated processes. States do not affect the essential nature of the object, but represent data associated with the object or user interaction possibilities.Β 

  • aria-busy
  • aria-current
  • aria-disabled
  • aria-grabbed
  • aria-hidden
  • aria-invalid

Finite state machines

  • have one initial state

  • a finite number of states

  • a finite number of events

  • a mapping of state transitionsΒ 
    triggered by events

  • a finite number of final states

Idle

Pending

Rejected

Fulfilled

Fetch

Resolve

reject

Fulfilled

Rejected

Idle

Searching...

SEARCH

Success

Failure

RESOLVE

REJECT

SEARCH

SEARCH

SEARCH

🚫

SEARCH?

Data Attributes

<button data-state="loading">
  Fetching data...
</button>
button[data-state="loading"] {
  opacity: 0.5;
}
elButton.dataset.state; // 'loading'

elButton.dataset.state = 'success';

delete elButton.dataset.state;

HTML

CSS

JavaScript

<button data-state="success">
  Fetched data!
</button>
<button>
  Fetch data
</button>

Data attributes

Fetching data...

<button data-state="idle">
  Fetch data
</button>

<button data-state="loading">
  Fetching data...
</button>

<button data-state="success">
  Fetched data!
</button>

<button data-state="error">
  Failed to fetch data
</button>

Fetched data!

Failed to fetch

Fetch data

Define transitions between
states & events

function searchReducer(state = 'idle', event) {
  switch (state) {
    case 'idle':
      switch (event.type) {
        case 'SEARCH':
          return 'searching';
        default:
          return state;
      }
    case 'searching':
      switch (event.type) {
        case 'RESOLVE':
          return 'success';
        case 'REJECT':
          return 'failure';
        default:
          return state;
      }
    case 'success':
      switch (event.type) {
        case 'SEARCH':
          return 'searching';
        default:
          return state;
      }
    case 'failure':
      switch (event.type) {
        case 'SEARCH':
          return 'searching';
        default:
          return state;
      }
    default:
      return state;
  }
}

FSMs with switch/case

const machine = {
  initial: 'idle',
  states: {
    idle: {
      on: { SEARCH: 'searching' }
    },
    searching: {
      on: {
        RESOLVE: 'success',
        REJECT: 'failure',
        SEARCH: 'searching'
      }
    },
    success: {
      on: { SEARCH: 'searching' }
    },
    failure: {
      on: { SEARCH: 'searching' }
    }
  }
};

Define transitions between
states & events

const transition = (state, event) => {
  return machine
    .states[state] // current state
    .on[event]     // next state
    || state;      // or same state
}

FSMs with object mapping

NAV2

NAV1

START

panup

PANDOWN

panup

PANDOWN

npm install xstate


const machine = {
  initial: 'idle',
  states: {
    idle: {
      on: { SEARCH: 'searching' }
    },
    searching: {
      on: {
        RESOLVE: 'success',
        REJECT: 'failure',
        SEARCH: 'searching'
      }
    },
    success: {
      on: { SEARCH: 'searching' }
    },
    failure: {
      on: { SEARCH: 'searching' }
    }
  }
};
import { Machine } from 'xstate';

const machine = Machine({
  initial: 'idle',
  states: {
    idle: {
      on: { SEARCH: 'searching' }
    },
    searching: {
      on: {
        RESOLVE: 'success',
        REJECT: 'failure',
        SEARCH: 'searching'
      }
    },
    success: {
      on: { SEARCH: 'searching' }
    },
    failure: {
      on: { SEARCH: 'searching' }
    }
  }
});
import { Machine } from 'xstate';

const machine = Machine({
  // ...
});

const searchingState = machine
  .transition('idle', 'searching');

searchingState.value;
// => 'searching'
import { Machine, interpret } from 'xstate';

const machine = Machine({
  // ...
});

const service = interpret(machine)
  .onTransition(state => {
    console.log(state);
  })
  .start();

service.send('SEARCH');
// State {
//   value: 'searching',
//   ...
// }

[data-state="idle"]

[data-show="loading"]

[data-hide="loading"]

data-active

data-active

activeΒ when app is in "loading" state

inactiveΒ when app is in "loading" state

[data-state="loading"]

const elApp = document.querySelector('#app');

function setAppState(state) {
  // change data-state attribute
  elApp.dataset.state = state;
  
  // remove any active data-attributes
  document.querySelectorAll(`[data-active]`)
    .forEach(el => {
      delete el.dataset.active;
    });
  
  // add active data-attributes to proper elements
  document
    .querySelectorAll([
      `[data-show~="${state}"]`,
      `[data-hide]:not([data-hide~="${state}"])`
    ].join(','))
    .forEach(el => {
      el.dataset.active = true;
    });
}

// set button state to 'loading'
setAppState('loading');
[data-show~="${state}"]
[data-hide]:not([data-hide~="${state}"])
<main data-state="loading" data-prev-state="idle">

idle

loading

error

<main data-state="loading" data-prev-state="error">
const elApp = document.querySelector('#app');

function setAppState(state) {
  // change data-state attribute
  elApp.dataset.prevState = elApp.dataset.state;
  elApp.dataset.state = state;
  
  // ...
}

✨✨✨

πŸ’₯πŸ’₯πŸ’₯

[data-state="loading"][data-prev-state="idle"] {
  /* loading initially */
}

[data-state="loading"][data-prev-state="error"] {
  /* loading after error */
}

start

selecting

selected

dragging

disposed

grabbing

Does this scale?

State explosion problem

Statecharts
(Hierarchical State Machines)

Statecharts

Idle

Searching...

entry / prefetchResources

entry / fetchResults

Search

  • ActionsΒ - onEntry, onExit, transition
  • GuardsΒ - conditional transitions

[query.length > 0]

Statecharts

  • ActionsΒ - onEntry, onExit, transition
  • GuardsΒ - conditional transitions
  • HierarchyΒ - nested states
  • OrthogonalityΒ - parallel states
  • HistoryΒ - remembered states

H

Idle

Searching...

SEARCH

Success

Failure

RESOLVE

REJECT

SEARCH

SEARCH

Hierarchical states

Idle

Searching...

SEARCH

Success

Failure

RESOLVE

REJECT

SEARCH

Searched

Hierarchical states

Idle

Searching...

SEARCH

Success

Failure

RESOLVE

REJECT

SEARCH

Searched

Hierarchical states

const machine = Machine({
  initial: 'idle',
  states: {
    // ...
    searching: {
      on: {
        RESOLVE: 'searched',
        REJECT: 'searched.failure'
      }
    },
    searched: {
      initial: 'success',
      states: {
        success: {},
        failure: {}
      },
      on: { SEARCH: 'searching' }
    }
  }
});
#app[data-state] {
  /* has attribute */
}

#app[data-state="idle"] {
  /* attribute equals exactly */
}

#app[data-state*="err"] {
  /* attribute contains */
}

#app[data-state^="idle"] {
  /* attribute starts with */
}

#app[data-state$="error"] {
  /* attribute ends with */
}

#app[data-state~="idle.error"] {
  /* attribute contains (spaced) */
}
<main id="app" data-state="idle">
  <!-- ... -->
</main>

<main id="app" data-state="idle idle.error">
  <!-- ... -->
</main>

Think in states,
not just events.

Advantages

of using statecharts

  • Visualized modeling
  • Precise di​agrams
  • Automatic code generation
  • Comprehensive test coverage
  • Accommodation of late-breaking requirements changes

Disadvantages

of using statecharts

Learning curve

Modeling requires planning ahead

Not everything can be modeled (yet)

Statecharts

FSMs

Bottom-up

Complexity

trade-offs

States & logic

Code Complexity

The world of statecharts

Spectrum community

Make your code do more.

Thank you Finch Front-End!

@davidkpiano Β· Finch Front-End 2019