import { useContext, useRef, useMemo, useState, useCallback, useEffect, } from 'react'; import { Loader, LoaderStatus } from './loaderManager'; import { RuntimeReactContext } from '@/runtime-context'; export interface LoaderOptions< Params = any, TData = any, TError extends Error = any, > { /** * Revoke when loader excuted successfully. */ onSuccess?: (data: TData) => void; /** * Revoke when loader ended with error */ onError?: (error: TError) => void; /** * initialData to display once loader is ready. */ initialData?: TData; /** * wheather skip loader * if true, the loader will not exec. */ skip?: boolean; /** * User params, it will bypass to loader's second parameter. */ params?: Params; /** * wheather loader can exec on build phase. */ static?: boolean; } // todo: SSRContext type LoaderFn

= (context: any, params: P) => Promise; const useLoader = ( loaderFn: LoaderFn, options: LoaderOptions = { params: undefined } as any, ) => { const context = useContext(RuntimeReactContext); const isSSRSecRender = Boolean(context.ssr); const { loaderManager } = context; const loaderRef = useRef(); const unlistenLoaderChangeRef = useRef<(() => void) | null>(null); const reload = useCallback( (params?: Params) => { if (typeof params === 'undefined') { return loaderRef.current?.load(); } const id = loaderManager.add( () => { try { const res = loaderFn(context, params); if (res instanceof Promise) { return res; } return Promise.resolve(res); } catch (e) { return Promise.reject(e instanceof Error ? e.message : e); } }, { ...options, params, }, ); loaderRef.current = loaderManager.get(id)!; if (isSSRSecRender) { unlistenLoaderChangeRef.current?.(); return undefined; } // skip this loader, then try to unlisten loader change if (options.skip) { unlistenLoaderChangeRef.current?.(); return undefined; } const res = loaderRef.current.load(); // unlisten old loader, and subsribe to new loader unlistenLoaderChangeRef.current?.(); unlistenLoaderChangeRef.current = loaderRef.current?.onChange( (_status, _result) => { setResult(_result); if (_status === LoaderStatus.fulfilled) { options?.onSuccess?.(result.data); } if (_status === LoaderStatus.rejected) { options?.onError?.(result.data); } }, ); return res; }, [options.skip], ); useEffect( () => () => { unlistenLoaderChangeRef.current?.(); }, [], ); useMemo(() => { const p = options.params ?? (loaderFn as any).id; if (!p) { throw new Error('Params is required in useLoader'); } reload(p); }, [options.params]); const [result, setResult] = useState<{ loading: boolean; reloading: boolean; data: TData; error: E; }>(loaderRef.current!.result); return { ...result, reload, }; }; export default useLoader;