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
+42 -53
View File
@@ -3,14 +3,9 @@
/**
* PaymentContext:基于 XState v5 的 React Context 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 { paymentMachine } from "./payment-machine";
import type {
@@ -37,61 +32,55 @@ export interface PaymentContextState {
isPaid: boolean;
}
const PaymentStateCtx = createContext<PaymentContextState | null>(null);
const PaymentDispatchCtx = createContext<Dispatch<PaymentEvent> | null>(null);
type PaymentSnapshot = SnapshotFrom<typeof paymentMachine>;
type PaymentSelector<T> = (snapshot: PaymentSnapshot) => T;
const PaymentActorContext = createActorContext(paymentMachine);
export interface PaymentProviderProps {
children: ReactNode;
}
export function PaymentProvider({ children }: PaymentProviderProps) {
const [state, send] = useMachine(paymentMachine);
const paymentState = useMemo<PaymentContextState>(
() => ({
status: String(state.value),
plans: state.context.plans,
isFirstRecharge: state.context.isFirstRecharge,
selectedPlanId: state.context.selectedPlanId,
payChannel: state.context.payChannel,
autoRenew: state.context.autoRenew,
agreed: state.context.agreed,
currentOrderId: state.context.currentOrderId,
payParams: state.context.payParams,
orderStatus: state.context.orderStatus,
errorMessage: state.context.errorMessage,
launchNonce: state.context.launchNonce,
isLoadingPlans:
state.matches("loadingCachedPlans") ||
state.matches("loadingPlans") ||
state.matches("refreshingPlans"),
isCreatingOrder: state.matches("creatingOrder"),
isPollingOrder:
state.matches("pollingOrder") || state.matches("waitingForPayment"),
isPaid: state.matches("paid"),
}),
[state],
);
return (
<PaymentStateCtx.Provider value={paymentState}>
<PaymentDispatchCtx.Provider value={send}>
{children}
</PaymentDispatchCtx.Provider>
</PaymentStateCtx.Provider>
);
return <PaymentActorContext.Provider>{children}</PaymentActorContext.Provider>;
}
export function usePaymentState(): PaymentContextState {
const ctx = useContext(PaymentStateCtx);
if (!ctx)
throw new Error("usePaymentState must be used inside <PaymentProvider>");
return ctx;
return usePaymentSelector(selectPaymentState, shallowEqual);
}
export function usePaymentDispatch(): Dispatch<PaymentEvent> {
const ctx = useContext(PaymentDispatchCtx);
if (!ctx)
throw new Error("usePaymentDispatch must be used inside <PaymentProvider>");
return ctx;
return PaymentActorContext.useActorRef().send;
}
export function usePaymentSelector<T>(
selector: PaymentSelector<T>,
compare?: (previous: T, next: T) => boolean,
): T {
return PaymentActorContext.useSelector(selector, compare);
}
function selectPaymentState(state: PaymentSnapshot): PaymentContextState {
return {
status: String(state.value),
plans: state.context.plans,
isFirstRecharge: state.context.isFirstRecharge,
selectedPlanId: state.context.selectedPlanId,
payChannel: state.context.payChannel,
autoRenew: state.context.autoRenew,
agreed: state.context.agreed,
currentOrderId: state.context.currentOrderId,
payParams: state.context.payParams,
orderStatus: state.context.orderStatus,
errorMessage: state.context.errorMessage,
launchNonce: state.context.launchNonce,
isLoadingPlans:
state.matches("loadingCachedPlans") ||
state.matches("loadingPlans") ||
state.matches("refreshingPlans"),
isCreatingOrder: state.matches("creatingOrder"),
isPollingOrder:
state.matches("pollingOrder") || state.matches("waitingForPayment"),
isPaid: state.matches("paid"),
};
}