refactor: scope providers by route and use XState selectors
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useMachine } from "@xstate/react";
|
||||
import { type Dispatch, type ReactNode, useMemo } from "react";
|
||||
import { createActorContext, shallowEqual } from "@xstate/react";
|
||||
import type { SnapshotFrom } from "xstate";
|
||||
|
||||
import { chatMachine } from "./chat-machine";
|
||||
import type { ChatEvent, ChatState as MachineContext } from "./chat-machine";
|
||||
@@ -38,57 +33,56 @@ interface ChatState {
|
||||
unlockPaywallRequest: MachineContext["unlockPaywallRequest"];
|
||||
}
|
||||
|
||||
const ChatStateCtx = createContext<ChatState | null>(null);
|
||||
const ChatDispatchCtx = createContext<Dispatch<ChatEvent> | null>(null);
|
||||
type ChatSnapshot = SnapshotFrom<typeof chatMachine>;
|
||||
type ChatSelector<T> = (snapshot: ChatSnapshot) => T;
|
||||
|
||||
const ChatActorContext = createActorContext(chatMachine);
|
||||
|
||||
export interface ChatProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ChatProvider({ children }: ChatProviderProps) {
|
||||
const [state, send] = useMachine(chatMachine, {});
|
||||
|
||||
// 映射 XState 状态机快照 → 原 ChatState 形状
|
||||
const chatState = useMemo<ChatState>(
|
||||
() => ({
|
||||
messages: appendPromotionMessage(
|
||||
state.context.messages,
|
||||
state.context.promotion,
|
||||
),
|
||||
historyMessages: state.context.messages,
|
||||
promotion: state.context.promotion,
|
||||
isReplyingAI: state.context.isReplyingAI,
|
||||
upgradePromptVisible: state.context.upgradePromptVisible,
|
||||
upgradeReason: state.context.upgradeReason,
|
||||
historyLoaded: state.context.historyLoaded,
|
||||
unlockHistoryPromptVisible: state.context.unlockHistoryPromptVisible,
|
||||
lockedHistoryCount: state.context.lockedHistoryCount,
|
||||
isUnlockingHistory: state.context.isUnlockingHistory,
|
||||
unlockHistoryError: state.context.unlockHistoryError,
|
||||
isUnlockingMessage: state.context.isUnlockingMessage,
|
||||
unlockingMessageId: state.context.unlockingMessageId,
|
||||
unlockMessageError: state.context.unlockMessageError,
|
||||
unlockPaywallRequest: state.context.unlockPaywallRequest,
|
||||
}),
|
||||
[state],
|
||||
);
|
||||
|
||||
return (
|
||||
<ChatStateCtx.Provider value={chatState}>
|
||||
<ChatDispatchCtx.Provider value={send}>{children}</ChatDispatchCtx.Provider>
|
||||
</ChatStateCtx.Provider>
|
||||
);
|
||||
return <ChatActorContext.Provider>{children}</ChatActorContext.Provider>;
|
||||
}
|
||||
|
||||
export function useChatState(): ChatState {
|
||||
const ctx = useContext(ChatStateCtx);
|
||||
if (!ctx) throw new Error("useChatState must be used inside <ChatProvider>");
|
||||
return ctx;
|
||||
const selected = useChatSelector(selectChatState, shallowEqual);
|
||||
const messages = useMemo(
|
||||
() => appendPromotionMessage(selected.historyMessages, selected.promotion),
|
||||
[selected.historyMessages, selected.promotion],
|
||||
);
|
||||
return useMemo(() => ({ ...selected, messages }), [messages, selected]);
|
||||
}
|
||||
|
||||
export function useChatDispatch(): Dispatch<ChatEvent> {
|
||||
const ctx = useContext(ChatDispatchCtx);
|
||||
if (!ctx)
|
||||
throw new Error("useChatDispatch must be used inside <ChatProvider>");
|
||||
return ctx;
|
||||
return ChatActorContext.useActorRef().send;
|
||||
}
|
||||
|
||||
export function useChatSelector<T>(
|
||||
selector: ChatSelector<T>,
|
||||
compare?: (previous: T, next: T) => boolean,
|
||||
): T {
|
||||
return ChatActorContext.useSelector(selector, compare);
|
||||
}
|
||||
|
||||
type SelectedChatState = Omit<ChatState, "messages">;
|
||||
|
||||
function selectChatState(state: ChatSnapshot): SelectedChatState {
|
||||
return {
|
||||
historyMessages: state.context.messages,
|
||||
promotion: state.context.promotion,
|
||||
isReplyingAI: state.context.isReplyingAI,
|
||||
upgradePromptVisible: state.context.upgradePromptVisible,
|
||||
upgradeReason: state.context.upgradeReason,
|
||||
historyLoaded: state.context.historyLoaded,
|
||||
unlockHistoryPromptVisible: state.context.unlockHistoryPromptVisible,
|
||||
lockedHistoryCount: state.context.lockedHistoryCount,
|
||||
isUnlockingHistory: state.context.isUnlockingHistory,
|
||||
unlockHistoryError: state.context.unlockHistoryError,
|
||||
isUnlockingMessage: state.context.isUnlockingMessage,
|
||||
unlockingMessageId: state.context.unlockingMessageId,
|
||||
unlockMessageError: state.context.unlockMessageError,
|
||||
unlockPaywallRequest: state.context.unlockPaywallRequest,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user