Closed ShMcK closed 6 years ago
Thank you very much for opening this issue, @ShMcK. This is an interesting problem, and I don't mind to release a breaking change as long as it adds value for the users of the library.
However, I would like to understand more what are the needs behind this change. Can you please provide a real-world use-case?
There's a saying "when you get a new hammer, everything looks like a nail." I think it summarizes my issue with trying to use the new Provider/Consumer Context API with "react-automata".
The old context API can allow more flexibility, and may be worth keeping. It gives you access to the state machine through this.context
, whereas the new context API expects you to explicitly pass down your props. Moving to the new Context API would break <State />
& <Action />
, or they would have to shift to explicitly receiving the statechart value.
The problem I have is that I would like to reuse a state machine with connected components in different parts of the component tree. I'm working on a complex interface, somewhat similar to photoshop, where toolbars by necessity are located apart.
I realize now there is a much easier fix for this that grants the same flexibility.
const a = withStatechart(statechart)(A)
const b = withStatechart(statechart)(B)
In the above example, a
& b
are not using the same state machine. They will not be in sync. "react-automata" internally generates the Machine, making each a different instance.
An easier fix to allow more flexibility would be to move the creation of the state machine outside of "react-automata":
import { Machine } from 'xstate'
const statechart = Machine({ /* ... */ })
const a = withStatechart(statechart)(A)
const b = withStatechart(statechart)(B)
This might be worth considering.
Thank you very much @ShMcK for providing more information. It's already possible to create a machine and pass it to the higher-order component.
However, I'm not 100% sure if you only want to reuse a machine or you also want the components' states to be in sync. I'm asking this because given that xstate
is stateless, creating the machine outside doesn't help with the "sync" part.
@MicheleBertoli I may be missing something, but as I see it (and according to what you guys exchanged here) the only way of keeping a machine in sync among different components in react-automata is passing it as a prop
to others, right? As it doesn't make use of the newer context API?
In that case the added lifecycle hooks provided by withStateChart
is limited to the top-most component only, as even when provided the same machine instance it's unaware of it's state due to it being stateless?
I'm still trying to get my head around react-automata's API and how it combines with xstate in general, as you may have noticed... 😄
I've written a somewhat incomplete and unpublished article on the topic, feel free to have a look and drop any feedback. I hope it helps. https://medium.com/@ShMcK/communicating-between-state-machines-components-33e6ab754605
@alexandrethsilva, thank you very much for your comment. @ShMcK, thank you very much for writing an article about this.
I think there's a little bit of confusion around the relation between the new context API, sharing the machine state between multiple components, and the concept of external state machines.
In fact, it's already possible to build a Provider-like structure with react-automata
, although the library uses the legacy context APIs (which is, and should be, an implementation detail).
For example, here is an app that looks like the example from the article, where multiple AlarmClock
components are "in sync":
import { Action, withStatechart } from 'react-automata'
const statechart = { // ... }
const AlarmClock = () => (
<>
<Action show="startRing">
<Ringing />
</Action>
<Clock />
</>
)
const Component = ({ children }) => children
const AlarmMachine = {
Provider: withStatechart(statechart)(Component),
}
const App = () => (
<AlarmMachine.Provider>
<>
<AlarmClock />
<AlarmClock />
</>
</AlarmMachine.Provider>
)
However, I don't see any difference with the following code, which is what I recommend, and provides the same behaviour:
import { Action, withStatechart } from 'react-automata'
const statechart = { // ... }
const AlarmClock = () => (
<>
<Action show="startRing">
<Ringing />
</Action>
<Clock />
</>
)
const Component = () => (
<>
<AlarmClock />
<AlarmClock />
</>
)
const App = withStatechart(statechart)(Component)
The main differences between react-automata
and xstateful-react
are two:
react-automata
uses the components methods to respond to actions, while xstateful-react
offers a reducer-pattern.
xstateful-react
's Provider
passes the machine state
, extstate
, and transition
to the <Machine.* />
components. react-automata
doesn't, but the same behaviour can be reproduced in userland, if needed.
Here's a variation of the previous example, that uses the new context API:
import { withStatechart } from 'react-automata'
const AutomataContext = React.createContext('automata')
const AlarmClock = () => (
<AutomataContext.Consumer>
{({ machineState, transition }) => // ...
</AutomataContext.Consumer>
)
const Component = props => (
<AutomataContext.Provider value={props}>
<>
<AlarmClock />
<AlarmClock />
</>
</AutomataContext.Provider>
)
const App = withStatechart(statechart)(Component)
If number 2 is what generates this sort of confusion and that's what you mean by "External State Machines", I'm happy to implement it at the library-level.
A couple of notes on the article, @ShMcK:
Snoozing
(as opposed to Snooze
).react-automata
action methods), that's correct. However, the reason they don't receive parameters is that they can access those information through the props.I hope this helps/makes sense, and I'm happy to discuss further.
@MicheleBertoli I have a question to your last comment regarding the example with react's new context API:
The AlarmClock
components receive the props (machineState
, transition
) via the AutomataContext.Provider
.
But what about the lifecycle methods? Those are only added to the AutomataContext.Provider
and not to the nested AlarmClock
components, or?
And what about the onEntry
actions defined in the state chart? Those should be called on the AlarmClock
components, but I think with this example they also would be called on the AutomataContext.Provider
.
Thank you very much for your comment, @rumtraubenuss. I'm more than happy to answer your questions.
It's correct when you say that lifecycle hooks and the action methods are fired only in the component wrapped into withStatechart
(the Provider
in this case).
You should consider that componentWillTransition
is fired right before componentWillReceiveProps
(now deprecated) and componentDidTransition
is basically componentDidUpdate
so their behaviour can be easily replicated in the tree.
In fact, given that the returned component is a regular React component (with a few special props and behaviours), nothing stops you from using common techniques (e.g. passing down props) to trigger "reactions" in children.
I'm under the impression that the fact this library is just a thin "bridge" between xstate
and React is a source of confusion.
Also, a possible "workaround" would be to use State
and Action
callbacks (not recommended), for example:
<State value="whatever" onShow={this.onEntry} onHide={this.onExit} />
<Action show="whatever" onShow={this.onAction} />
I hope this help, and it would be great if you could share your use-cases so that we can discuss further.
@MicheleBertoli thanks a lot for your extensive feedback on these last questions we posted. I didn't have time last week to get back to you, but appreciated very much your reply.
My question after it would be exactly what @rumtraubenuss raised and I agree with you that may have been what confused me at first, as when seeing the description of the additional lifecycle hooks I thought it was intended to be the standard/recommended way of interacting with the automata.
When thinking about it now, after going a few times through this thread to wrap my head around it, I wonder if these hooks are a meaningful part of the library of if their absence could actually contribute to clarifying the idea of this library having only the role of the "thin bridge" you mentioned?
Maybe without them I would have felt less compelled to use the HOC in every component which interacted with the automata, which in turn led me to think that simply having it available on the context level would be incorrect or a not recommended use of it.
I hope I didn't get too lost when making my point? Does it make some sense? And, ultimately, what would be your thoughts on removing the hooks and adding some of the examples you mentioned in this thread to the docs? I'd be glad to contribute with a PR if you think that'd make sense.
Thank you very much @alexandrethsilva for providing more context.
I'm glad the discussion was useful, and thanks for confirming that the lifecycle hooks are a source of confusion.
The cause might also be the Provider
/connect
approach enforced by react-redux
.
I'm currently in the process of rewriting some parts of the library (better APIs, improved performance, support actions/activities) and the README. I will follow your suggestion, and I'll add a FAQ section with the examples from this thread and more.
I don't think we should remove features, as they are useful (e.g. logging in componentDidTransition
), but we need to provide clearer information.
I'll ping you when the new stuff is ready, and please feel free to contribute to it.
Here's the new FAQ section, @alexandrethsilva. I hope it's useful, and please feel free to contribute to it.
Hi @MicheleBertoli, sorry for the delay in replying. 😬
Thanks a lot for the continuous feedback on this. I think the confusion may indeed have been influenced by the way react-redux
does things with connect
. And I also see your point when you talk about not removing features which may be useful.
Really nice to read that you're bringing more updated to the lib. I've been enjoying using it quite a lot.
And last but not least, thanks for the new FAQ! Whenever something pops that I think may be useful I'll open an issue here to discuss and a subsequent PR if it makes sense 👍
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.A pattern like this may be worth exploring in later releases of "react-automata". Unfortunately, it would currently result in some major breaking changes.