jvcgpt / src /utils /context.ts
Greums's picture
major improvements to the app
a417977
raw
history blame
3.31 kB
import type {ComponentChildren} from "preact";
import {Context, createContext as preactCreateContext, h, JSX} from "preact";
import {Dispatch, StateUpdater, useEffect, useRef, useState} from "preact/hooks";
// TODO: use a context in another context
type Actions = {
[key: string]: (...rest: unknown[]) => unknown;
};
type Conf<T, A extends Actions = never> = {
initialValue: T | (() => T),
controllers?: (value: T, setValue: Dispatch<StateUpdater<T>>) => {
// It will only be called after the initial render
onMount?: () => void,
// It will only be called after the initial render and after re-renders with changed value
effect?: () => void,
actions?: A
},
}
type CustomContext<T, A> = Context<[
value: T,
setter: Dispatch<StateUpdater<T>>,
// actions: A extends Actions<T, ActionsMap> ? { [K in keyof A]: (...rest: ActionsMap[K][0]) => ActionsMap[K][1] } : never,
actions: A extends Actions ? A : never,
...never[]
]>;
export function createContext<T, A>(conf: Conf<T, A>): CustomContext<T, A> {
return preactCreateContext(conf) as CustomContext<T, A>;
}
export function ContextsProvider(props: {
// contexts: Array<CustomContext<any, any, any>>,
contexts: Array<CustomContext<unknown, unknown>>,
children: ComponentChildren,
}): JSX.Element | ComponentChildren {
let node: JSX.Element | ComponentChildren = props.children;
for (let i = props.contexts.length - 1; i >= 0; i--) {
const context = props.contexts[i];
if (!context || typeof context !== "object" || !("__" in context)) {
throw new Error("Invalid context provided. Ensure all contexts conform to CustomContext.");
}
const conf = context.__ as Conf<unknown, unknown>;
// WARNING: Hooks rules: Only Call Hooks at the Top Level
const state = useState(conf.initialValue);
let actions: Actions | undefined = undefined;
if (conf.controllers) {
// TODO: do not call the controller getter every time the state is changer
const controller = conf.controllers(state[0], state[1]);
if (controller.onMount) {
useEffect(() => {
controller.onMount!();
}, [])
}
if (controller.effect) {
// Used to prevent infinite loop in case of setState called in effect
const effectRef = useRef(false);
useEffect(() => {
if (effectRef.current) return;
effectRef.current = true;
controller.effect!();
effectRef.current = false;
}, [state[0]])
}
// let actions: { [key: string]: (...args: unknown[]) => any } | undefined = undefined;
if (controller.actions) {
// actions = {};
// Object.entries(controller.actions as Actions).forEach(([key, func]) => {
// (actions as object)[key] = func;
// });
actions = controller.actions as Actions;
}
}
// @ts-expect-error
node = h(context.Provider, {value: actions ? [...state, actions] : [...state]}, node)
}
return node;
}