@davidkpiano · React Finland 2018

Reactive State Machines

And Statecharts

isLoading
loading

shame on you

The most neglected variable is time.

📞

🔙

🙏🤐

🅰️⚓️

🅰️🏋️‍♀️

🔭

🅰️

⚽️🎾🎱

Callbacks

Promises

Async-Await

Observables

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

state management library

#Frameworkless

How can we model the behavior

of user interfaces?

API

Human

  • documented

  • predictable

  • testable

  • un

  • un

  • un

// ...
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 => {
      // mitä vittua
      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 => {
      // mitä vittua
      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

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

first

intermediate

second

UPDATE DATA

After .3 seconds

Click

Click

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 analytics

A

B

C

D

E

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

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]

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'
      }
    }
  }
});
class App extends Component {
  static machine = Machine({ /* ... */ });

  state = {
    appState: machine.initialState
  };

  actions = { /* ... */ };

  send(event) {
    const nextState = machine.transition(
      this.state.appState,
      event.type,
      this.state
    );
    const { actions } = nextState;

    this.setState(
      { appState: nextState },
      () => {
        const nextExtState = actions
          .reduce((extState, action) => {
            const command = this.actions[action];
            // Execute the command
            return command(extState, eventType) || extState;
          }, this.state);

        this.setState(nextExtState);
      }
    );
  }
render() {
  return (
    <form onSubmit={() => this.send('SUBMIT')}>
      <input
        type="text"
        onChange={e => this.send({
          type: 'CHANGE',
          value: e.target.value
        })}
      />

      <button>Submit</button>
    </form>
  );
}
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

Not everything can be modeled (yet)

Statecharts

FSMs

Bottom-up

Complexity

trade-offs

States & logic

Code Complexity

Statechart
visualization?

BETA!

  • I just pushed our first couple statecharts to production this afternoon. We have a medical calculator in our app that handles dose calculations.
  • The logic was slowly growing more confusing, especially with subtle UX tweaks as the component was maintained.
  • We rewrote with a statechart and everything is much clearer! Hopefully less bugs going forward.

The future

of xstate

  • Full SCXML support and conversion
  • A reactive interpreter
  • Editable visualization tools
  • Analysis and testing tools

xstate 3.2 just released!

  • Any state can transition to any state
  • Actions can be functions 🎉
  • Support for internal transitions
  • Support for raised events
  • Support for transient states/transitions
  • Support for conditional transition arrays
  • (WIP) More SCXML conversion

Resources

and tools

Write once, write anywhere

Learn once, write anywhere

Model once, implement anywhere

Let's improve the way we develop.

Thank you React Finland!

@davidkpiano · React Finland 2018