ron-store
Version:
Lightweight store for React apps
134 lines (132 loc) • 3.87 kB
text/typescript
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useRef, useState } from "react";
export type PackageType = Record<string, any>;
//Cho phép dev chỉ tương tác với các reducer qua IPackage thôi
export interface IPackage<T extends PackageType> {
getState(): T;
setState(callback: (state: T) => T): void;
}
export type Action<T extends PackageType> = (
pkg: IPackage<T>, //Cho phép dev chỉ tương tác với các reducer qua IPackage thôi
payload?: any
) => void;
export type Reducer<T extends PackageType> = Record<string, Action<T>>;
type SubcribeCallback<T extends PackageType> = (state: T) => void;
/**
* Class Package
*/
export class Package<T extends PackageType, R extends Reducer<T>>
implements IPackage<T>
{
private state$: T;
private reducers$: R;
private listeners$: Record<number, SubcribeCallback<T>> = {};
constructor({ initState, reducers }: { initState: T; reducers: R }) {
this.state$ = initState;
this.reducers$ = reducers;
}
getState() {
return this.state$;
}
get state() {
return this.state$;
}
get reducers() {
return this.reducers$;
}
setState(callback: (state: T) => T) {
this.state$ = callback(this.state$);
}
get listeners() {
return this.listeners$;
}
/**
*
* @param callback
* @returns
* Nếu listenerId trùng thì đệ quy
*/
subcribe(callback: SubcribeCallback<T>): number {
const listenerId = Date.now();
if (this.listeners$[listenerId]) {
return this.subcribe(callback);
}
this.listeners$[listenerId] = callback;
return listenerId;
}
unsubcribe(listenerId: number) {
delete this.listeners$[listenerId];
}
}
/**
* usePackage
*/
export interface UsePackageReturn<T extends PackageType, R extends Reducer<T>> {
state: T;
dispatch: (name: keyof R, payload?: any) => void;
subcribe: (callback: SubcribeCallback<T>) => number;
unsubcribe: (listenerId: number) => void;
}
export function usePackage<T extends PackageType, R extends Reducer<T>>({
pkg,
}: {
pkg: Package<T, Reducer<T>>;
}): UsePackageReturn<T, R> {
const isMountedRef = useRef(false); // Xử lý mounted n lần khi gọi usePackage
const packageRef = useRef(pkg);
const [state$, setState$] = useState<T>(pkg.state);
const doAction = (reducer: Action<T>, payload?: any) => {
reducer(packageRef.current, payload);
setState$(packageRef.current.state);
};
const dispatch = (name: keyof R, payload?: any) => {
const reducers = packageRef.current.reducers as R;
const reducer = reducers[name];
if (reducer) {
doAction(reducer, payload);
return;
} else {
throw new Error(`[Package] <dispatch: ${String(name)} > not found`);
}
};
const subcribe = (callback: SubcribeCallback<T>) => {
return packageRef.current.subcribe(callback);
};
const unsubcribe = (listenerId: number) => {
packageRef.current.unsubcribe(listenerId);
};
useEffect(() => {
if (isMountedRef.current) {
Object.entries(packageRef.current.listeners).forEach(([, callback]) => {
callback(state$);
});
} else {
isMountedRef.current = true;
}
}, [state$]);
return {
state: state$,
dispatch: dispatch,
subcribe: subcribe,
unsubcribe: unsubcribe,
};
}
/**
* Cho phép đăng ký một subcribe cho package
*/
export function usePkgSubcribe<T extends PackageType>({
pkg,
action,
}: {
pkg: Package<T, Reducer<T>>;
action: SubcribeCallback<T>;
}) {
const { subcribe, unsubcribe } = usePackage({ pkg: pkg });
useEffect(() => {
const id = subcribe(action);
return () => {
unsubcribe(id);
};
}, []);
}