UNPKG

crux-wrapper

Version:

A React provider for your crux application

117 lines 4.61 kB
import { is, wrap } from "../index.js"; import react, { useCallback, useContext, useLayoutEffect, useRef, useSyncExternalStore, } from "react"; import { State } from "./state.js"; export const CoreContext = react.createContext({ dispatch: () => { throw new Error("CoreProvider not initialized"); }, logs: [], ready: false, }); class Logger { state = []; listeners = []; addListener(listener) { this.listeners.push(listener); return () => { this.listeners = this.listeners.filter((l) => l !== listener); }; } log(entry) { this.state = [...this.state, entry]; this.listeners.forEach((listener) => listener()); } getLogs() { return this.state; } } let logger = new Logger(); /** * Provides a context that exposes the crux api. */ export function CoreProvider({ children, coreConfig, RenderEffect, initialState, mergeViewModel, }) { const isInit = useRef(false); const state = useRef(RenderEffect && new State(initialState, mergeViewModel)); const [ready, setReady] = react.useState(false); const useReactLogger = typeof coreConfig.log === "boolean" && coreConfig.log; const onLog = useReactLogger ? // When no logger is provided, but the consumer wants to log events, we register our custom logger // this logger will be used by the useLog hook to subscribe to log changes logger.log.bind(logger) : coreConfig.log; const core = useRef(wrap({ ...coreConfig, onEffect: async (effect, callbacks) => { if (state.current && RenderEffect && is(effect, RenderEffect)) { state.current.setViewModel(await callbacks.view()); return; } return coreConfig.onEffect(effect, callbacks); }, log: onLog, })); // We use layout effect in this case to make sure it's going to be called before the consumer starts sending events // Using useEffect would result in the consumer being able (via a useEffect) to call `send` before the core is initialized useLayoutEffect(() => { if (!isInit.current) { isInit.current = true; core.current.init().then(() => setReady(true)); } }, [core]); return react.createElement(CoreContext.Provider, { value: { dispatch: core.current.send, logs: core.current.logs, state: state.current, ready, }, }, children); } export function useDispatch() { return useContext(CoreContext).dispatch; } /** * Subscribe to changes to the crux viewModel. * This hooks only works if a RenderEffect class was given when mounting the `CruxProvider` component. * @param selector allows selecting a slice of the state and only subscribing to changes to that particular slice */ export function useViewModel(selector) { const state = useContext(CoreContext).state; if (!state) { throw new Error("useViewModel cannot be used when RenderEffect property was not set"); } return useSyncExternalStore((onStoreChange) => state.subscribe(onStoreChange), () => state.getViewModel(selector)); } /** * This hook will return a function that could give you the current loaded state. * It gives you direct access to the viewmodel state at instant T without subscribing to changes. */ export function useViewModelGetter(selector) { const state = useContext(CoreContext).state; if (!state) { throw new Error("useViewModelGetter cannot be used when RenderEffect property was not set"); } return useCallback(() => state.getViewModel(selector), [state]); } /** * Will subscribe to logs from the core and return a reactive state containing the logs from the core. */ export function useLogs() { return useSyncExternalStore((onLog) => logger.addListener(onLog), () => logger.getLogs()); } /** * This hook will return a function that could give you the current logs. * It gives you direct access to the logs at instant T without subscribing to changes. */ export function useGetLogs() { return useCallback(() => logger.getLogs(), []); } /** * Subscribes to the changes of the `read` state of the core. * This hook could be used to show a loading state while the core is being initialized. * There is no particular need to wait for this property to be true before sending events to the core, as the core will handle them once it is ready. */ export function useIsReady() { return useContext(CoreContext).ready; } //# sourceMappingURL=CoreProvider.js.map