@davidkpiano Β· Craft Conf 2019

State-of-the-Art User Interfaces

with State Machines

🍎

😐

πŸ•‘

πŸ›

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

The most neglected variable is time.

state + event = nextState

❌

+ effects

βœ…

        event = nextState

❌

πŸ“ž

πŸ”™

πŸ™πŸ€

πŸ…°οΈβš“οΈ

πŸ…°οΈπŸ‹οΈβ€β™€οΈ

πŸ”­

πŸ…°οΈ

⚽️🎾🎱

Callbacks

Promises

Async-Await

Observables

// ...
onSearch(query) {
  fetch(FLICKR_API + '&tags=' + query)
    .then(data => this.setState({ data }));
}
// ...

Show data when results retrieved

// ...
onSearch(query) {
  this.setState({ loading: true });

  fetch(FLICKR_API + '&tags=' + query)
    .then(data => {
      this.setState({ data, loading: false });
    });
}
// ...

Show loading screen

Show data when results retrieved

Hide loading screen

// ...
onSearch(query) {
  this.setState({ loading: true });

  fetch(FLICKR_API + '&tags=' + query)
    .then(data => {
      this.setState({ data, loading: false });
    })
    .catch(error => {
      this.setState({
        loading: false,
        error: true
      });
    });
}
// ...

Show loading screen

Show data when results retrieved

Hide loading screen

Show error

Hide loading screen

// ...
onSearch(query) {
  this.setState({
    loading: true,
    error: false
  });

  fetch(FLICKR_API + '&tags=' + query)
    .then(data => {
      this.setState({
        data,
        loading: false,
        error: false
      });
    })
    .catch(error => {
      this.setState({
        loading: false,
        error: true
      });
    });
}
// ...

Show loading screen

Show data when results retrieved

Hide loading screen

Show error

Hide loading screen

Hide error

Hide error

// ...
onSearch(query) {
  if (this.state.loading) return;

  this.setState({
    loading: true,
    error: false,
    canceled: false
  });

  fetch(FLICKR_API + '&tags=' + query)
    .then(data => {
      if (this.state.canceled) {
        return;
      }

      this.setState({
        data,
        loading: false,
        error: false
      });
    })
    .catch(error => {
      if (this.state.canceled) {
        return;
      }
      
      // mi a faszΓ©rt csinΓ‘lod ezt?
      this.setState({
        loading: false,
        error: true
      });
    });
}

onCancel() {
  this.setState({
    loading: false,
    error: false,
    canceled: true
  });
}
// ...

Show loading screen

Show data when results retrieved

Hide loading screen

Show error

Hide loading screen

Hide error

Hide error

Search in progress already

Cancel cancellation

Ignore results if cancelled

Ignore error if cancelled

Cancel search


    .catch(error => {
      if (this.state.canceled) {
        return;
      }
      
      // mi a faszΓ©rt csinΓ‘lod ezt?
      this.setState({
        loading: false,
        error: true
      });
    });
}

onCancel() {
  this.setState({
    loading: false,
    error: false,
    canceled: true
  });
}
// ...

πŸ‘ πŸ†™

The bottom-up

approach

someButton.addEventListener('click', e => {















});

πŸ€” LOGIC

πŸ€” πŸ€” MORE LOGIC

πŸ’Ό BUSINESS LOGIC

🍻 PARTY LOGIC

⁉️ CONDITIONAL LOGIC

πŸ’₯ SIDE EFFECT

πŸ†• UPDATE STATE

🌎 UPDATE GLOBAL STATE

⏱ AWAIT... UPDATE STATE

πŸ’£ ANOTHER SIDE EFFECT

πŸ›Œ // TODO: TEST

πŸ‘ πŸ†™
code

Difficult to understand

Difficult to test

Will contain bugs

Difficult to enhance

Features make it worse

Source: Ian Horrocks, "Constructing the User Interface with Statecharts", ch. 3 pg. 17

How can we model the behavior

of user interfaces?

Finite state machines

and statecharts

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

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

function transition(state, event) {
  return machine
    .states[state]  // current state
    .on[event]      // next state based on event
  || state;         // fallback to current state
}

Transition function determines
next state from state + event

Software bugs are

made visually clear

idle

loading

success

failure

FETCH

RESOLVE

ERROR...?

Error

RETRY

Does this scale?

Statecharts

