refactor: scope providers by route and use XState selectors
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user