I've been creating an API to help manage state machines in React.
It consists of three components:
<StateMachine>
: Receives anxstate
machine as a prop, and sets up a context for deeper components to use.<StateView>
: Receives two props:state
&children
, and renders its children only if that state is currently active.<StateControl>
: Receives some arbitrary props - each being an event used to transition the machine - and converts them to transition callbacks to be passed down to itschildren
(which is NOT an element, but anelementType
as determined byPropTypes
).
Here is a visual representation of what is at play:
Using React's context API, I can flexibly switch on/off nodes in the React tree based on the state of the machine. Here's a sample code snippet demonstrating this:
const MyMachine = () =>
{
return (
<StateMachine machine={sampleMachine}>
<StateView state="initializing">
<StateControl onSuccess="success">
{MySampleInitializer}
</StateControl>
</StateView>
<StateView state="initialized">
<p>{"App initialized"}</p>
</StateView>
</StateMachine>
);
This works great! When the machine is in the "initializing" state, MySampleInitializer
gets rendered. When initialization is complete, onSuccess
is called which transitions the machine to "initialized". At this point, the <p>
gets rendered.
Now the problem:
In most situations, each "state view" would render a different component (which gets created & mounted when the appropriate state becomes active).
However what if we wanted to apply the machine to a single component only? For example, I have a <Form>
component which handles the rendering of some form elements, and should receive different props depending on the state the form is currently in.
const MyFormMachine = () =>
{
return (
<StateMachine machine={formMachine}>
<StateView state="unfilled">
<StateControl onFill="filled">
{(props) => <MyForm {...props} disableSubmit/>}
</StateControl>
</StateView>
<StateView state="filled">
<StateControl onClear="unfilled" onSubmit="submit">
{(props) => <MyForm {...props}/>}
</StateControl>
</StateView>
<StateView state="submitting">
<MyForm disableInput disableSubmit showSpinner/>
</StateView>
</StateMachine>
);
Using my current API, rendering a <MyForm>
within each <StateView>
will cause <MyForm>
to be re-mounted anytime a state change happens (thereby destroying any internal state associated with it). The DOM nodes themselves will also be re-mounted, which may re-trigger things like autofocus
(for instance).
I was hoping there may be a way to share the same <MyForm>
instance across the various "views" such that this re-mounting does not occur. Is this possible? If not, is there an alternative solution which would fit with this API?
Any help greatly appreciated.
PS: If the question title is unsuitable, please suggest a change so that this question may more accessible. Thanks