From 935290328a34379d802357f54fec3dbe1d3093df Mon Sep 17 00:00:00 2001 From: Maciej Olejnik Date: Fri, 13 Jan 2023 21:29:28 +0100 Subject: [PATCH] feat: add support for first render --- src/__tests__/events.test.tsx | 45 ++++++++++++ src/events.ts | 127 ++++++++++++---------------------- 2 files changed, 90 insertions(+), 82 deletions(-) diff --git a/src/__tests__/events.test.tsx b/src/__tests__/events.test.tsx index 5745d01..64318b1 100644 --- a/src/__tests__/events.test.tsx +++ b/src/__tests__/events.test.tsx @@ -281,3 +281,48 @@ test('Static usage', () => { expect(staticAliceListener).toBeCalledTimes(2) expect(staticBobListener).toBeCalledTimes(1) // should not changed }) + +test('Fresh binding', () => { + const aliceListener = jest.fn() + const bobListener = jest.fn() + + const [Provider, useEvents] = events({ + onAlice: () => {}, + onBob: () => {} + }) + + const Alice = () => { + const { onBob } = useEvents({ + onAlice: aliceListener + }) + + React.useEffect(() => { + onBob() + }, []) + + return <> + } + + const Bob = () => { + useEvents({ + onBob: bobListener + }) + + return <> + } + + const App = () => ( + + + + + ) + + expect(aliceListener).toBeCalledTimes(0) + expect(bobListener).toBeCalledTimes(0) + + render() + + expect(aliceListener).toBeCalledTimes(0) + expect(bobListener).toBeCalledTimes(1) +}) diff --git a/src/events.ts b/src/events.ts index 9501bdf..209fdc1 100644 --- a/src/events.ts +++ b/src/events.ts @@ -4,13 +4,11 @@ import { unstable_runWithPriority as runWithPriority } from 'scheduler' -import { isDev, useIsomorphicLayoutEffect } from './common' +import { useIsomorphicLayoutEffect } from './common' /** * Types */ -type ContextListener = (value: Readonly) => void - type EventKey = string | number | symbol type EventMiddleware[], Out> = ( @@ -24,20 +22,7 @@ type EventRegistry< Event extends EventMiddleware > = Record -type EventState = Readonly<{ type: EventKey; payloadArgs: any[] }> - -type EventDispatcher = (event: EventState) => void - -/** - * Constants - */ - -const EVENT_DISPATCHED_NULL: EventDispatcher = () => { - /* istanbul ignore next */ - if (isDev) { - console.warn('Tried to dispatch event without Provider') - } -} +type EventRecord = Readonly<{ key: EventKey; payload: any[] }> export default function events< Key extends EventKey, @@ -48,110 +33,88 @@ export default function events< [key in keyof Partial]: EventListener> } - const dispatcher: { current: EventDispatcher } = { - current: EVENT_DISPATCHED_NULL + const isBinded = { current: false } + const listeners: ListenerRegistry[] = [] + const records: EventRecord[] = [] + + const informListeners = ( + listeners: ListenerRegistry[], + record: EventRecord + ) => { + listeners.forEach((registry) => + registry[record.key]?.(middlewares[record.key](...record.payload)) + ) } - const contextListeners: ContextListener[] = [] - /** - * Proxy Dispatcher for easier use - * @param event - * @param payload - */ type Dispatcher = { [key in keyof Registry]: (...payload: Parameters) => void } - const proxyDispatcher = new Proxy(dispatcher, { - get: ({ current: targetDispatcher }, type) => { - return (...payloadArgs: unknown[]) => - targetDispatcher({ type, payloadArgs }) + const dispatcher = new Proxy(listeners, { + get: (thisListeners, eventKey) => { + return (...payload: unknown[]) => { + if (isBinded.current) { + // Dispatch immediately + informListeners(thisListeners, { key: eventKey, payload }) + } else { + // Dispatch after the first render + records.push({ key: eventKey, payload }) + } + } } }) as unknown as Dispatcher - /** - * Informat - * @param eventListeners ListenerRegistry - * @returns void - */ - const informant: ( - eventListeners?: ListenerRegistry - ) => ContextListener = - (eventListeners?: ListenerRegistry) => (event: EventState) => { - eventListeners?.[event.type]?.( - middlewares[event.type](...event.payloadArgs) - ) - } - /** * Provider */ const BindProvider: React.FC = (props) => { - // setEvent is never updated - const [events, setEvents] = React.useState([]) - dispatcher.current = React.useCallback( - (event: EventState) => setEvents((oldEvents) => [...oldEvents, event]), - [setEvents] - ) - useIsomorphicLayoutEffect(() => { runWithPriority(NormalPriority, () => { - contextListeners.forEach((listener) => { - events.forEach((event) => listener(event)) - }) - }) + // Informing listeners + records.forEach((record) => informListeners(listeners, record)) + records.splice(0, records.length) - // Removing events without render - events.splice(0, events.length) - }, [events]) + isBinded.current = true + }) + }, []) return React.createElement(React.Fragment, props) } /** * useEvent hook - * @param eventListeners List of subscribed events + * @param newListeners List of subscribed events * @returns Dispatch function */ - const useEvent = (eventListeners?: ListenerRegistry) => { - // EventListener caller - const update = React.useCallback(informant(eventListeners), [ - eventListeners - ]) - - // Adding listener on component initialization + const useEvent = (newListeners?: ListenerRegistry) => { useIsomorphicLayoutEffect(() => { - contextListeners.push(update) + if (!newListeners) { + return + } + + listeners.push(newListeners) return () => { - const index = contextListeners.indexOf(update) - contextListeners.splice(index, 1) + listeners.splice(listeners.indexOf(newListeners), 1) } - }, [contextListeners]) + }, [newListeners]) - return proxyDispatcher + return dispatcher } /** * Subscriber for non-hook aproaches - * @param eventListeners List of subscribed events + * @param newListeners List of subscribed events * @returns Subscriber with unsubscribe method */ - const subscribe = (eventListeners: ListenerRegistry) => { - const subscriber: ContextListener = informant(eventListeners) - - contextListeners.push(subscriber) + const subscribe = (newListeners: ListenerRegistry) => { + listeners.push(newListeners) const unsubscribe = () => { - const index = contextListeners.indexOf(subscriber) - contextListeners.splice(index, 1) + listeners.splice(listeners.indexOf(newListeners), 1) } return { unsubscribe } } - return [ - BindProvider, - useEvent, - { subscribe, dispatcher: proxyDispatcher } - ] as const + return [BindProvider, useEvent, { subscribe, dispatcher }] as const }