Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: a React-specific interpreter hook #1094

Closed
jeremy-deutsch opened this issue Mar 24, 2020 · 8 comments
Closed

Proposal: a React-specific interpreter hook #1094

jeremy-deutsch opened this issue Mar 24, 2020 · 8 comments

Comments

@jeremy-deutsch
Copy link

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 Interpreters 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. Calling send 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 the useEffect 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)

@Andarist
Copy link
Member

I'm very interested in this area and thought about this extensively. However, I don't quite see right now why we'd like, for example, to delay the execution of particular actions. Machines, even if tied to some component, are still living beside components - it's like a parallel system in my head (which only integrates with React). Queuing etc also makes things a lot harder to reason about and I'm not sure if it's worth the trouble.

Note also that machines tied to components should mainly respond to user interactions and even when using plain React you don't expect the handler for, let's say, onClick to fire after a delay (in React's commit phase), but rather immediately. XState's actions are just that - those are handlers for external inputs.

@jeremy-deutsch
Copy link
Author

Yeah, I think I see your point. I don’t really think of it as “delaying” the actions, but it would definitely have that effect some (or a lot) of the time.

I don’t know how often in practice it’s important that side effects fire immediately, though. In the case you gave, where an event gets sent by a user interaction, the state and/or context would still be updated at a high priority (almost synchronously). Would that not be enough to satisfy the user that gave the input? What other actions could that transition produce that need to run at a high priority (or else)?

The ability to reason about the interpreter’s behavior is also definitely a real concern. That might become easier as our mental model of the concurrent mode APIs gets better, but it also might not.

@jeremy-deutsch
Copy link
Author

@davidkpiano Just saw useEffectReducer - does the "scheduling side effects with useEffect" approach used there mean you're likely to do something similar with useMachine when concurrent mode becomes stable?

@davidkpiano
Copy link
Member

@davidkpiano Just saw useEffectReducer - does the "scheduling side effects with useEffect" approach used there mean you're likely to do something similar with useMachine when concurrent mode becomes stable?

Already done. Check out @xstate/react@next - it handles actions through useEffect rather than just firing them.

@jeremy-deutsch
Copy link
Author

Already done. Check out @xstate/react@next - it handles actions through useEffect rather than just firing them.

That's really cool! Looking over the useActor code now - looks like I was a bit behind the curve 😄

This is the branch, right?

@davidkpiano
Copy link
Member

Yep! That's the branch. Still a couple things left to fix, though.

@jeremy-deutsch
Copy link
Author

Cool stuff! Definitely a lot less extreme than reimplementing interpret() in a React hook.

@davidkpiano
Copy link
Member

Added to Roadmap

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants