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
@@ -1,13 +1,8 @@
"use client";
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 { privateRoomMachine } from "./private-room-machine";
import type {
@@ -33,64 +28,54 @@ export interface PrivateRoomContextState {
isUnlocking: boolean;
}
const PrivateRoomStateCtx = createContext<PrivateRoomContextState | null>(null);
const PrivateRoomDispatchCtx = createContext<Dispatch<PrivateRoomEvent> | null>(
null,
);
type PrivateRoomSnapshot = SnapshotFrom<typeof privateRoomMachine>;
type PrivateRoomSelector<T> = (snapshot: PrivateRoomSnapshot) => T;
const PrivateRoomActorContext = createActorContext(privateRoomMachine);
export interface PrivateRoomProviderProps {
children: ReactNode;
}
export function PrivateRoomProvider({ children }: PrivateRoomProviderProps) {
const [state, send] = useMachine(privateRoomMachine);
const privateRoomState = useMemo<PrivateRoomContextState>(
() => ({
status: String(state.value),
profile: state.context.profile,
items: state.context.items,
nextCursor: state.context.nextCursor,
hasMore: state.context.hasMore,
creditBalance: state.context.creditBalance,
errorMessage: state.context.errorMessage,
unlockingMomentId: state.context.unlockingMomentId,
unlockErrorMessage: state.context.unlockErrorMessage,
pendingConfirmMomentId: state.context.pendingConfirmMomentId,
unlockPaywallRequest: state.context.unlockPaywallRequest,
unlockSuccessNonce: state.context.unlockSuccessNonce,
isLoading: state.matches("loading"),
isLoadingMore: state.matches("loadingMore"),
isUnlocking: state.matches("unlocking"),
}),
[state],
);
return (
<PrivateRoomStateCtx.Provider value={privateRoomState}>
<PrivateRoomDispatchCtx.Provider value={send}>
{children}
</PrivateRoomDispatchCtx.Provider>
</PrivateRoomStateCtx.Provider>
<PrivateRoomActorContext.Provider>{children}</PrivateRoomActorContext.Provider>
);
}
export function usePrivateRoomState(): PrivateRoomContextState {
const ctx = useContext(PrivateRoomStateCtx);
if (!ctx) {
throw new Error(
"usePrivateRoomState must be used inside <PrivateRoomProvider>",
);
}
return ctx;
return usePrivateRoomSelector(selectPrivateRoomState, shallowEqual);
}
export function usePrivateRoomDispatch(): Dispatch<PrivateRoomEvent> {
const ctx = useContext(PrivateRoomDispatchCtx);
if (!ctx) {
throw new Error(
"usePrivateRoomDispatch must be used inside <PrivateRoomProvider>",
);
}
return ctx;
return PrivateRoomActorContext.useActorRef().send;
}
export function usePrivateRoomSelector<T>(
selector: PrivateRoomSelector<T>,
compare?: (previous: T, next: T) => boolean,
): T {
return PrivateRoomActorContext.useSelector(selector, compare);
}
function selectPrivateRoomState(
state: PrivateRoomSnapshot,
): PrivateRoomContextState {
return {
status: String(state.value),
profile: state.context.profile,
items: state.context.items,
nextCursor: state.context.nextCursor,
hasMore: state.context.hasMore,
creditBalance: state.context.creditBalance,
errorMessage: state.context.errorMessage,
unlockingMomentId: state.context.unlockingMomentId,
unlockErrorMessage: state.context.unlockErrorMessage,
pendingConfirmMomentId: state.context.pendingConfirmMomentId,
unlockPaywallRequest: state.context.unlockPaywallRequest,
unlockSuccessNonce: state.context.unlockSuccessNonce,
isLoading: state.matches("loading"),
isLoadingMore: state.matches("loadingMore"),
isUnlocking: state.matches("unlocking"),
};
}