David Khourshid Β· @davidkpiano Β· Full Stack Fest 2018

State of the art

User Interfaces with Statecharts

Microsoft

PequeΓ±oflojo

The most neglected variable is time.

πŸ“ž

πŸ”™

πŸ™πŸ€

πŸ…°οΈ

πŸ”­

πŸ…°οΈ

⚽️🎾🎱

Callbacks

Promises

Async-Await

Observables

5️⃣

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

Fetch data

πŸ“„

GET api/users

Pending

200 VALE

Fetch data

πŸ“„

GET api/users

Pending

200 VALE

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 VALE

Fetch data

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

πŸ“„

GET api/users

Pending

Fetch data

// ...
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 => {
      // quΓ© carajo
      if (this.state.canceled) {
        return;
      }

      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

        error: false
      });
    })
    .catch(error => {
      // quΓ© carajo
      if (this.state.canceled) {
        return;
      }

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

πŸ‘ πŸ†™

The bottom-up

EVENT

  • ACTION 1
  • ACTION 2
  • ACTION 3
  • ACTION 4
  • ACTION 5
  • ACTION 6

approach

State

πŸ‘ πŸ†™
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

Intuition

  • UI components are not independent
  • Actions are based on event & state
  • The event-action paradigm is too simple

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

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' }
    }
  }
};
function transition(state, event) {
  return machine.states[state].on[event];
}

Define transitions between
states & actions

Transition function determines
next state from state + event

const machine = {
  // ...
};
function transition(state, event) {
  return machine.states[state].on[event];
}

Store the current state

In event handlers, only send events

let currentState = machine.initial;
function send(event) {
  currentState = transition(currentState, event);
}

// ...

someButton.addEventListener('click', () => {
  send('SEARCH');
});

Create a function for
dispatching (sending) events

Any sufficiently complicated model class contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of a state machine.

state management library

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

C

D

E

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

Using state machines

for integration testing

Using state machines

for analytics

transition(currentState, event) {
  const nextState = // ...

  Telemetry.sendEvent(
    currentState,
    nextState,
    event
  );

  return nextState;
}

Using state machines

for analytics

A

B

C

D

E

Software bugs are

made visually clear

idle

loading

success

failure

FETCH

RESOLVE

ERROR...?

Error

RETRY

Does this scale?

Harel Statecharts

extended finite state machines

Statecharts

Idle

Searching...

onEntry / prefetchResources

onEntry / fetchResults

Search

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

[query.length > 0]

onExit / cancelSearch

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

SEARCH

Idle

Searching...

SEARCH

Success

Failure

RESOLVE

REJECT

SEARCH

SEARCH

Searched

Statecharts

with xstate

npm install xstate --save
  • ActionsΒ - onEntry, onExit, transition
  • GuardsΒ - conditional transitions
  • HierarchyΒ - nested states
  • OrthogonalityΒ - parallel states
  • HistoryΒ - remembered states
const lightMachine = Machine({
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow'
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      on: {
        TIMER: 'green'
      }
    }
  }
});

const nextState = lightMachine
  .transition('green', 'TIMER');

// State {
//   value: 'yellow' 
// }
const lightMachine = Machine({
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow'
      }
    },
    yellow: {
      onEntry: ['activateYellow']
      on: {
        TIMER: 'red'
      }
    },
    red: {
      onExit: ['stopCountdown']
      on: {
        TIMER: 'green'
      }
    }
  }
});
const lightMachine = Machine({
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: {
          yellow: {
            cond: (xs, event) =>
              event.elapsed > 10000
          }
        }
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      on: {
        TIMER: 'green'
      }
    }
  }
});
const lightMachine = Machine({
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow'
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      initial: 'walk',
      states: {
        walk: {
          { on: { PED_COUNTDOWN: 'wait' } }
        },
        wait: {
          { on: { PED_COUNTDOWN_END: 'stop' } }
        },
        stop: {}
      }
    }
  }
});
const lightsMachine = Machine({
  parallel: true,
  states: {
    northSouthLight: {
      initial: 'green',
      states: {
        // ...
      }
    },
    eastWestLight: {
      initial: 'red',
      states: {
        // ...
      }
    }
  }
});
const payMachine = Machine({
  initial: 'method',
  states: {
    method: {
      initial: 'card',
      states: {
        card: {
          on: { SELECT_CASH: 'cash' }
        },
        cash: {
          on: { SELECT_CARD: 'card' }
        }
      },
      on: {
        NEXT: 'review'
      }
    },
    review: {
      on: {
        PREV: 'method.$history'
      }
    }
  }
});
import { scan } from 'rxjs/operators';

const machine = new Machine({/* ... */});
const event$ = // ...

const state$ = event$.pipe(
  scan(machine.transition),
  tap(({ actions }) => {
    // execute actions
  });
const machine = new Machine({/* ... */});

export const reducer = machine.transition;

// ...

class App extends Component() {
  // ...
  componentDidUpdate() {
    const { state } = this.props;
    const { actions } = state;

    const nextState = actions.reduce(action => {
      // execute the action commands
    }, this.state);

    // local state, or send it to Redux
    this.setState(nextState);
  }
  // ...
}

machine.transition() is
just a reducer function!

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

Statecharts

FSMs

Bottom-up

Complexity

trade-offs

States & logic

Code Complexity

Statechart
visualization?

BETA!

xstate v4 ✨

npm install xstate@next

πŸš€ External state (context)

⏱ Delayed transitions and events

πŸ‘©β€πŸ« Interpreter

πŸ‘Ύ SCXML conversion

BETA!

πŸ”œ New visualization and simulation tools

// ...

on: {
  INC: [{
    actions: [assign({
      count: ctx => ctx.count + 1
    })]
  }]
}

// ...

state.context;
// {
//   count: 42
// }
// ...

// After 1 second, go to 'yellow' state
after {
  1000: 'yellow'
}

// ...
import { Machine } from 'xstate';
import { interpret }
  from 'xstate/lib/interpreter';

const machine = Machine(...);

// Create interpreter
const interpreter = interpret(machine);

// Add listener
interpreter
  .onTransition(currentState => {
    // Listen to state updates
    this.setState({ currentState });
  });

// Initialize interpreter
interpreter.init();

// Send events!
interpreter.send('SOME_EVENT');

🏁 Final states

// ...

crosswalk1: {
  initial: 'walk',
  states: {
    walk: {
      on: { PED_WAIT: 'wait' }
    },
    wait: {
      on: { PED_STOP: 'stop' }
    },
    stop: {
      type: 'final'
    }
  }
},
crosswalk2: {
  initial: 'walk',
  states: {
    walk: {
      on: { PED_WAIT: 'wait' }
    },
    wait: {
      on: { PED_STOP: 'stop' }
    },
    stop: {
      type: 'final'
    }
  }
}

// ...

Resources

and tools

Make your code do more.

Β‘Gracias Barcelona! πŸ‡ͺπŸ‡Έ

David Khourshid Β· @davidkpiano Β· Full Stack Fest 2018

State of the Art User Interfaces with Statecharts

By David Khourshid

State of the Art User Interfaces with Statecharts

Full Stack Fest Barcelona 2018

  • 5,195