@davidkpiano Β· JSConf Iceland 2018

Simplifying Complex UIs

with Finite Automata & Statecharts

New APIs

#Frameworkless

If software is music,

developers are composers

What the different parts are

How these parts are composed together

How things change over time

What is the "music notation"

of user interfaces?

API

Human

  • documented

  • predictable

  • testable

  • un

  • un

  • un

  • > 50.000 software engineers
  • > 7 designers
  • > 1,5 billion customers

Dear Microsoft Teams, please fix this ☝️

We don't develop software

for just 1 person

happy path

// ...
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 => {
      // fokking fokk
      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 => {
      // fokking fokk
      if (this.state.canceled) {
        return;
      }

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

Spaghetti code

Lasagna code

πŸ‘ πŸ†™

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

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

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

State machines inΒ 

VS Live Share

Signed out

Signing in

Signed in

SIGN IN

SIGN IN SUCCESS

SIGN IN FAILURE

Sharing...

Shared

Joining...

Joined

share

share Success

Join Success

Join

Leave

End Collab session

Sign in

Using state machines

for analytics

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

  Telemetry.sendEvent(
    currentState,
    nextState,
    event
  );

  return nextState;
}

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

Software bugs are

made visually clear

A

B

X

Wrong state

E

E

Software bugs are

made visually clear

A

B

Unhandled event

E

E

??

Software bugs are

made visually clear

A

B

Missing transition

???

C

D

E1

E2

Software bugs are

made visually clear

A

B

Missing states

Loading...

E

Success

Software bugs are

made visually clear

A

B

E1

C

E2

E2

Race condition

E1

Harel Statecharts

extended finite state machines

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

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

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

The future

of xstate

  • Improved developer ergonomics
  • Full SCXML support and conversion
  • A reactive interpreter
  • Editable visualization tools
  • More examplesΒ in many frameworks

Statecharts

FSMs

Bottom-up

Complexity

trade-offs

States & logic

Code Complexity

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)

Resources

and tools

Write once, write anywhere

Learn once, write anywhere

Model once, implement anywhere

Let's improve the way we develop.

Takk JSConf Iceland!

@davidkpiano Β· JSConf Iceland 2018

Simplifying Complex UIs with Finite Automata & Statecharts

By David Khourshid

Simplifying Complex UIs with Finite Automata & Statecharts

JSConf Iceland 2018

  • 4,278
Loading comments...

More from David Khourshid