import type { MutableRefObject, Ref, RefObject } from 'react'; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { MaybePromiseOrNull } from 'agora-rtc-react'; import { AsyncTaskRunner, createAsyncTaskRunner } from './helpers'; export const useIsomorphicLayoutEffect = typeof document !== 'undefined' ? useLayoutEffect : useEffect; export function isPromise(value: MaybePromiseOrNull): value is PromiseLike { return value != null && typeof (value as PromiseLike).then === "function"; } export function useForceUpdate() { const [_, forceUpdate] = useState(0); return useCallback(() => forceUpdate(n => (n + 1) | 0), []); } export function useIsUnmounted(): RefObject { const isUnmountRef = useRef(false); useEffect(() => { isUnmountRef.current = false; return () => { isUnmountRef.current = true; }; }, []); return isUnmountRef; } /** * Leave promise unresolved when the component is unmounted. * * ```js * const sp = useSafePromise() * setLoading(true) * try { * const result1 = await sp(fetchData1()) * const result2 = await sp(fetchData2(result1)) * setData(result2) * } catch(e) { * setHasError(true) * } * setLoading(false) * ``` */ export function useSafePromise() { const isUnmountRef = useIsUnmounted(); function safePromise( promise: PromiseLike, onUnmountedError?: (error: E) => void, ) { // the async promise executor is intended // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { try { const result = await promise; if (!isUnmountRef.current) { resolve(result); } // unresolved promises will be garbage collected. } catch (error) { if (!isUnmountRef.current) { reject(error); } else if (onUnmountedError) { onUnmountedError(error as E); } else { if (process.env.NODE_ENV === 'development') { console.error("An error occurs from a promise after a component is unmounted", error); } } } }); } return useCallback(safePromise, [isUnmountRef]); } export function applyRef(ref: Ref, value: T) { if (typeof ref === "function") { ref(value); } else if (typeof ref === "object" && ref) { (ref as MutableRefObject).current = value; } } /** * Sugar to merge forwarded ref and produce a local ref (state). * * ```jsx * const Button = forwardRef((props, ref) => { * const [div, setDiv] = useForwardRef(ref) * // use 'div' here * return
* }) * ``` */ export function useForwardRef(ref: Ref): [T | null, (value: T | null) => void] { const [current, setCurrent] = useState(null); const forwardedRef = useCallback( (value: T | null) => { setCurrent(value); applyRef(ref, value); }, [ref, setCurrent], ); return [current, forwardedRef]; } /** * Await a promise or return the value directly. */ export function useAwaited(promise: MaybePromiseOrNull): T | undefined { const sp = useSafePromise(); const [value, setValue] = useState(); useIsomorphicLayoutEffect(() => { if (isPromise(promise)) { sp(promise).then(setValue); } else { setValue(promise); } }, [promise, sp]); return value; } /** * Accepts a function that contains imperative, possibly asynchronous effect-ful code. * During the side-effect running/removing, if multiple effects are triggered, only the last one will be executed. */ export function useAsyncEffect( effect: () => MaybePromise MaybePromise)>, deps?: ReadonlyArray, ): void { const runnerRef = useRef(); useEffect(() => { const { run, dispose } = (runnerRef.current ||= createAsyncTaskRunner()); run(effect); return dispose; // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); } export function compareVersion(v1: string, v2: string): number { const v1Parts = v1.split("."); const v2Parts = v2.split("."); const maxLength = Math.max(v1Parts.length, v2Parts.length); for (let i = 0; i < maxLength; i++) { const part1 = parseInt(v1Parts[i] || "0"); const part2 = parseInt(v2Parts[i] || "0"); if (part1 > part2) { return 1; } if (part1 < part2) { return -1; } } return 0; }