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 = { initialValue: T | (() => T), controllers?: (value: T, setValue: Dispatch>) => { // 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 = Context<[ value: T, setter: Dispatch>, // actions: A extends Actions ? { [K in keyof A]: (...rest: ActionsMap[K][0]) => ActionsMap[K][1] } : never, actions: A extends Actions ? A : never, ...never[] ]>; export function createContext(conf: Conf): CustomContext { return preactCreateContext(conf) as CustomContext; } export function ContextsProvider(props: { // contexts: Array>, contexts: Array>, 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; // 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; }