Xstate: Allow useMachine to accept a new machine between renders

Created on 28 Mar 2020  路  4Comments  路  Source: davidkpiano/xstate

Bug or feature request?

Feature request: @xstate/react

Description:

Currently useMachine accepts only the first machine instance passed to it and logs a warning if a new instance is passed to it between renders. This prevents the use of a dynamically created machine derived from props. One such example is the createrCounterMachine factory function from the context docs. If the initial count is a prop and you intend to create a new machine and interpret whenever the count prop changes, this is not supported.

Workaround

One way around this is to leverage the useService hook as it supports resubscribing to a new service when the instance changes between renders. This is essentially how I am currently handling this. Note that useFirstMountState comes from react-use and is used to prevent overwriting the service created on the first render without stopping it.

const machine = useMemo(() => createrCounterMachine(count), [count]);
const [service, setService] = useState(() => interpret(machine).start());
const [state, send] = useService(service);
const isFirstMount = useFirstMountState();

useEffect(() => {
  if (!isFirstMount) {
    setService(interpret(machine).start());
  }

  return () => {
    service.stop();
  };
}, [machine]);

(Feature) Potential implementation:

The use-case described above could be accomplished in two different ways without an API change.

  • useMachine subscribes to changes to the context value passed in the options object.
  • useMachine subscribes to changes to the passed machine.

Although the former is a simpler API for the demonstrated use-case, it would be a breaking change as passing an object literal as the context value would result in the machine being reset between each render.

The latter would likely be a better solution as it is more generic and I believe would not qualify as a breaking change.

One potential solution could also be to introduce a new hook that explicitly supports an updatable machine instance as to avoid any changes in behavior with the existing useMachine hook.

Link to reproduction or proof-of-concept:

https://codesandbox.io/s/use-machine-6jg8j

has workaround question 鈿涳笌 @xstatreact

Most helpful comment

@jtmthf Here's a more idiomatic way you can alternatively do this:

// if the machine changes, and has a different ID (which it should),
// the component will reinitialize with a new machine.
<MyComponent machine={someMachine} key={someMachine.id} />

// ...

function MyComponent({ machine }) {
  // Just like normal
  const [state, send] = useMachine(machine);

  // ...
}

All 4 comments

Hey @jtmthf - this is one of those "can" vs. "should" questions.

Can we allow the machine to be dynamic? Absolutely, it would be very easy to support this.

Should we? Probably not.

Machines that describe the behavior/logic of a component should be as static and deterministic as possible. If we introduce functionality that allows the hot replacement of a machine at any time, then that defeats the entire purpose of using state machines in the first place.

Well, it would be great if we could support preserving the old machine instance (to keep its state) if we could compare the previous and current configs (to check if they are the same) and if they wouldnt be the same then we should force the whole component to reinitialize (with the new machine). This, of course, would only apply to the hot module replacement scenario. I'm not sure though how to hook into it. Also - the described logic actually matches how this works in React (roughly). It tries to keep the old state in a best-effort manner but when it recognizes that it just can't be done then it remounts the component (or refresh the whole page, this im not 100% sure).

@davidkpiano That makes sense. Perhaps I'm not approaching my problem in the correct way. In my situation, the value I need to initialize the context with is provided asynchronously. I imagine suspense will eventually solve this problem, but in the meantime I need to explore alternatives. Would a better solution for this use-case be to provide an internal transition for updating the context?

@jtmthf Here's a more idiomatic way you can alternatively do this:

// if the machine changes, and has a different ID (which it should),
// the component will reinitialize with a new machine.
<MyComponent machine={someMachine} key={someMachine.id} />

// ...

function MyComponent({ machine }) {
  // Just like normal
  const [state, send] = useMachine(machine);

  // ...
}
Was this page helpful?
0 / 5 - 0 ratings