164 lines
4.7 KiB
TypeScript
164 lines
4.7 KiB
TypeScript
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<T>(value: MaybePromiseOrNull<T>): value is PromiseLike<T> {
|
|
return value != null && typeof (value as PromiseLike<T>).then === "function";
|
|
}
|
|
|
|
export function useForceUpdate() {
|
|
const [_, forceUpdate] = useState(0);
|
|
return useCallback(() => forceUpdate(n => (n + 1) | 0), []);
|
|
}
|
|
|
|
export function useIsUnmounted(): RefObject<boolean> {
|
|
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<T, E = unknown>(
|
|
promise: PromiseLike<T>,
|
|
onUnmountedError?: (error: E) => void,
|
|
) {
|
|
// the async promise executor is intended
|
|
// eslint-disable-next-line no-async-promise-executor
|
|
return new Promise<T>(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<T>(ref: Ref<T>, value: T) {
|
|
if (typeof ref === "function") {
|
|
ref(value);
|
|
} else if (typeof ref === "object" && ref) {
|
|
(ref as MutableRefObject<T>).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 <div ref={setDiv} />
|
|
* })
|
|
* ```
|
|
*/
|
|
export function useForwardRef<T>(ref: Ref<T>): [T | null, (value: T | null) => void] {
|
|
const [current, setCurrent] = useState<T | null>(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<T>(promise: MaybePromiseOrNull<T>): T | undefined {
|
|
const sp = useSafePromise();
|
|
const [value, setValue] = useState<T | undefined>();
|
|
|
|
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<void | (() => MaybePromise<void>)>,
|
|
deps?: ReadonlyArray<unknown>,
|
|
): void {
|
|
const runnerRef = useRef<AsyncTaskRunner | undefined>();
|
|
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;
|
|
}
|