Skip to content

Instantly share code, notes, and snippets.

@crazy4groovy
Last active September 19, 2023 18:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save crazy4groovy/5bf250f0eaf2de4807cbdadd77c88984 to your computer and use it in GitHub Desktop.
Save crazy4groovy/5bf250f0eaf2de4807cbdadd77c88984 to your computer and use it in GitHub Desktop.
A simple hook that implements an event bus with useReducer (ReactJS)
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)
}
// 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);
}
@SrJSDev
Copy link

SrJSDev commented Jun 12, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment