UNPKG

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