Add useExternalSnapshot + useNitroEventReducer + useMessageEventReducer hooks

The three companions promised in docs/ARCHITECTURE.md proposal #1
('Companion to add later') are now in src/hooks/events/:

- useExternalSnapshot wraps useSyncExternalStore for the renderer's
  EventDispatcher.subscribe() + getXxxSnapshot() pairing introduced in
  Nitro_Render_V3 2.1.0.
- useNitroEventReducer and useMessageEventReducer mirror the existing
  *State hooks but collapse multiple event types into a single owned
  state slice. The message variant accepts either a single event type
  or an array; subscription is wired through a single useEffect to keep
  the rules-of-hooks happy.
This commit is contained in:
simoleo89
2026-05-11 20:37:09 +02:00
parent f75762a2db
commit bb1238a5e5
4 changed files with 145 additions and 0 deletions
+3
View File
@@ -1,6 +1,9 @@
export * from './useEventDispatcher';
export * from './useExternalSnapshot';
export * from './useMessageEvent';
export * from './useMessageEventReducer';
export * from './useMessageEventState';
export * from './useNitroEvent';
export * from './useNitroEventReducer';
export * from './useNitroEventState';
export * from './useUiEvent';
+25
View File
@@ -0,0 +1,25 @@
import { useSyncExternalStore } from 'react';
/**
* useSyncExternalStore wrapper for the Nitro renderer's subscribe + snapshot
* getter contract.
*
* Pair with EventDispatcher.subscribe() (Nitro_Render_V3 v2.1.0+) and a
* referentially-stable snapshot getter such as
* SessionDataManager.getUserDataSnapshot() or
* RoomSessionManager.getActiveRoomSessionSnapshot().
*
* const userData = useExternalSnapshot(
* cb => GetEventDispatcher().subscribe(NitroEventType.SESSION_DATA_UPDATED, cb),
* () => GetSessionDataManager().getUserDataSnapshot()
* );
*
* Snapshot reference invariance is guaranteed by the renderer: the same
* object is returned across reads until the corresponding *_UPDATED event
* dispatches.
*/
export const useExternalSnapshot = <T,>(
subscribe: (onChange: () => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T
): T => useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot ?? getSnapshot);
@@ -0,0 +1,65 @@
import { GetCommunication, IMessageEvent, MessageEvent } from '@nitrots/nitro-renderer';
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
/**
* Reducer companion of useMessageEventState for the server message
* channel. Accepts either a single event type or an array of event types
* that all collapse into the same state slice — typical for streams where
* different composers update the same shape (e.g. furniture list +
* add/update + remove all maintain one GroupItem[]).
*
* const groupItems = useMessageEventReducer<GroupItem[], A | B | C>(
* [ParserA, ParserB, ParserC],
* (state, event) => {
* if (event instanceof ParserA) return applyA(state, event);
* if (event instanceof ParserB) return applyB(state, event);
* return state;
* },
* []
* );
*
* Closure stability via reducerRef refreshed in commit phase.
*/
export const useMessageEventReducer = <S, T extends IMessageEvent>(
eventType: typeof MessageEvent | (typeof MessageEvent)[],
reducer: (state: S, event: T) => S,
initial: S | (() => S)
): S =>
{
const [ value, setValue ] = useState<S>(initial);
const reducerRef = useRef(reducer);
useLayoutEffect(() =>
{
reducerRef.current = reducer;
});
const handler = useCallback((event: T) =>
{
setValue(prev => reducerRef.current(prev, event));
}, []);
const types = useMemo(() => Array.isArray(eventType) ? eventType : [ eventType ], [ eventType ]);
useEffect(() =>
{
const communication = GetCommunication();
const registered = types.map(t =>
{
//@ts-ignore
const event = new t(handler);
communication.registerMessageEvent(event);
return event;
});
return () =>
{
for(const event of registered) communication.removeMessageEvent(event);
};
}, [ types, handler ]);
return value;
};
+52
View File
@@ -0,0 +1,52 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useNitroEvent } from './useNitroEvent';
/**
* Reducer companion of useNitroEventState for the cases where multiple
* event types collapse into a single state slice. Replaces the pattern:
*
* const [state, setState] = useState(initial);
* useNitroEvent(EVENT_A, e => setState(prev => reduceA(prev, e)));
* useNitroEvent(EVENT_B, e => setState(prev => reduceB(prev, e)));
*
* with:
*
* const state = useNitroEventReducer<S, A | B>(
* [EVENT_A, EVENT_B],
* (state, event) => {
* if (event instanceof EventA) return reduceA(state, event);
* if (event instanceof EventB) return reduceB(state, event);
* return state;
* },
* initial
* );
*
* Closure stability: the reducer ref is refreshed in commit phase, so a
* new reducer identity per render does not force the listener to
* re-subscribe.
*/
export const useNitroEventReducer = <S, T extends NitroEvent>(
types: string | string[],
reducer: (state: S, event: T) => S,
initial: S | (() => S),
enabled: boolean = true
): S =>
{
const [ value, setValue ] = useState<S>(initial);
const reducerRef = useRef(reducer);
useLayoutEffect(() =>
{
reducerRef.current = reducer;
});
const handler = useCallback((event: T) =>
{
setValue(prev => reducerRef.current(prev, event));
}, []);
useNitroEvent<T>(types, handler, enabled);
return value;
};