get-set-action
Version:
A React State Management Library similar and alternative to mobx, redux.etc. it's the simplest, smallest and fastest state management library for react with dev tools support.
548 lines (516 loc) • 14 kB
text/typescript
import { fnRef } from "fn-ref";
import { useEffect, useRef, useState } from "react";
function debounce(
func: Function,
wait: number,
setStatus?: (status: StatusType) => void
) {
let timeout: any;
return function (...args: any[]): any {
clearTimeout(timeout);
setStatus?.("loading");
// @ts-ignore
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function throttle(
func: Function,
limit: number,
setStatus?: (status: StatusType) => void
) {
let inThrottle: boolean;
return function (...args: any[]): any {
if (!inThrottle) {
// @ts-ignore
func.apply(this, args);
inThrottle = true;
setStatus?.("loading");
setTimeout(() => (inThrottle = false), limit);
}
};
}
type StatusType = "intial" | "loading" | "success" | "error" | "cancelled";
export type ActionType<T, E, P extends Record<string, any> = {}> = {
run: (payload: P, signal?: AbortSignal) => Promise<T>;
abort: () => void;
onRun: (subscriber: any) => (subscriber: any) => void;
onSuccess: (subscriber: any) => (subscriber: any) => void;
subscribe: (subscriber: any) => (subscriber: any) => void;
onError: (subscriber: any) => (subscriber: any) => void;
getState: () => ActionReturnType<T, E, P>;
intercept: (interceptor: (props: P) => P) => void;
interceptAfter: (interceptor: (data: any) => T) => void;
interceptError: (interceptor: (error: any) => E) => void;
// subscribe: (subscriber: any) => (subscriber: any) => void;
};
export function asyncAction<T, E, P extends Record<string, any>>(
// p is the props type
fn: (payload: P, signal?: AbortSignal) => any,
options: { debounce?: number; throttle?: number } = {}
): ActionType<T, E, P> & ActionReturnType<T, E, P> {
let data: T | null = null;
let error: E | null = null;
let status: "intial" | "loading" | "success" | "error" | "cancelled" =
"intial";
let interceptorAfter: ((props: any) => T) | null = null;
let interceptorError: ((props: any) => E) | null = null;
let isDebounced = false;
let isDebouncing = false;
let abortController = new AbortController();
let subscribers: any[] = [];
let successSubscriber: any[] = [];
let errorSubscriber: any[] = [];
let stateSubscriber: any[] = [];
let isThrottled = false;
let payload: P = {} as P;
const onSuccess = (subscriber: any) => {
successSubscriber.push(subscriber);
return (subscriber: any) => {
successSubscriber = successSubscriber.filter((s) => s !== subscriber);
};
};
const onError = (subscriber: any) => {
errorSubscriber.push(subscriber);
return (subscriber: any) => {
errorSubscriber = errorSubscriber.filter((s) => s !== subscriber);
};
};
const onRun = (subscriber: any) => {
subscribers.push(subscriber);
return (subscriber: any) => {
subscribers = subscribers.filter((s) => s !== subscriber);
};
};
const onStateChange = (subscriber: any) => {
stateSubscriber.push(subscriber);
return (subscriber: any) => {
stateSubscriber = stateSubscriber.filter((s) => s !== subscriber);
};
};
const callSubs = debounce((subscriber: any) => {
subscribers.forEach((s) => s(subscriber));
}, 0);
const callSuccessSubs = debounce((subscriber: any) => {
successSubscriber.forEach((s) => s(subscriber));
}, 0);
const callErrorSubs = debounce((subscriber: any) => {
errorSubscriber.forEach((s) => s(subscriber));
}, 0);
const callStateChangeSubs = debounce((subscriber: any) => {
buildState();
stateSubscriber.forEach((s) => s(subscriber));
}, 0);
const execute = async function (_payload: P = {} as P) {
payload = _payload;
if (interceptor) {
try {
payload = await interceptor(payload);
} catch (err) {
console.error("error in interceptor", err);
}
}
let _data: T | null = null;
let _error: E | null = null;
abortController.abort();
setStatus("loading");
try {
abortController = new AbortController();
// @ts-ignore
_data = await fn.call(this, payload, abortController.signal);
if (interceptorAfter) {
try {
_data = await interceptorAfter(_data);
} catch (err) {
console.error("error in interceptorAfter", err);
}
}
setStatus("success");
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
setStatus("cancelled");
return null;
}
_error = err as E;
if (interceptorError) {
try {
_error = await interceptorError(_error);
} catch (err) {
console.error("error in interceptorError", err);
}
}
setStatus("error");
} finally {
callSubs();
if (status === "success") {
setData(_data);
setError(null);
callSuccessSubs();
}
if (status === "error") {
setError(_error);
setData(null);
callErrorSubs();
}
_data = null;
_error = null;
}
return data;
};
const abort = () => {
abortController.abort();
};
let currentPage = 1;
let countPerPage = 10;
const setPage = (page: number) => {
currentPage = page;
callStateChangeSubs();
};
const setCountPerPage = (count: number) => {
countPerPage = count;
callStateChangeSubs();
};
let extraState: any = {};
const setExtraState = (state: any) => {
extraState = state;
callStateChangeSubs();
};
let filters: any = {};
const setFilters = (filters: any) => {
filters = filters;
callStateChangeSubs();
};
const setData = (_data: T | null) => {
data = _data;
callStateChangeSubs();
};
const setError = (_error: E | null) => {
error = _error;
callStateChangeSubs();
};
const setStatus = (
_status: "intial" | "loading" | "success" | "error" | "cancelled"
) => {
status = _status;
callStateChangeSubs();
};
let run = execute;
if (options.debounce) {
run = debounce(execute, options.debounce, setStatus);
isDebounced = true;
}
if (options.throttle) {
run = throttle(execute, options.throttle, setStatus);
isThrottled = true;
}
let interceptor: ((props: P) => P) | null = null;
let state: ActionReturnType<T, E, P> = {} as ActionReturnType<T, E, P>;
const buildState = () => {
state = {
get data() {
return data;
},
run,
abort,
get error() {
return error;
},
get isRunning() {
return status === "loading";
},
get isSuccess() {
return status === "success";
},
get isError() {
return status === "error";
},
get isDebounced() {
return isDebounced;
},
get isThrottled() {
return isThrottled;
},
get isDebouncing() {
return isDebouncing;
},
get status() {
return status;
},
get abortController() {
return abortController;
},
get currentPage() {
return currentPage;
},
get countPerPage() {
return countPerPage;
},
get extraState() {
return extraState;
},
get filters() {
return filters;
},
get payload() {
return payload;
},
setData,
setError,
setPage,
setCountPerPage,
setExtraState,
setFilters,
setStatus,
};
};
buildState();
let returnVal = {
run,
abort,
getState: () => state,
onRun,
intercept(_interceptor: (props: P) => P) {
interceptor = _interceptor;
},
interceptAfter(_interceptor: (props: T) => T) {
interceptorAfter = _interceptor;
},
interceptError(_interceptor: (props: E) => E) {
interceptorError = _interceptor;
},
onSuccess,
onError,
subscribe: onStateChange,
get data() {
return data;
},
get error() {
return error;
},
get isRunning() {
return status === "loading";
},
get isSuccess() {
return status === "success";
},
get isError() {
return status === "error";
},
get isDebounced() {
return isDebounced;
},
get isThrottled() {
return isThrottled;
},
get isDebouncing() {
return isDebouncing;
},
get status() {
return status;
},
get abortController() {
return abortController;
},
get currentPage() {
return currentPage;
},
get countPerPage() {
return countPerPage;
},
get extraState() {
return extraState;
},
get filters() {
return filters;
},
get payload() {
return payload;
},
setData,
setError,
setPage,
setCountPerPage,
setExtraState,
setFilters,
setStatus,
};
return returnVal;
}
type ActionReturnType<T, E, P extends Record<string, any>> = {
data: T | null;
error: E | null;
isRunning: boolean;
isSuccess: boolean;
isError: boolean;
isDebounced: boolean;
isThrottled: boolean;
isDebouncing: boolean;
status: StatusType;
abortController: AbortController;
currentPage: number;
countPerPage: number;
extraState: any;
filters: any;
payload: P;
setData: (data: T | null) => void;
setError: (error: E | null) => void;
setPage: (page: number) => void;
setCountPerPage: (count: number) => void;
setExtraState: (state: any) => void;
setFilters: (filters: any) => void;
setStatus: (status: StatusType) => void;
run: (payload: P, signal?: AbortSignal) => Promise<T>;
abort: () => void;
};
export function useAction<T, E, P extends Record<string, any>>(
action: ActionType<T, E, P>
): ActionReturnType<T, E, P> {
const [actionData, setActionData] = useState(action.getState());
useEffect(() => {
action.subscribe(() => {
setActionData(action.getState());
});
}, [setActionData]);
return actionData;
}
export function useOnMount(callback: () => void) {
// Implement logic to run callback on component mount
useEffect(() => {
callback();
}, []);
}
export function useOnRun(
callback: () => void,
deps: ActionType<any, any, any>[]
) {
// Implement logic to run callback after action is called
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
deps.forEach((action) => {
action.onRun(() => callbackRef.current());
});
}, []);
}
export function useOnSuccess(
callback: () => void,
deps: ActionType<any, any, any>[]
) {
// Implement logic to run callback on success
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
deps.forEach((action) => {
action.onSuccess(() => callbackRef.current());
});
}, []);
}
export function useOnError(
callback: () => void,
deps: ActionType<any, any, any>[]
) {
// Implement logic to run callback on error
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
deps.forEach((action) => {
action.onError(() => callbackRef.current());
});
}, []);
}
type ActionFunction<T, P> = (props: P) => T;
interface EnhancedAction<T, P> extends ActionFunction<T, P> {
intercept: (interceptor: (props: P) => P) => void;
subscribe: (callback: (result: T) => void) => () => void;
callCount: number;
callCountTotal: number;
retry: () => void;
getState: () => T;
forwardCall: (callback: (props: P) => T) => void;
}
export function action<T, P extends Record<string, any>>(
fn: ActionFunction<T, P>
): EnhancedAction<T, P> {
let state: T;
let subscribers: ((result: T) => void)[] = [];
let interceptor: ((props: P) => P) | null = null;
// Create the enhanced function
let forward: (props: P) => T;
let forwardInProgress: boolean = false;
let _props: P = {} as P;
const enhancedFn = function (props: P): T {
_props = props;
enhancedFn.callCountTotal++;
if (forward && !forwardInProgress) {
forwardInProgress = true;
forward(_props);
forwardInProgress = false;
}
// Check for interceptor
else {
if (interceptor) {
_props = interceptor(_props);
}
state = fn(_props);
forwardInProgress = false;
enhancedFn.callCount++;
}
// Notify subscribers
subscribers.forEach((callback) => callback(state));
return state;
} as EnhancedAction<T, P>;
enhancedFn.callCount = 0;
enhancedFn.callCountTotal = 0;
// Add properties
enhancedFn.intercept = (_interceptor: (props: P) => P) => {
interceptor = _interceptor;
};
enhancedFn.forwardCall = (callback: (props: P) => T) => {
forward = callback;
};
enhancedFn.subscribe = (callback: (result: T) => void) => {
subscribers.push(callback);
return () => {
subscribers = subscribers.filter((cb) => cb !== callback);
};
};
enhancedFn.retry = () => {
enhancedFn(_props);
};
enhancedFn.getState = () => state;
return enhancedFn;
}
export const interceptorFetch = fnRef((payload: FetchPayloadType) => payload);
export const interceptorFetchSuccess = fnRef((data) => data);
export const interceptorFetchError = fnRef((error) => error);
type FetchPayloadType = {
url: string;
qsObj?: Record<string, any>;
} & RequestInit;
export function Fetch(
options: { debounce?: number; throttle?: number } = { debounce: 1000 }
) {
const action = asyncAction(
async (
payload: { url: string; qsObj?: Record<string, any> } & RequestInit,
signal
) => {
return fetch(payload.url, { signal, ...payload }).then((res) => {
if (
res.ok &&
res.headers.get("content-type")?.includes("application/json")
) {
return res.json();
}
return res;
});
},
options
);
//@ts-ignore
action.intercept(interceptorFetch);
//@ts-ignore
action.interceptAfter(interceptorFetchSuccess);
//@ts-ignore
action.interceptError(interceptorFetchError);
return action;
}