React hooks and utilities for integrating XState finite state machines and statecharts into React applications
The context utilities provide a way to share XState actors across React component trees using React Context. This is ideal for global state management and avoiding prop drilling.
Creates a React context with provider and context-bound hooks for sharing an XState actor across components.
function createActorContext<TLogic extends AnyActorLogic>(
actorLogic: TLogic,
actorOptions?: ActorOptions<TLogic>
): {
useSelector: <T>(
selector: (snapshot: SnapshotFrom<TLogic>) => T,
compare?: (a: T, b: T) => boolean
) => T;
useActorRef: () => Actor<TLogic>;
Provider: React.ComponentType<{
children: React.ReactNode;
options?: ActorOptions<TLogic>;
logic?: TLogic;
}>;
};actorLogic: The XState actor logic to be sharedactorOptions: Default actor configuration optionsAn object containing:
useSelector: Context-bound selector hookuseActorRef: Context-bound actor reference hookProvider: React component to provide the actor contextimport { createActorContext } from "@xstate/react";
import { createMachine, assign } from "xstate";
// Define your machine
const authMachine = createMachine({
id: "auth",
initial: "loggedOut",
context: {
user: null,
token: null
},
states: {
loggedOut: {
on: {
LOGIN: {
target: "loggedIn",
actions: assign({
user: ({ event }) => event.user,
token: ({ event }) => event.token
})
}
}
},
loggedIn: {
on: {
LOGOUT: {
target: "loggedOut",
actions: assign({
user: null,
token: null
})
}
}
}
}
});
// Create the context
const AuthContext = createActorContext(authMachine);
// App component with provider
function App() {
return (
<AuthContext.Provider>
<Navigation />
<MainContent />
</AuthContext.Provider>
);
}
// Components that use the context
function Navigation() {
const user = AuthContext.useSelector((state) => state.context.user);
const isLoggedIn = AuthContext.useSelector((state) => state.matches("loggedIn"));
const authActor = AuthContext.useActorRef();
if (!isLoggedIn) {
return (
<nav>
<button onClick={() => authActor.send({
type: "LOGIN",
user: { name: "John" },
token: "abc123"
})}>
Login
</button>
</nav>
);
}
return (
<nav>
<span>Welcome, {user.name}!</span>
<button onClick={() => authActor.send({ type: "LOGOUT" })}>
Logout
</button>
</nav>
);
}
function MainContent() {
const isLoggedIn = AuthContext.useSelector((state) => state.matches("loggedIn"));
return (
<main>
{isLoggedIn ? <Dashboard /> : <LoginPrompt />}
</main>
);
}The Provider component created by createActorContext accepts the following props:
interface ProviderProps<TLogic extends AnyActorLogic> {
children: React.ReactNode;
options?: ActorOptions<TLogic>;
logic?: TLogic;
/** @deprecated Use `logic` instead. */
machine?: never;
}children: React children to receive the contextoptions: Optional actor configuration options (overrides defaults)logic: Optional different actor logic (overrides default)function App() {
return (
<AuthContext.Provider
options={{
input: { apiUrl: "https://api.example.com" }
}}
>
<AppContent />
</AuthContext.Provider>
);
}const devAuthMachine = createMachine({
// Development version with mock data
});
function App() {
const isDevelopment = process.env.NODE_ENV === "development";
return (
<AuthContext.Provider
logic={isDevelopment ? devAuthMachine : authMachine}
>
<AppContent />
</AuthContext.Provider>
);
}The context-bound useSelector hook works identically to the standalone version but automatically uses the actor from context.
const contextUseSelector: <T>(
selector: (snapshot: SnapshotFrom<TLogic>) => T,
compare?: (a: T, b: T) => boolean
) => T;The context-bound useActorRef hook returns the actor reference from context.
const contextUseActorRef: () => Actor<TLogic>;The context hooks will throw an error if used outside of their corresponding Provider:
function ComponentOutsideProvider() {
// This will throw an error
const user = AuthContext.useSelector((state) => state.context.user);
// Error: You used a hook from "ActorProvider" but it's not inside a <ActorProvider> component.
}You can create and use multiple actor contexts in the same application:
const AuthContext = createActorContext(authMachine);
const ThemeContext = createActorContext(themeMachine);
const CartContext = createActorContext(cartMachine);
function App() {
return (
<AuthContext.Provider>
<ThemeContext.Provider>
<CartContext.Provider>
<AppContent />
</CartContext.Provider>
</ThemeContext.Provider>
</AuthContext.Provider>
);
}
function AppContent() {
const user = AuthContext.useSelector((state) => state.context.user);
const theme = ThemeContext.useSelector((state) => state.context.theme);
const cartItems = CartContext.useSelector((state) => state.context.items);
// Use all three contexts...
}// Custom hook for auth operations
function useAuth() {
const user = AuthContext.useSelector((state) => state.context.user);
const isLoggedIn = AuthContext.useSelector((state) => state.matches("loggedIn"));
const authActor = AuthContext.useActorRef();
const login = useCallback((credentials) => {
authActor.send({ type: "LOGIN", ...credentials });
}, [authActor]);
const logout = useCallback(() => {
authActor.send({ type: "LOGOUT" });
}, [authActor]);
return { user, isLoggedIn, login, logout };
}// Memoize selectors for complex computations
const memoizedSelector = useMemo(
() => (state) => expensiveComputation(state.context.data),
[]
);
const result = AuthContext.useSelector(memoizedSelector);function FeatureComponent({ useGlobalState }: { useGlobalState: boolean }) {
if (useGlobalState) {
const data = GlobalContext.useSelector((state) => state.context.data);
return <div>{data}</div>;
}
// Use local state instead
const [localData] = useState("local");
return <div>{localData}</div>;
}Install with Tessl CLI
npx tessl i tessl/npm-xstate--react