Xstate: Actions as functions in addition to strings and objects?

Created on 22 Jan 2018  路  3Comments  路  Source: davidkpiano/xstate

Bug or feature request?

Feature Request

Description:

Since functions are first-class citizens in JavaScript, and in the interest of reducing boilerplate, I suggest that function actions would be more ergonomic than strings or objects (in many cases).

(Feature) Potential implementation:

  • What the API would look like:

New Syntax:

const initTimer = seconds => () => {
  // use seconds to initiate a timer
}

const machine = Machine({
  initial: '1',
  states: {
    '1': {
      on: { 
        timer: '2'
      },
      onEntry: initTimer(30)
    },
    '2': {
      on: { 
        timer: '3'
      },
      onEntry: initTimer(45)
    },
    '3': {
      on: { 
        timer: '1'
      },
      onEntry: initTimer(60)
    }
  }
})

Current Syntax:

const actionMap = {
  initTimer: ({ seconds }) => {
    // use seconds to initiate a timer
  }
}

const machine = Machine({
  initial: '1',
  states: {
    '1': {
      on: { 
        timer: '2'
      },
      onEntry: { type: 'initTimer', seconds: 30 }
    },
    '2': {
      on: { 
        timer: '3'
      },
      onEntry: { type: 'initTimer', seconds: 45 }
    },
    '3': {
      on: { 
        timer: '1'
      },
      onEntry: { type: 'initTimer', seconds: 60 }
    }
  }
})
  • If this is a breaking change
    I don't see this as a breaking change. Actions are already polymorphic (either string or object), and it's the user's responsibility to determine what type an action is and what to do with it / how to handle it. In effect, this is likely to be a documentation change plus a ts change to allow actions of type function. This would signal to users that actions can be functions and that this construction is specifically supported by project maintainers.

  • If this is part of the existing SCXML specification
    It's a variation on the definition of actions in the SCXML spec.

Most helpful comment

This was my initial thought when adding the actions syntax; however, w.r.t. statecharts it comes with some (probably non-obvious) disadvantages:

  • Testing becomes harder. You can't easily assert that the correct actions will be called upon state transitions

    • This is a very common idiosyncrasy in testing modern apps: side effects are simply called and the developer has to _assume_ what the result of those side effects are in an opaque way.

    • Named functions can help, with the limitation that you can't pass data into them, e.g.: namedTimer(30) doesn't create a function called "namedTimerWith30Seconds" whereas { type: 'namedTimer', seconds: 30 } is fully descriptive

    • Anonymous functions completely destroy testability, unless you .toString() them or something like that.

// String-based actions
// You can assert that state.actions contains 'foo', for instance.
State {
  value: ...,
  actions: ['foo', 'bar', 'baz']
}

// Function-based actions
// More difficult to assert that the correct actions are called
State {
  value: ...,
  actions: [function, function, function]
}
  • Visualization becomes harder. If you look at examples of statecharts or UML diagrams, entry/exit/transition actions are clearly stated.

    • By specifying functions instead, especially anonymous functions, it becomes more difficult (or impossible) to know what action is going to be called with which arguments.

example statechart with transition action

  • Execution of actions becomes more difficult to optimize. Given an array of actions as strings (or descriptions of actions and parameters as objects), you can easily create opportunities to batch calls, parallelize calls (web workers), have environment-specific execution considerations, etc.
  • Statecharts become significantly less reusable. If I wanted to use the same defined statechart between React and React Native, or even between _languages_ (yes, it's very possible), not having named actions is a huge hindrance. One of the main purposes of named actions is to be framework- and language-agnostic.

I know @mogsie has similar thoughts on this, and yes, I realize this seems more boilerplatey, but I encourage you to think of this as a better long-term developer experience rather than a short-term developer experience. To the compiler, functions are, for the most part, opaque -- in that their output cannot be known until it is executed.

To see another good example of the motivation behind this syntax, take a look at how redux-saga handles declarative effects::

// Effect -> call the function Api.fetch with `./products` as argument
{
  CALL: {
    fn: Api.fetch,
    args: ['./products']
  }
}

All 3 comments

I'm not a typescript whiz, but looking through the code, it seems the only change other than documentation might be to this line in types.ts.

From:

export type Action = string | ActionObject;

To:

export type Action = string | function | ActionObject;

In addition to reducing boilerplate in the actionMap and machine definitions, I'm pretty sure actions as functions would reduce the amount of code the user would need to write in their action handlers.

This was my initial thought when adding the actions syntax; however, w.r.t. statecharts it comes with some (probably non-obvious) disadvantages:

  • Testing becomes harder. You can't easily assert that the correct actions will be called upon state transitions

    • This is a very common idiosyncrasy in testing modern apps: side effects are simply called and the developer has to _assume_ what the result of those side effects are in an opaque way.

    • Named functions can help, with the limitation that you can't pass data into them, e.g.: namedTimer(30) doesn't create a function called "namedTimerWith30Seconds" whereas { type: 'namedTimer', seconds: 30 } is fully descriptive

    • Anonymous functions completely destroy testability, unless you .toString() them or something like that.

// String-based actions
// You can assert that state.actions contains 'foo', for instance.
State {
  value: ...,
  actions: ['foo', 'bar', 'baz']
}

// Function-based actions
// More difficult to assert that the correct actions are called
State {
  value: ...,
  actions: [function, function, function]
}
  • Visualization becomes harder. If you look at examples of statecharts or UML diagrams, entry/exit/transition actions are clearly stated.

    • By specifying functions instead, especially anonymous functions, it becomes more difficult (or impossible) to know what action is going to be called with which arguments.

example statechart with transition action

  • Execution of actions becomes more difficult to optimize. Given an array of actions as strings (or descriptions of actions and parameters as objects), you can easily create opportunities to batch calls, parallelize calls (web workers), have environment-specific execution considerations, etc.
  • Statecharts become significantly less reusable. If I wanted to use the same defined statechart between React and React Native, or even between _languages_ (yes, it's very possible), not having named actions is a huge hindrance. One of the main purposes of named actions is to be framework- and language-agnostic.

I know @mogsie has similar thoughts on this, and yes, I realize this seems more boilerplatey, but I encourage you to think of this as a better long-term developer experience rather than a short-term developer experience. To the compiler, functions are, for the most part, opaque -- in that their output cannot be known until it is executed.

To see another good example of the motivation behind this syntax, take a look at how redux-saga handles declarative effects::

// Effect -> call the function Api.fetch with `./products` as argument
{
  CALL: {
    fn: Api.fetch,
    args: ['./products']
  }
}

Actions as functions will be supported in 3.3, and they're already in master 馃帀

Was this page helpful?
0 / 5 - 0 ratings

Related issues

suku-h picture suku-h  路  3Comments

greggman picture greggman  路  3Comments

mattiamanzati picture mattiamanzati  路  3Comments

hnordt picture hnordt  路  3Comments

amelon picture amelon  路  3Comments