Wode Emittery Event-Driven Pattern
You are strictly required to follow this event-driven architecture when implementing inter-component communication or side-effect handling. This pattern decouples state management (Zustand) from side-effects (event handlers).
1. Core Rules (MUST FOLLOW)
- Event Types: MUST use
as constobjects namedXxxEventType. NEVER use TypeScript enums. - Event Naming: MUST use
Module:Actionformat (e.g.,FileSystem:Open,User:Login). - Typed Emitter: MUST define
type XxxEmitter = Emittery<XxxEventData>for full type safety. - Sidecar Pattern: Side-effects (toast, confirm dialogs, network requests) MUST be handled in Sidecar components, NEVER inside Store actions.
- Store Decoupling: Store actions MUST only emit events. They MUST NOT care who handles them.
2. Event Type Definition
// MUST use as const, NEVER enum
export const FileSystemEventType = {
Open: 'FileSystem:Open',
Goto: 'FileSystem:Goto',
Refresh: 'FileSystem:Refresh',
Download: 'FileSystem:Download',
Delete: 'FileSystem:Delete',
} as const;
// Event data types
type FileSystemEventData = {
[FileSystemEventType.Open]: { fs: IFileSystem; file: IFileStat };
[FileSystemEventType.Goto]: { path: string };
[FileSystemEventType.Refresh]: {};
[FileSystemEventType.Download]: { fs: IFileSystem; files: IFileStat[] };
[FileSystemEventType.Delete]: { fs: IFileSystem; files: IFileStat[] | string[] };
};
// Emitter type
export type FileSystemEmitter = Emittery<FileSystemEventData>;
3. Emitter Factory & Store Integration
import Emittery from 'emittery';
export function createEmitter<EventData = Record<string, any>>(name: string) {
return new Emittery<EventData>({ debug: { name } });
}
// In Store — actions MUST only emit events
const events: FileSystemEmitter = createEmitter('FileSystemEmitter');
actions: {
open: async (item) => {
// Emit event, let Sidecar handle the rest
return events.emit(FileSystemEventType.Open, { fs: getState().fs, file: item });
},
refresh: () => events.emit(FileSystemEventType.Refresh, {}),
}
4. useEmitteryListen Hook
export function useEmitteryListen<E>(
e: Emittery<E>,
handle: { [K in keyof E]?: (data: E[K]) => void },
) {
const ref = useRef<any>(handle);
ref.current = handle;
useEffect(() => {
const unsub = Object.keys(ref.current).map((event) => {
return e.on(event as any, (data) => ref.current[event]?.(data));
});
return () => unsub.forEach((fn) => fn());
}, [e, ...Object.keys(handle).sort()]);
}
5. Sidecar Component
export const FileManagerSidecar = () => {
const { events, actions } = useFileManagerStore(
useShallow((s) => ({ events: s.events, actions: s.actions }))
);
useEmitteryListen(events, {
[FileSystemEventType.Refresh]: async () => {
await load();
},
[FileSystemEventType.Delete]: async ({ fs, files }) => {
const ok = await showConfirm({ title: '删除确认' });
if (!ok) return;
for (const file of files) await fs.rm(file.path);
actions.refresh();
},
});
return null; // Renders no UI
};
// Mount inside Provider
<FileManagerProvider>
<FileManagerSidecar /> {/* Event handler */}
{children}
</FileManagerProvider>
6. Advanced Patterns
For complete Sidecar implementation examples (upload, rename, move, mkdir, error handling) and best practices, read: references/events-pattern.md