Statecharts

Idle

Searching...

onEntry / prefetchResources

onEntry / 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

Bold ON

Bold OFF

Italics ON

Italics OFF

Underline ON

Underline OFF

Characters

Idle

Searching...

SEARCH

Success

Failure

RESOLVE

REJECT

SEARCH

SEARCH

Idle

Searching...

SEARCH

Success

Failure

RESOLVE

REJECT

SEARCH

Searched

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' }
    }
  }
});

πŸ“¦ Hierarchical states
πŸ‘―β€β™€οΈ Parallel states
πŸ’₯ Actions (declarative side effects)
πŸ•° History states
❓ Conditional transitions
πŸš€ External state (context)
⏱ Delayed transitions and events
πŸ‘©β€πŸ« Interpreter
🏁 Final states
πŸ”— Invoking external machines
πŸ”œ New visualization and simulation tools
❀️ And much more

Using state machines

for integration testing

  • Shortest path algorithms (Dijkstra, Bellman-Ford, A* search, etc.)
  • Analytics provides weights
  • Represents all happy paths
  • Can be automatically generated

A

B

C

D

E

A β†’ B
A β†’ B β†’ C
A β†’ D
A β†’ D β†’ E

Using state machines

for integration testing

  • Depth-first search (DFS) algorithm for finding all simple paths
  • Represents all possible user flows
  • Reveals all edge cases
  • Can be automatically generated ⚠️
A β†’ B
A β†’ B β†’ C
A β†’ D
A β†’ D β†’ E
A β†’ D β†’ E β†’ C
A β†’ D β†’ B β†’ C
A β†’ B β†’ E β†’ C
A β†’ D β†’ E β†’ B β†’ C

A

B

C

D

E

Determinism

Visualization

πŸ‘©β€πŸ’» Developer Experience

Communication

Analytics

Simulation

Testability

πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ User Experience

1. Abstract model

2. Transition analytics

3. Identify adaptive paths

4. Use analysis for adaptation

Adaptive User Interfaces

Photo by NEW DATA SERVICESΒ on Unsplash

Success

Signing in

Error

0.9

0.1

Weighted graphs

Login

Gallery

Profile

Camera

SUCCESS

TAP PROFILE

TAP CAMERA

BACK

BACK

Scroll

Analysis of transitions

πŸ—Ί Login, Gallery, Gallery

πŸ—Ί Login, Gallery, Camera

πŸ—Ί Login, Gallery, Profile

0.90, 0.31

0.90, 0.57

0.90, 0.12

0.90

0.31

0.57

0.13

UNAUTHORIZED

0.10

SUCCESS

(decide)

TAP AD

Analysis of actions

+10

Gallery

Ads interspersed

Ads shown at top

Ad

Scroll

Scroll

TAP AD

+10

-1

-10

INTERSP > .5

TOP > .5

Decision trees

Contextual data

  • Registration date
  • Number of friends
  • Posting frequency
  • Location
  • ...etc.

New user?

< 30 days

>= 30 days

Show ad at top

Friend count?

< 100

>= 100

Show more ads

Show less ads

All data

A

B

C

D

E

import { Machine } from 'xstate';

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

const myService = interpret(myMachine)
  .onTransition(state => {
    analytics.track({
      target: state.value,
      source: state.history
        ? state.history.value
        : undefined,
      event: sanitize(event),
      timestamp: Date.now()
    });
  })
  .start();

A

B

C

D

E

import { Machine } from 'xstate';
import { getShortestPaths } from '@xstate/graph';

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

const shortestPaths = getShortestPaths(myMachine);
  • A: A
  • B: A -> B
  • C: A -> B -> C
  • D: A -> D
  • E: A -> D -> E

Agent

Environment

Action

State

Reward

Reinforcement
learning

service.onTransition(state => {
  analytics.track({
    source: state.history.value,
    target: state.value,
    context: state.context,
    event: state.event,
    timestamp: Date.now()
  })
});

Application

Executable Model

Metrics Tracking

Adaptive Transitions

Adaptive UI

βž•

The future of state

is nothing new.

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

Make your code do more.

Thank you Craft Conf!

@davidkpiano Β· Craft Conf 2019

State of the Art User Interfaces with State Machines

By David Khourshid

Private

State of the Art User Interfaces with State Machines

Craft Conf 2019

More from David Khourshid