File size: 3,309 Bytes
a417977
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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;
}