Te enseño una manera de create un XState Actor y compartirlo globalmente en React usando la API de Context. Lo bueno de este método es que nuestro actor no cambia nunca, de modo que es seguro usarlo como valor del Context global evitando así renderizados innecesarios o renderizados completos del arbol completo de la app cuando el estado global cambia.
import { createModel } from "xstate/lib/model"
type User = {
username: string
id: string
email: string
}
export const authModel = createModel(
{
user: null as User | null,
},
{
events: {
LOG_IN: () => ({}),
LOG_OUT: () => ({}),
},
}
)
export const authMachine = authModel.createMachine({
context: {
user: {
// puedes obtener la info del usuario dentro de la máquina
// Te recomiendo que veas mi post sobre cómo hacerlo (en inglés):
// https://www.horacioh.com/writing/auth-flow-with-xstate-and-react
email: "foo@bar.com",
username: "horacio",
id: "1234567890",
},
},
// ...
})
// ...
const authContext = React.createContext<InterpreterFrom<
typeof authMachine
> | null>(null)
export function useAuth() {
const context = React.useContext(authContext)
if (!context) {
throw new Error(`useAuth must be called inside a AuthProvider`)
}
return context
}
export const AuthProvider = authContext.Provider
import { useSelector } from "@xstate/react"
// ...
export function useUser() {
return useSelector(useAuth(), (state) => state.context.user)
}
import { AuthProvider } from "./auth"
export function App() {
const authService = useInterpret(authMachine)
// ...
return <AuthProvider value={authService}>{/* ... */}</AuthProvider>
}
import { useAuth } from "./auth"
import { useActor } from "@xstate/react"
export function Topbar() {
const authService = useAuth()
const [state, send] = useActor(authService)
// ...
}
BONUS
// machine-utils.ts
import { useSelector } from "@xstate/react"
import * as React from "react"
import { ActionTypes, Interpreter } from "xstate"
export function isNullEvent(eventName: string) {
return eventName == ActionTypes.NullEvent
}
export function isInternalEvent(eventName: string) {
const allEventsExceptNull = Object.values(ActionTypes).filter(
(val) => !isNullEvent(val)
)
return allEventsExceptNull.some((prefix) => eventName.startsWith(prefix))
}
export function createInterpreterContext<
TInterpreter extends Interpreter<any, any, any>
>(displayName: string) {
const [Provider, useContext] =
createRequiredContext<TInterpreter>(displayName)
const createUseSelector =
<Data>(selector: (state: TInterpreter["state"]) => Data) =>
() => {
return useSelector(useContext(), selector)
}
return [Provider, useContext, createUseSelector] as const
}
export function createRequiredContext<TContext>(displayName: string) {
const context = React.createContext<TContext | null>(null)
context.displayName = displayName
function useContext() {
const ctx = React.useContext(context)
if (!ctx) {
throw new Error(
`use${displayName} must be called inside a ${displayName}Provider`
)
}
return ctx
}
return [context.Provider, useContext] as const
}
Y usarlas de esta manera:
// auth-context.ts
import { authStateMachine } from "./auth"
import { InterpreterFrom } from "xstate"
import { createInterpreterContext } from "./machine-utils"
const [AuthProvider, useAuth, createAuthSelector] =
createInterpreterContext<InterpreterFrom<typeof authStateMachine>>("Auth")
export { AuthProvider, useAuth }
export const useUser = createAuthSelector((state) => state.context.user)
Puedes Revisar el código functionando de la primera versión aquí y el de la versión con el bonus aquí