import { Update } from "common"; import { User } from "common/the_api"; import { identity } from "effect"; import { useLayoutEffect, useState } from "react"; export const mapProp = (prop: K, action: Update) => (object: O): O => { return Object.freeze({ ...object, [prop]: typeof action === "function" ? (action as (prev: T) => T)(object[prop]) : action }); }; export interface Store { readonly user: User | null; readonly pieceQueryName: string; readonly pieceQueryAuthor: string; } let store: Store = Object.freeze({ user: null, pieceQueryName: "", pieceQueryAuthor: "", }); export function setUser(action: Update) { set(mapProp("user", action)); } export function setPieceQueryName(action: Update) { set(mapProp("pieceQueryName", action)); } export function setPieceQueryAuthor(action: Update) { set(mapProp("pieceQueryAuthor", action)); } // --- STORE IMPLEMENTATION ---------------------------------------------------- class Listener { private lastState: T; private readonly selector: Selector; private readonly setState: (state: T) => void; constructor( selector: Selector, setState: (state: T) => void, ) { this.lastState = selector(store); this.selector = selector; this.setState = setState; } react() { const state = this.selector(store); if (!Object.is(this.lastState, state)) { this.lastState = state; this.setState(state); } } } const listeners = new Set>(); export type Selector = (store: Store) => T; function set(action: Partial | ((store: Store) => Partial), replace?: false): void; function set(action: Update, replace: true): void; function set(action: Partial | ((store: Store) => Partial), replace: boolean = false): void { const nextPartial = typeof action === "function" ? action(store) : action; if (Object.is(store, nextPartial)) return; store = Object.freeze(replace ? nextPartial as Store : Object.assign({}, store, nextPartial)); for (const listener of listeners) { listener.react(); } } export function useStore(selector: Selector = identity as Selector): T { const [state, setState] = useState(() => selector(store)); useLayoutEffect(() => { const listener = new Listener(selector, setState); listeners.add(listener); return () => { listeners.delete(listener); }; }, [selector]); return state; }