refactor: scope providers by route and use XState selectors

This commit is contained in:
2026-07-13 19:07:53 +08:00
parent f9b5f22ee9
commit 37ae152abb
24 changed files with 512 additions and 336 deletions
+32 -47
View File
@@ -1,21 +1,10 @@
"use client";
/**
* UserContext:基于 XState v5 的 React Context Provider
*
* 业务事件通过 `useMachine(userMachine).send()` 派发。
* XState 自动处理异步副作用(invoke + fromPromise actors)。
*
* 兼容性:保持原 `useUserState()` / `useUserDispatch()` API。
* 内部将 XState 状态机快照映射回原 `UserState` 形状。
* UserContext:基于 XState actor context 和 selector 的 React Provider
*/
import {
type Dispatch,
type ReactNode,
createContext,
useContext,
useMemo,
} from "react";
import { useMachine } from "@xstate/react";
import type { Dispatch, ReactNode } from "react";
import { createActorContext, shallowEqual } from "@xstate/react";
import type { SnapshotFrom } from "xstate";
import { userMachine } from "./user-machine";
import type { UserState as MachineContext, UserEvent } from "./user-machine";
@@ -31,48 +20,44 @@ interface UserState {
creditBalance: number;
}
const UserStateCtx = createContext<UserState | null>(null);
const UserDispatchCtx = createContext<Dispatch<UserEvent> | null>(null);
type UserSnapshot = SnapshotFrom<typeof userMachine>;
type UserSelector<T> = (snapshot: UserSnapshot) => T;
const UserActorContext = createActorContext(userMachine);
export interface UserProviderProps {
children: ReactNode;
}
export function UserProvider({ children }: UserProviderProps) {
const [state, send] = useMachine(userMachine);
// 映射 XState 状态机快照 → 原 UserState 形状
const userState = useMemo<UserState>(
() => ({
currentUser: state.context.currentUser,
isLoading:
state.matches("initializing") ||
state.matches("fetching") ||
state.matches("loggingOut") ||
state.context.isLoading,
avatarUrl: state.context.avatarUrl,
isVip: state.context.isVip,
creditBalance: state.context.creditBalance,
}),
[state],
);
return (
<UserStateCtx.Provider value={userState}>
<UserDispatchCtx.Provider value={send}>{children}</UserDispatchCtx.Provider>
</UserStateCtx.Provider>
);
return <UserActorContext.Provider>{children}</UserActorContext.Provider>;
}
export function useUserState(): UserState {
const ctx = useContext(UserStateCtx);
if (!ctx) throw new Error("useUserState must be used inside <UserProvider>");
return ctx;
return useUserSelector(selectUserState, shallowEqual);
}
export function useUserDispatch(): Dispatch<UserEvent> {
const ctx = useContext(UserDispatchCtx);
if (!ctx)
throw new Error("useUserDispatch must be used inside <UserProvider>");
return ctx;
return UserActorContext.useActorRef().send;
}
export function useUserSelector<T>(
selector: UserSelector<T>,
compare?: (previous: T, next: T) => boolean,
): T {
return UserActorContext.useSelector(selector, compare);
}
function selectUserState(state: UserSnapshot): UserState {
return {
currentUser: state.context.currentUser,
isLoading:
state.matches("initializing") ||
state.matches("fetching") ||
state.matches("loggingOut") ||
state.context.isLoading,
avatarUrl: state.context.avatarUrl,
isVip: state.context.isVip,
creditBalance: state.context.creditBalance,
};
}