91 lines
2.4 KiB
TypeScript
91 lines
2.4 KiB
TypeScript
import { Update } from "common";
|
|
import { User } from "common/the_api";
|
|
import { identity } from "effect";
|
|
import { useLayoutEffect, useState } from "react";
|
|
|
|
export const mapProp = <const K extends string, T>(prop: K, action: Update<T>) => <O extends { readonly [_ in K]: T }>(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<Store>({
|
|
user: null,
|
|
pieceQueryName: "",
|
|
pieceQueryAuthor: "",
|
|
});
|
|
|
|
export function setUser(action: Update<User | null>) {
|
|
set(mapProp("user", action));
|
|
}
|
|
|
|
export function setPieceQueryName(action: Update<string>) {
|
|
set(mapProp("pieceQueryName", action));
|
|
}
|
|
|
|
export function setPieceQueryAuthor(action: Update<string>) {
|
|
set(mapProp("pieceQueryAuthor", action));
|
|
}
|
|
|
|
// --- STORE IMPLEMENTATION ----------------------------------------------------
|
|
|
|
class Listener<T> {
|
|
|
|
private lastState: T;
|
|
|
|
private readonly selector: Selector<T>;
|
|
private readonly setState: (state: T) => void;
|
|
|
|
constructor(
|
|
selector: Selector<T>,
|
|
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<Listener<any>>();
|
|
|
|
export type Selector<T> = (store: Store) => T;
|
|
|
|
function set(action: Partial<Store> | ((store: Store) => Partial<Store>), replace?: false): void;
|
|
function set(action: Update<Store>, replace: true): void;
|
|
function set(action: Partial<Store> | ((store: Store) => Partial<Store>), 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<T = Store>(selector: Selector<T> = identity as Selector<T>): T {
|
|
|
|
const [state, setState] = useState(() => selector(store));
|
|
|
|
useLayoutEffect(() => {
|
|
const listener = new Listener(selector, setState);
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
};
|
|
}, [selector]);
|
|
|
|
return state;
|
|
}
|