Last active
September 19, 2023 18:06
-
-
Save crazy4groovy/5bf250f0eaf2de4807cbdadd77c88984 to your computer and use it in GitHub Desktop.
A simple hook that implements an event bus with useReducer (ReactJS)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
createContext, | |
useContext, | |
useEffect, | |
useRef, | |
} from "react" | |
// Create the event bus context | |
const EventBusContext = createContext({}) | |
// Custom hook to access the event bus context | |
export const useEventBus = () => useContext(EventBusContext) | |
// Event bus provider component | |
export function useAsyncReducer(reducer, initState) { | |
const [state, setState] = useState(initState) | |
const dispatchState = async (action) => { | |
const newState = await reducer(state, action) | |
setState(newState); | |
} | |
return [state, dispatchState]; | |
} | |
export const EventBusProvider = ({ children }) => { | |
// Array to hold all the registered reducers | |
const dispatchers = useRef([]) | |
// Custom useCombineReducer hook | |
const useCombineReducer = (reducer, initState) => { | |
const [localState, dispatch] = useAsyncReducer(reducer, initState) | |
// Add the reducer and its dispatch function to the array | |
useEffect(() => { | |
dispatchers.current = [ | |
...dispatchers.current, | |
dispatch, | |
] | |
}, [dispatch]) | |
const removeReducer = () => | |
(dispatchers.current = dispatchers.current.filter( | |
(disp) => disp !== dispatch, | |
)) | |
const _localDispatch = (msg) => { | |
delete msg.scope | |
delete msg.global | |
return dispatch(window.structuredClone(msg)) | |
} | |
const localDispatchHelper = (...args) => { | |
if ( | |
args.length === 2 && | |
typeof args[0] === "string" | |
) { | |
const [type, payload] = args | |
return _localDispatch({ type, payload }) | |
} | |
if ( | |
args.length === 1 && | |
typeof args[0] === "string" | |
) { | |
// assume this arg is scope; just ignore it, return a localDispatch helper | |
return (type, payload) => | |
_localDispatch({ type, payload }) | |
} | |
throw new Error("Invalid dispatch args", { | |
context: args, | |
}) | |
} | |
return [localState, localDispatchHelper, removeReducer] | |
} | |
// Dispatch a GLOBAL event msg to all reducers | |
const _dispatch = (scope, msg) => { | |
if (typeof scope !== "string" || scope.length < 1) { | |
throw new Error( | |
"A scope must be provided for global dispatch", | |
) | |
} | |
const sealedMessage = deepFreeze( | |
window.structuredClone({ | |
...msg, | |
scope, | |
global: true, | |
}), | |
) | |
// Loop through all the registered reducers and dispatch the event to each one | |
dispatchers.current.forEach((dispatch) => { | |
return dispatch(sealedMessage) | |
}) | |
} | |
const dispatchHelper = (...args) => { | |
if ( | |
args.length === 3 && | |
typeof args[0] === "string" && | |
typeof args[1] === "string" | |
) { | |
const [scope, type, payload] = args | |
return _dispatch(scope, { type, payload }) | |
} | |
if (args.length === 1 && typeof args[0] === "string") { | |
const [scope] = args | |
return (type, payload) => { | |
return _dispatch(scope, { type, payload }) | |
} | |
} | |
throw new Error("Invalid dispatch") | |
} | |
// Provide an event bus context to the children components | |
return ( | |
<EventBusContext.Provider | |
value={{ | |
dispatch: dispatchHelper, | |
useCombineReducer, | |
}} | |
> | |
{children} | |
</EventBusContext.Provider> | |
) | |
} | |
function deepFreeze(object) { | |
const propNames = Reflect.ownKeys(object) | |
for (const name of propNames) { | |
const value = object[name] | |
if ( | |
(value && typeof value === "object") || | |
typeof value === "function" | |
) { | |
deepFreeze(value) | |
} | |
} | |
return Object.freeze(object) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// NOTE: Alpha, probably bugs, this is hard!!! | |
import React, { createContext, useContext, useEffect, useReducer, useRef, useState, type ReactElement, type Dispatch } from 'react'; | |
interface Action { | |
type: string; | |
payload?: any; | |
} | |
interface GlobalAction extends Action { | |
scope: string; | |
global: boolean; | |
}; | |
type Reducer<S, A> = (state: S, action: A) => Promise<S> | S; | |
export function useAsyncReducer<S, A>( | |
reducer: Reducer<S, A>, | |
initState: S | |
): [S, Dispatch<A>] { | |
const [state, setState] = useState<S>(initState); | |
const dispatchState: Dispatch<A> = async (action: A) => { | |
const newState = await reducer(state, action); | |
setState(newState); | |
}; | |
return [state, dispatchState]; | |
} | |
type DispatchHelper = <P = any>(type: string, payload: P) => void; | |
interface CombineReducerResult<S> { | |
state: S; | |
dispatch: DispatchHelper; | |
removeReducer: () => void; | |
} | |
interface EventBusContextType<A> { | |
dispatch: Dispatch<A>; | |
useCombineReducer: <S,A>(reducer: Reducer<S, A>, initState: S) => CombineReducerResult<S>; | |
} | |
const EventBusContext = createContext<any>({ | |
dispatch: () => {}, | |
useCombineReducer: null, | |
}); | |
export const useEventBus = <A extends Action>(): EventBusContextType<A> => { | |
const context = useContext(EventBusContext); | |
if (!context) { | |
throw new Error( | |
'useEventBus must be used within an EventBusProvider'); | |
} | |
return context; | |
}; | |
interface EventBusProviderProps { | |
children: ReactElement; | |
} | |
export const EventBusProvider = <A extends Action>({ children }: EventBusProviderProps) => { | |
const dispatchers = useRef<Dispatch<A>[]>([]); | |
const useCombineReducer = <S,>(reducer: Reducer<S, A>, initState: S): CombineReducerResult<S> => { | |
const [localState, dispatch] = useAsyncReducer<S, A>(reducer, initState) | |
useEffect(() => { | |
dispatchers.current = [...dispatchers.current, dispatch]; | |
return () => { | |
dispatchers.current = dispatchers.current.filter((disp: Dispatch<A>) => disp !== dispatch); | |
}; | |
}, [dispatch]); | |
const removeReducer = () => { | |
dispatchers.current = dispatchers.current.filter((disp: Dispatch<A>) => disp !== dispatch); | |
}; | |
const _localDispatch = (action: A) => { | |
dispatch(window.structuredClone(action)); | |
}; | |
const localDispatchHelper = (...args: [string, any] | [string]): DispatchHelper | void => { | |
if (args.length === 2 && typeof args[0] === 'string') { | |
const [type, payload] = args; | |
_localDispatch({ type, payload } as A); | |
return | |
} | |
if (args.length === 1 && typeof args[0] === 'string') { | |
// const [scope] = args; | |
const cb: DispatchHelper = (type: string, payload?: any) => _localDispatch({ type, payload } as A) | |
return cb; | |
} | |
throw new Error('Invalid dispatch args'); | |
}; | |
return { | |
state: localState, | |
dispatch: localDispatchHelper, | |
removeReducer, | |
}; | |
}; | |
const _dispatch = (scope: string, action: A) => { | |
if (typeof scope !== 'string' || scope.length < 1) { | |
throw new Error('A scope must be provided for global dispatch'); | |
} | |
const sealedMessage: GlobalAction = deepFreeze({ | |
...action, | |
scope, | |
global: true, | |
}); | |
dispatchers.current.forEach((dispatch: Dispatch<any>) => { | |
dispatch(sealedMessage); | |
}); | |
}; | |
const dispatchHelper = (...args: [string, string, any] | [string]): DispatchHelper | void => { | |
if ( | |
args.length === 3 && | |
typeof args[0] === 'string' && | |
typeof args[1] === 'string' | |
) { | |
const [scope, type, payload] = args; | |
_dispatch(scope, { type, payload } as A); | |
return; | |
} | |
if (args.length === 1 && typeof args[0] === 'string') { | |
const [scope] = args; | |
const cb: DispatchHelper = (type: string, payload?: any) => { | |
_dispatch(scope, { type, payload } as A); | |
} | |
return cb; | |
} | |
throw new Error('Invalid dispatch'); | |
}; | |
const value = { | |
dispatch: dispatchHelper, | |
useCombineReducer, | |
} | |
return ( | |
<EventBusContext.Provider value={value}> | |
{children} | |
</EventBusContext.Provider> | |
); | |
} | |
function deepFreeze<T>(obj: T): T { | |
const propNames = Reflect.ownKeys(obj as any); | |
for (const name of propNames) { | |
const value = (obj as any)[name]; | |
if ((value && typeof value === 'object') || typeof value === 'function') { | |
deepFreeze(value); | |
} | |
} | |
return Object.freeze(obj); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example: https://codesandbox.io/s/hook-useeventbus-sy87rp?file=/src/useEventBus.js