-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Bug or feature request?
Technically a feature request, but I'm about to make a bunch of uninformed claims about React's concurrent mode, so let's call it a proposal draft discussion.
Description:
Once React's concurrent mode is released, @xstate/react
should look into including a useMachine
-style React hook (let's call it useInterpret
) that doesn't use an Interpreter
instance internally, and instead interprets a machine using React's scheduling primitives. The API can be similar (const [state, send] = useInterpret(machine)
), but internally, the state
is stored in React state, send
is implemented with a pure state transition inside a setState
function, and actions are executed from useEffect
.
Motivation:
In React's new concurrent mode, changes to state are scheduled rather than immediately executed, any of those changes might potentially be discarded, and side effects are only run for changes that are "committed". This works because React's state is fully immutable and all state changes are made through pure update functions. On the other hand, the best way to represent a complex statechart in a React component right now is by using an Interpreter
, which contains mutable state, and executes every single event sent to it (using its own scheduler, which React is unaware of).
This doesn't mean that Interpreter
s aren't compatible with concurrent mode. The React team seems interested in bridging the gap between themselves and libraries that use mutable state with useMutableSource
. That would probably work fine with XState, but that wouldn't allow React the optimizations of discarding or re-ordering updates based on priority (e.g. if a user clicks a button in the middle of some other work).
By using React primitives to interpret a statechart, and sticking totally to the "rules" of React, I'd imagine that a user could get the most possible benefits from concurrent mode.
Potential pitfalls:
- I really don't know that much about concurrent mode. It's possible that using
useMutableSource
just ends up being better than this once we actually get to play with it. - A second officially supported interpreter would make new and existing features harder to add and maintain. Also, the differences in behavior between this and
interpret()
might be confusing.
(Feature) Potential implementation:
I have a rough implementation down below in a codesandbox, but the overall gist goes like this:
- The
useInterpret
hook has an internal state object with the latest machine state and a queue of actions to execute. Callingsend
schedules a state update where a new machine state is created from the old one, the new state's actions are bound to that new state, and those actions are pushed to the action queue without being called. This way, the new state, including the side effect functions, is a pure function of the previous state and the given event. - In
useEffect
(which runs after React has "committed" to a certain state), all the bound actions that haven't been called before are called in the order they were added to the queue. Then an update is scheduled to wipe the queue. - Invoked children are kept in a mutable
useRef
object, which is only read from and written to by theuseEffect
mentioned above.
Link to reproduction or proof-of-concept:
https://codesandbox.io/s/pedantic-tdd-nqbgx
(lots of this code was copied from interpreter.tsx
in this repo)