Files
music-repo/packages/frontend/src/hooks/useStore.ts

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;
}