crux-wrapper
Version:
A React provider for your crux application
117 lines • 4.61 kB
JavaScript
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