Git Product home page Git Product logo

react-automata's Introduction

npm Build Status tested with jest code style: prettier

React Automata

A state machine abstraction for React that provides declarative state management and automatic test generation.

Quick Start

Installation

react and react-test-renderer are peer dependencies.

yarn add react-automata

Usage

// App.js

import React from 'react'
import { Action, withStateMachine } from 'react-automata'

const statechart = {
  initial: 'a',
  states: {
    a: {
      on: {
        NEXT: 'b',
      },
      onEntry: 'sayHello',
    },
    b: {
      on: {
        NEXT: 'a',
      },
      onEntry: 'sayCiao',
    },
  },
}

class App extends React.Component {
  handleClick = () => {
    this.props.transition('NEXT')
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>NEXT</button>
        <Action is="sayHello">Hello, A</Action>
        <Action is="sayCiao">Ciao, B</Action>
      </div>
    )
  }
}

export default withStateMachine(statechart)(App)
// App.spec.js

import { testStateMachine } from 'react-automata'
import App from './App'

test('it works', () => {
  testStateMachine(App)
})
// App.spec.js.snap

exports[`it works: a 1`] = `
<div>
  <button
    onClick={[Function]}
  >
    NEXT
  </button>
  Hello, A
</div>
`;

exports[`it works: b 1`] = `
<div>
  <button
    onClick={[Function]}
  >
    NEXT
  </button>
  Ciao, B
</div>
`;

API

withStateMachine(statechart[, options])(Component)

The withStateMachine higher-order component accepts an xstate configuration object or an xstate machine, some options and a component. It returns a new component with special props, action and activity methods and additional lifecycle hooks. The initial machine state and the initial data can be passed to the resulting component through the initialMachineState and initialData props.

Options

Option Type Description
channel string The key of the context on which to set the state.
devTools bool To connect the state machine to the Redux DevTools Extension.

Props

transition(event[, updater])

The method to change the state of the state machine. It takes an optional updater function that receives the previous data and returns a data change. The updater can also be an object, which gets merged into the current data.

handleClick = () => {
  this.props.transition('FETCH')
}

machineState

The current state of the state machine.

It's not recommended to use this value because it couples the component and the state machine.

<button onClick={this.handleClick}>
  {this.props.machineState === 'idle' ? 'Fetch' : 'Retry'}
</button>

Action and Activity methods

All the component's methods whose names match the names of actions and activities, are fired when the related transition happen. Actions receive the state and the event as arguments. Activities receive a boolean that is true when the activity should start, and false otherwise.

For example:

const statechart = {
  // ...
  fetching: {
    on: {
      SUCCESS: 'success',
      ERROR: 'error',
    },
    onEntry: 'fetchGists',
  },
  // ...
}

class App extends React.Component {
  // ...
  fetchGists() {
    fetch('https://api.github.com/users/gaearon/gists')
      .then(response => response.json())
      .then(gists => this.props.transition('SUCCESS', { gists }))
      .catch(() => this.props.transition('ERROR'))
  }
  // ...
}

Lifecycle hooks

componentWillTransition(event)

The lifecycle method invoked when the transition function has been called. It provides the event, and can be used to run side-effects.

componentWillTransition(event) {
  if (event === 'FETCH') {
    fetch('https://api.github.com/users/gaearon/gists')
      .then(response => response.json())
      .then(gists => this.props.transition('SUCCESS', { gists }))
      .catch(() => this.props.transition('ERROR'))
  }
}

componentDidTransition(prevMachineState, event)

The lifecycle method invoked when a transition has happened and the state is updated. It provides the previous state machine, and the event. The current machineState is available in this.props.

componentDidTransition(prevMachineState, event) {
  Logger.log(event)
}

<Action />

The component to define which parts of the tree should be rendered for a given action (or set of actions).

