Closed davidkpiano closed 1 year ago
Overview:
import { createHooks } from '@xstate/react';
import { authMachine } from './authMachine';
import { interpret } from 'xstate';
// Interpret the machine first (full interpretation control)
const authService = interpret(authMachine, {/* ... */}).start();
// Create a "hooks object" from the actor
const auth = createHooks(authService);
// ...
const SomeForm = () => {
// Get full state snapshot and send() function
const [state, send] = auth.useActor();
// Get selected state
const isLoggedIn = auth.useSelector(state => state.hasTag('authorized'));
// ...
}
I think the proposed API looks good, and I, for one, would like to remove React context for my shared machines 👍🏻
A few thoughts come to mind:
<Auth.LoggedIn>{access to secret state here}</Auth.LoggedIn>
. Would this be a concern using globally available hooks? I guess not since Zustand, Recoil, etc., already does this.Thanks for the feedback @mellson!
Re: the React context comparison, those are valid points. I'm going to research how Zustand handles this (Recoil provides a Provider), but we can add this to the API instead of having it "always global":
const auth = createHooks(authService);
export function App() {
// ...
return <auth.Provider>
<MainPage />
</auth.Provider>
}
export function Component() {
const [state, send] = auth.useActor();
// ...
}
This doesn't add too much to the proposed API, and has the benefits of providing a "scope" only where you need it.
The proposed API looks good in general. Simple and intuitive. I do agree with the point @mellson pointed out about the scope.
I second @Andarist's questions about .start
. What's the best place for it to be called, considering SSR and Concurrent React? We need what useInterpreter
does for global singleton actors.
This will also play nicely with the system receptionist RFC.
And here's a link to a playground I made using the proposed API: https://codesandbox.io/s/xstate-rfc-shared-state-with-global-hooks-for-actors-ybxc5l?file=/src/App.tsx
Pull request made: https://github.com/statelyai/xstate/pull/3401
@mattpocock Can you add your comments from that PR to this RFC? Specifically with regard to this:
Instead, createHooks should work based off a machine definition, not a running service. This solves both problems, but will require some API changes.
My concern with tightly coupling the idea of a provider with a running machine is that, in the case of a global store that might consist of many machines, you could end up with deeply nested providers that imply some hierarchy where there is none. I'm trying to think of other common use cases where this might be a problem. But effectively, you could end up with this:
const auth = createHooks(authService);
const user = createHooks(userService);
const settings = createHooks(settingsService);
export function App() {
// ...
return <auth.Provider>
<user.Provider>
<settings.Provider>
<MainPage />
</settings.Provider>
</user.Provider>
</auth.Provider>
}
export function Component() {
const [state, send] = auth.useActor();
// ...
}
But if instead you could compose services together under a single provider, it could make for a more extendable API
const StoreProvider = createProvider({
auth: createHooks(authService),
user: createHooks(userService),
settings: createHooks(settingsService),
});
export const useStoreProvider = StoreProvider.useProvider;
export function App() {
// ...
return <StoreProvider>
<MainPage />
</StoreProvider>
}
export function Component() {
const { auth } = useStoreProvider();
const [state, send] = auth.useActor();
// ...
}
This would add more boilerplate though in the case of one-off machines that simply need a single provider. It might also further complicate the question on when to call .start()
on the provided services. I'm also unsure if it introduces the problem of excessive rerenders for downstream consumers of the provider, if they might rerender from changes to an unrelated machine in the provider that they do not care about.
I added details on how the execution of the actor can be controlled. This maintains the original idea of giving the developer full control over when the actor is started:
// ...
// Actor not started yet
const authService = interpret(authMachine, {/* ... */});
const auth = createHooks(authService);
const App = () => {
// If you must start the actor in a useEffect...
useEffect(() => {
auth.actorRef.start();
// Since it's a global actor, no need for cleanup
}, []);
const [state, send] = auth.useActor();
// ...
}
This is now released as createActorContext()
Rendered text