Skip to content

Commit

Permalink
feat: add support for first render
Browse files Browse the repository at this point in the history
  • Loading branch information
macoley committed Jan 13, 2023
1 parent 0083f46 commit 9352903
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 82 deletions.
45 changes: 45 additions & 0 deletions src/__tests__/events.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => (
<Provider>
<Alice />
<Bob />
</Provider>
)

expect(aliceListener).toBeCalledTimes(0)
expect(bobListener).toBeCalledTimes(0)

render(<App />)

expect(aliceListener).toBeCalledTimes(0)
expect(bobListener).toBeCalledTimes(1)
})
127 changes: 45 additions & 82 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import {
unstable_runWithPriority as runWithPriority
} from 'scheduler'

import { isDev, useIsomorphicLayoutEffect } from './common'
import { useIsomorphicLayoutEffect } from './common'

/**
* Types
*/
type ContextListener<Value> = (value: Readonly<Value>) => void

type EventKey = string | number | symbol

type EventMiddleware<In extends Readonly<unknown>[], Out> = (
Expand All @@ -24,20 +22,7 @@ type EventRegistry<
Event extends EventMiddleware<any, any>
> = Record<Key, Event>

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,
Expand All @@ -48,110 +33,88 @@ export default function events<
[key in keyof Partial<Registry>]: EventListener<ReturnType<Registry[key]>>
}

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<EventState>[] = []

/**
* Proxy Dispatcher for easier use
* @param event
* @param payload
*/
type Dispatcher = {
[key in keyof Registry]: (...payload: Parameters<Registry[key]>) => 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<EventState> =
(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<EventState[]>([])
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<EventState> = 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
}

0 comments on commit 9352903

Please sign in to comment.