Prop Type Description
is oneOfType(string, arrayOf(string)) The action(s) for which the children should be shown. It accepts the exact value, a glob expression or an array of values/expressions (e.g. is="fetch", is="show*" or is={['fetch', 'show*']).
channel string The key of the context from where to read the state.
children node The children to be rendered when the conditions match.
render func The render prop receives a bool (true when the conditions match) and it takes precedence over children.
onHide func The function invoked when the component becomes invisible.
onShow func The function invoked when the component becomes visible.
<Action is="showError">Oh, snap!</Action>
<Action
  is="showError"
  render={visible => (visible ? <div>Oh, snap!</div> : null)}
/>

<State />

The component to define which parts of the tree should be rendered for a given state (or set of states).

Prop Type Description
is oneOfType(string, arrayOf(string)) The state(s) for which the children should be shown. It accepts the exact value, a glob expression or an array of values/expressions (e.g. is="idle", is="error.*" or is={['idle', 'error.*']).
channel string The key of the context from where to read the state.
children node The children to be rendered when the conditions match.
render func The render prop receives a bool (true when the conditions match) and it takes precedence over children.
onHide func The function invoked when the component becomes invisible.
onShow func The function invoked when the component becomes visible.
<State is="error">Oh, snap!</State>
<State
  is="error"
  render={visible => (visible ? <div>Oh, snap!</div> : null)}
/>

testStateMachine(Component[, { fixtures, extendedState }])

The method to automagically generate tests given a component wrapped into withStateMachine. It accepts an additional fixtures option to describe the data to be injected into the component for a given transition, and an extendedState option to control the statechart's conditions - both are optional.

const fixtures = {
  initialData: {
    gists: [],
  },
  fetching: {
    SUCCESS: {
      gists: [
        {
          id: 'ID1',
          description: 'GIST1',
        },
        {
          id: 'ID2',
          description: 'GIST2',
        },
      ],
    },
  },
}

test('it works', () => {
  testStateMachine(App, { fixtures })
})

Examples

Frequently Asked Questions

You might find the answer to your question here.

Upgrades

The upgrade process is documented here.

Inspiration

Federico, for telling me "Hey, I think building UIs using state machines is the future".

David, for giving an awesome talk about infinitely better UIs, and building xstate.

Ryan, for experimenting with xstate and React - Ryan's approach to React has always been a source of inspiration to me.

Erik, for writing about statecharts, and showing me how to keep UI and state machine decoupled.

react-automata's People

Contributors

andarist avatar aulisius avatar baransu avatar davidkpiano avatar ferdinandsalis avatar fiberjw avatar gantman avatar gcazaciuc avatar ioss avatar jxom avatar kirillku avatar kriswep avatar lopis avatar michelebertoli avatar shmck avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-automata's Issues

Reusable State Machines

Recently, I've been using a modified version of react-automata at work. The changes address a problem I've had with trying to connect multiple components to a single statechart.

Using the React 16.3+ Context API you can set a root Provider, and connect multiple components using a generated withStatechart for that specific config.

import createStatechart from './lib/someLib'

const { Provider, withStatechart } = createStatechart(machineConfig)

const A = withStatechart(AComponent)
const B = withStatechart(AComponent)

const someComponent = () => (
  <Provider>
    <A />
    <B />
  </Provider>
)

A pattern like this may be worth exploring in later releases of "react-automata". Unfortunately, it would currently result in some major breaking changes.

Unable to nest withStatechart components

When a react-automata wrapped component has a child component that uses also uses react-automata with its own statechart, State (or Action, I would assume) cannot discern which statechart its value prop is compared to.

I believe it has to do with how State tries to access the machineState via context. Since the statechart is nested within a parent component, also with a statechart, it doesn't access the correct machineState.

class StatechartComponentA extends Component {
  // ...
}

const StatechartComponentAWrapped = withStatechart(statechartA)(StatechartComponentA)

class StatechartComponentB extends Component {
  // ...
}

const StatechartComponentBWrapped = withStatechart(statechartB)(StatechartComponentB)

In a file elsewhere…

<StatechartComponentAWrapped>
  <StatechartComponentBWrapped>
    <State value="stateA1">Impossible to render</State>
    <State value="stateB1">Possible to render</State>
  </StatechartComponentBWrapped>
</StatechartComponentAWrapped>

Would it be possible to provide a key to identify which statechart State and Action should reference?

xstate can take in a key property, so maybe this value could be used? I'm not 100% sure what key is used for within xstate, so I may be wrong suggesting it.

Question: Is history nodes supported ?

Typically it is desirable to have the possibility to go back to the state that you came from.

This can be handled with at history transition. My problem is that it stays at the history state instead of changing to the previous state

`const initialState = "a";

const statechart = {
initial: initialState,
states: {
a: {
on: {
NEXT: "b",
PREVIOUS: "hist"
},
onEntry: "sayHello"
},
b: {
on: {
NEXT: "c",
PREVIOUS: "hist"
},
onEntry: "sayCiao"
},
c: {
on: {
NEXT: "a",
PREVIOUS: "hist"
},
onEntry: "sayHej"
},
hist: {
type:"history",
history: "deep",
target: initialState
}
}
};
`

My test code

Typescript support?

Is Typescript support planned? Currently, the withStateMachine HOC produces a component that no longer has the props from the wrapped component. Thus VSCode can no longer validate props when the resulting component is used in JSX.

Upgrade guide for v4

Since v4 had some API changes and renaming, I was looked for an upgrade guide.
Only thing I found was the changelog. That could be a little bit clearer.
Are you interested in a Upgrading Documentation PR? Especially from 3.x.x to 4.x x?
If yes, I could send a PR in the hacktoberfest spirit.

Inconsistent lifecycle methods firing

I'm seeing the componentDidTransition inconsistently firing.

Reproduce:

  1. Go to https://codesandbox.io/s/30x3r5vj6p, and open console
  2. Click dowork button. Notice willTransition, beep action, and didTransition fires as expected
  3. Click dowork again. Notice willTransition and didTransition fires
  4. Click dowork again (one or more times). Notice only willTransition fires

Expectation:
I expect steps 3 and 4 to be firing the same lifecycle methods consistently. I'm not sure if they should fire both willTransition and didTransition or only willTransition.

Animated Action

Hi, first I would to say that this is an awesome project!

I have a question, it's possible to create a version of Action that has animation integrate within it?
I thought in using react-transition-group to simplify, if you guide me I can submit a PR or whatever you think is appropriate.

Thanks in advance.

Future plans for react automata

Hi Michele,

I am curious if and how you are going to evolve this library in the coming month. I am sure there some new interesting ways to do things with the changes recently introduced to react and xstate. Anyway, just dropping by to say that I am happily using react automata in two of my projects and look forward to all the things statecharts and react that are happening.

👋 Ferdinand

Actions don't appear to fire in automated snapshot tests.

I have an example traffic light app I'm working on and I just tried to auto generate the snapshots with testStatechart - which is one of the main reasons I HAD to play with this cool lib.

Unfortunately the snapshots don't reflect the states correctly.
I expected to see the classes on the traffic light's child divs change for each state but it appears that the actions were not fired in the state transitions. Should I expect the actions to have fired and snapshot's to reflect the component state after those actions? Or am I missing something.

Changelog

Had some problems switching from v3 to v4 because didn't noticed all the api changes in code. Would be nice to have at least sort changelog message when updating the package version.

DevTools

Features:

  • Expose history / time travel
  • Highlight state machines and transition

handling nested state

I have a sorting table with three sorting options. A state machine was a great fit for the situation because I only want people to be able to sort by option at any give time. This is the state machine I came up with:


const stateMachine = {
  initial: 'price',
  states: {
    price: {
      onEntry: 'orderByPrice',
      on: {
        RATE: 'rate',
        TIME: 'time'
      },
    },
    time: {
      onEntry: 'orderByTime',
      on: {
        PRICE: 'price',
        RATE: 'rate'
      }
    },
    rate: {
      onEntry: 'orderByRate',
      on: {
        PRICE: 'price',
        TIME: 'time'
      }
    }
  }
};

Then I realised that I need to let people sort ascending or descending within each category. So I added nested state to each state:

const stateMachine = {
  initial: 'price',
  states: {
    price: {
      onEntry: 'orderByPrice',
      on: {
        RATE: 'rate',
        TIME: 'time'
      },
      initial: 'asc',
      states: {
        asc: {
          on: {
            TOGGLE_PRICE: 'desc'
          }
        },
        desc: {
          on: {
            TOGGLE_PRICE: 'asc'
          }
        }
      }
    },
    time: {
      onEntry: 'orderByTime',
      on: {
        PRICE: 'price',
        RATE: 'rate'
      },
      initial: 'asc',
      states: {
        asc: {
          on: {
            TOGGLE_TIME: 'desc'
          }
        },
        desc: {
          on: {
            TOGGLE_TIME: 'asc'
          }
        }
      }
    },
    rate: {
      onEntry: 'orderByRate',
      on: {
        PRICE: 'price',
        TIME: 'time'
      },
      initial: 'asc',
      states: {
        asc: {
          on: {
            TOGGLE_RATE: 'desc'
          }
        },
        desc: {
          on: {
            TOGGLE_RATE: 'asc'
          }
        }
      }
    }
  }
};

I'd like to increase opacity for the category when a given state is selected and show the correct logo for the correct nested state so I went with...

<div
  className={`
  ${
    Object.keys(machineState.value).includes('price') 
    ? 'o-90' : 'o-30'
  }`}
 onClick={() => transition('PRICE')}
>
  <p>BY PRICE</p>
  <State is="price.asc">
    <UpArrowCircle className="ml2" />
  </State>
  <State is="price.desc">
    <DownArrowCircle className="ml2" />
  </State>
</div>

My problem is with the line...
Object.keys(machineState.value).includes('price')

This smells wrong.

When you are nesting states is there a way to check which state you are nested in?

Support updaters

withStateMachine(machine, { [initialState] })(Component)
transition(action, [updater])
prevState => stateChange

StateMachine Component

I have a proposal, only half thought through, but I’d love to get some feedback.

StateMachine Component

Currently, in React Automata we wrap components with withStatechart to connect them to an instance of the state chart.

Statechart → wrapper → Component

As an experiment, I wonder if it might be easier if this extra level of abstraction could be removed.

Statechart → StateMachineComponent

While inheritance over composition is generally not a best practice, I think there is a case to make for creating an abstract React class for state machine management.

import { Machine } from 'xstate'

const StateMachine = Machine({ ... })

class SomeComponent extends StateMachineComponent<Props, States> {
  state = {}
  stateMachine = StateMachine
}

A StateMachineComponent might include:

  • additional lifecycle methods like componentWillTransition & componentDidTransition
  • a built in this.transition that acts like this.setState but targets this.stateMachine
  • methods that get called by actions, as they do with a wrapped withStatechart

Note that this would also resolve the issue of reusable state machines in #59.

Add props, wrapper component in testStatechart

Given:

import { testStatechart } from 'react-automata'
import { App, statechart } from './App'

test('it works', () => {
  testStatechart({ statechart }, App)
})

Is there any way to supply App with required props, or wrap it in a component like <MemoryRouter/>?

Hook?

Hey, first of all thanks for this project, it's great to see finite state machines gaining traction in react development and i'm a big advocate of them myself!

I'm interested whether this library has plans for offering a hook (e.g. useStateMachine or similar)? I'm interested in creating a solution myself but am not sure if it would be suitable to be included as part of this project, or as a separate package.

My rough idea is to have a useStateMachine(statechart) hook where statechart is an object which is then interpreted by xstate. This would return [State, transition] where State is a component (with is prop that can be a string, array of strings/glob pattern) and transition is a function that executes an action on the current state of the machine (e.g. transition('EVENT')). An 'optional' third value, machineState would also be returned, if the user wishes to tightly couple their UI with the internal machine's state.

Example usage would be similar to:

function App() {
	const statechart = { ... }

	const [State, transition, machineState] = useStateMachine(statechart);

	return (
		<button onClick={() => transition('TOGGLE')}>toggle</button>
		<State is="active">active</State>
		{ machineState === 'inactive' && <span>inactive</span> }
	)
}

State could maybe accept children as a pure React node or as a render prop function with visible (or similar) passed as a parameter.

Invariant violation error when componentState changed but not machineState

Re-pro: https://codesandbox.io/s/ook0p3y23z

If you click on -> b with error button, the flag is locked and you cannot click on -> b button anymore.

I trace down to these methods:

In handleTransition(), this.isTransitioning is turned on

handleTransition = (event, updater) => {
invariant(
!this.isTransitioning,
'Cannot transition on "%s" in the middle of a transition on "%s".',
event,
this.lastEvent
)
this.lastEvent = event
this.isTransitioning = true

In handleComponentDidUpdate(), it supposed to be turned off but it does not because the machineState doesn't change.

handleComponentDidUpdate(prevProps, prevState) {
if (prevState.machineState !== this.state.machineState) {
this.isTransitioning = false

So when another transition triggered, the invariant guard yielded.

Strict mode

Add a strict option which makes the component throw an exception if the current state doesn't support the action.

withStateMachine({ config, strict: true })(App)

Support xstate Machine options parameter

I want to add guards as strings (for visualizer compatibility). This requires passing in an object to the Machine function, but it doesn't look like its possible to pass such a configuration into withStatechart.

This is further described here statelyai/xstate#157

const lightMachine = Machine({
  // ...
  states: {
    green: {
	  on: { TIMER: { yellow: { cond: 'someCond' } } }
	}
  }
}, {
  guards: {
	someCond: (xs, e) => /* evaluate condition here */
  }
});

How to access machine outside React component ?

Hello,
First of all thanks for the project, it's an awesome library. I'm wondering how can one access the machine outside of a React component?
The use case is this:
I have a state machine used to orchestrate an UI flow. The state machine needs to do transitions based on events happening outside a certain React component, eg in a Mobx store, when the route changes etc.
Also the machine receives events from a top level React view.
I'm thinking the library would be much more flexible if it could accept either a state chart spec like it does now or a xtstate Machine object created outside the library.
Ideally i should be able to create the machine based on the state chart and pass it to the component.

Please let me know your thoughts.

componentWillTransition not working

Hi, I'm using react-automata with react-native. I can't for the life of me get componentWillTransition() to fire. I've added it to the same compoent that his wrapped in withStateMachine the view is def transitioning between states. Are there any examples showing how to implement `componentWillTransition/componentDidTransition' in case I'm doing something silly? thanks

Initialising a machine with a different initial state is inert

Currently when you want to initialise a Machine with a different state than specified in the statechart the resulting state is inert and might lack necessary information to be useful to render an ui. That is actions and history (maybe more in the future eg. data).

Might it be better to able to pass a state object such as from new State(stateValue, history?, actions?) to the withStatechart decorated component?

My use case is as stated in this issue statelyai/xstate#31.

Understanding testStateMachine

Firstly - Awesome work here man!! ❤️❤️❤️

Secondly - I am trying to understand testStateMachine from reading the README and I'll admit I'm slightly stumped. So I thought I'd reach out for some help and then maybe I can return the favour by fleshing out the README to be more friendly to slow people, like myself.

The main thing I'm having trouble with is how to pass props into the component. After reading the README I thought that the initialData object in the fixtures would be passed into the component as props, but this doesn't seem to work. Am I missing something?

ReferenceError: statechart is not defined when testing

I made a super simple component, no routing or redux confusion and then a test like it says in the docs and it says " ReferenceError: statechart is not defined" in the test runner console. I'm using jest. Am I doing something wrong?

Awesome project

Just coming here to tell you: awesome project, first tests with it showed good results on the project I'm working on!

We were kinda of implementing it by hand on that project, just reinventing the wheel.

I'm probably going to send a PR to it (https://github.com/pagarme/artis) proposing the usage of react-automata

Question: is there a scheduled time for launching the next version on npm? I saw that it is out of date

🎉

(this is not a issue, just a thank note, feel free to close it after reading)

Give a way to update data (without transition)

This is more of a question, hence an issue not a PR.

I see a lot of use cases where I need to do update the state outside of a transition. It might just be because I'm doing something wrong, but how about an this.props.updateData({} || updater) that gets passed down, similar to second arg of transition. we can event give it a callback, updateData = ({}, callback) => this.setState({}, callback). Any potential problems with this approach?

Update docs: testStateMachine is neither a method nor exported

Hi,

Thanks for making this nice abstraction of xstate for react. I found that in the Readme and in the palyground you import and use the method testStateMachine, which is not exported by react-automata and indeed I can't find it in the source code.

I presume that it has been superceded by testStatechart? If I can help, please let me know.

Just an idea.. Storybook...

Having jest snapshots generated is really nice. What do you think about generating the same kind of thing for storybook ? Great lib ! Thanks :)

Bug: Component gets rerendered when trying to transition to impossible state

First of all, thanks for a great lib. I started to use react-automata in my project and found one issue. When there is an transition to impossible state, the transition doesn't happen, but component still gets rerendered. As for me, in this case nothing should happen. I checked the code, looks like you do setState on wrapper component even if a transition was not possible and this results in rerendering wrapped component.

Here is a codesandbox with example: https://codesandbox.io/s/9lz8jq68ro

Is testStateMachine code included in prod builds?

Hi,

Thanks for the library, it's awesome!

I've started using the library to model a small state machine for a react app and I believe that when doing a production build of my app, the testStateMachine code is included in it. (I'm using webpack to build my app).

Can this be the case?

If it's not the case, sorry for the confusion. In case this is happening, is there a way to prevent this?

This is just a guess, but I think this happens because ./src/testStateMachine.js is required from ./src/index.js. Maybe this could be change to allow users to import it explicitly in the application code with something like

import testStateMachine from "react-automata/testStateMachine"

making the file optional for users, in my case, I want to include it only in dev/test builds.

Let me know if you would accept a PR for making this happen 😃 .

React Native builds failed because of minimatch dependency

Hi! I tried to use react-automata in React Native app (built with Expo), but builds are failed because of minimatch dependency. It tries to import the Node's path library:

path = require('path')

and this fails with the error:

The package at "./node_modules/minimatch/minimatch.js" attempted to import the Node standard library module "path". It failed because React Native does not include the Node standard library.

Everything works as expected, when the require('path') part is commented out. It's imported there only for comparing file paths, in order to support Windows/Unix paths differentiation. Since react-automata doesn't need that functionality, I wonder if it would be possible to implement some other way of comparing State objects than using minimatch (to make react-automata React Native friendly)?

Is it possible to pass some context into an action?

Firstly - Super awesome project!!!

Secondly - I am writing a traffic lights example and I have an action that changes the state from green to orange to red. I expected to see the context of the state that an action was called from in the params of the action but got undefined. This means I have to have all these actions:

...
  changeColorToRed = () => setState({ color: 'red' })
  changeColorToOrange = () => setState({ color: 'orange' })
  changeColorToGreen = () => setState({ color: 'green' })
...

... as opposed to:

...
  changeColor = ({ key }) => setState({ color: key })
...

Is it possible to pass the state context through to the action?

handleTransition's setState batched, actions not called

I'm using withStatechart and this parallel machine (simplified):

export default {
  key: 'controller',
  parallel: true,
  states: {
    NormalizeItems: {
      initial: 'Normalizing',
      states: {
        Normalizing: {
          on: {
            NORMALIZE_SUCCESS: {
              Normalizing: {
                actions: ['startSessionSocket', 'subscribeItems'],
              },
            },
          },
        },
      },
    },
    SessionSocket: {
      initial: 'Disconnected',
      states: {
        Disconnected: {
          on: {
            START_SESSION_SOCKET: {
              Connected: {
                actions: ['joinSessionSocket'],
              },
            },
          },
        },
        Connected: {
          on: {
            SESSION_SOCKET_SUBSCRIBE: {
              Connected: {
                actions: ['emitSubscribeItems'],
              },
            },
          },
        },
      },
    },
  },
}

At some point, the first "child machine" calls startSessionSocket and subscribeItems, which look like this:

startSessionSocket = () => {
  this.props.transition('START_SESSION_SOCKET')
}

subscribeItems = () => {
  this.props.transition('SESSION_SOCKET_SUBSCRIBE')
}

This triggers the second child machine and should also call joinSessionSocket and emitSubscribeItems. However, only the second action (emitSubscribeItems) is called.

I suspect that's because I call one transition after the other, sync, and setState gets batched by React here. And since runActionMethods is called by componentDidUpdate, the joinSessionSocket action is not called.

I understand you may not see this as a bug, and I could possibly fix this by grouping this to one transition / one action, but this happens in more places in the app and I figured such a fix doesn't always result in maintainable code.

BTW: This component is one of the many "units" I have, and it has no UI. So I don't care much about re-renders.

TL;DR: Action methods are called in componentDidUpdate. When React happens to batch multiple setState() calls in handleTransition, the machine itself is in the correct state (thanks to setState(prevState => ...)), but only actions from the last transition are called.

Shrink Import Size, restructure dependencies

Currently react-automata is coming in at 87.9KB (27.3KB gzipped).

Considering React comes in at 7.7KB (3.3KB gzipped), this seems a bit large.


It may be worth moving some dependencies into "devDependencies" or "peerDependencies" where it makes sense.

{
  "dependencies": {
    "glob-to-regexp": "^0.4.0",
    "invariant": "^2.2.4",
    "prop-types": "^15.6.1",
    "react-test-renderer": "^16.4.0",
    "xstate": "^3.3.0"
  }
}

React Test Renderer

In this case, the "react-test-renderer" locks in to a specific version of React (v16.4+). This is bound to cause some conflicts and would probably be better suited as a "peerDependency".

Prop Types

"prop-types" may be better suited as devDependencies.

Actions are not updated when the transition is to the same state

With a statechart that looks like this, where an event triggers a transition to the same state, its associated actions are not updated unless transition is called with the second (data) argument.

const statechart = {
  initial: "login",
  states: {
    login: {
      on: {
        error: {
          login: {
            actions: ["showErrorMessage"]
          }
        },
        success: {
          welcome: {
            actions: ["showSuccessMessage"]
          }
        }
      }
    },
    welcome: {}
  }
};

doesn't work: transition('error')
works: transition('error', {})

Here is a reproduction.

Updated: this works if you pass in an empty object as the second arg to transition

Sponsorship

Hello Michele!

I want to discuss sponsoring react-automata. I really love the <State></State> approach.

We just re-launched https://bridge21.com using react-automata, and it's a huge success.

Mark

Pass parameters to onEnter/onExit methods?

Hi there,

First off, awesome library! I've just started using it and am playing with retrofitting an existing app. Because of some of the architecture in the existing app, it would be really convenient if I could pass parameters along to the methods registered with onEnter and onExit.

For example, I have a sign in component which makes use of a form component. When the submit handler of the form component is triggered, it passes the parent component an object with keys and values for each field. What I used to do is take those values in the onSubmit handler and just pass them directly to fetch, however now what it seems to make sense to do is have the onSubmit handler simply transition to the submitting state. I then have and onEnter handler for the submitting state which will now be responsible for making the call to fetch.

The issue I'm having is, how to best get the form data from the onSubmit handler to the onEnter handler for the submitting state. I know I can add it as a second argument to this.props.transition() to get that data merged into the components props, however having data that should be ephemeral now hanging around on props feels a little gross. The other thing I can do is attach those values to the component's this and then reference them again in the onEnter handler, but this has the same issue.

Ideally I would like to be able to just pass any arbitrary amount of arguments to this.props.transition() to then be passed to the handler method. In my case this would look something like:

const stateChart = {
  initial: 'start',
  states: {
    start: {
      on: {
        SUBMIT: 'submitting',
      },
    },
    submitting: {
      on: {
        SUBMITTING_SUCCESS: 'verified',
        SUBMITTING_FAILURE: 'error',
      },
      onEntry: 'enterSubmitting',
    }
    ...
}

export class SignIn extends Component {
  ...
  onSubmit(formDatat) {
    // we would do something like pass null as the second argument if we didn't want any props merged
    this.props.tranistion('SUBMIT', null, formData)
  }

  enterSubmitting(formData) {
    fetch({
      method: 'POST',
      url: 'foo.bar.com',
      body: formData
    })
     ....
  }
}

My first question is, does this seem reasonable or is there some reason not do this. Also, is there a better way to approach this problem with the existing implementation that I'm not thinking of? Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.