优化
This commit is contained in:
26
client/shared/hooks/factory/createUpdateEffect.ts
Normal file
26
client/shared/hooks/factory/createUpdateEffect.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useRef } from 'react';
|
||||
import type { useEffect, useLayoutEffect } from 'react';
|
||||
|
||||
// Reference: https://github.com/alibaba/hooks/blob/master/packages/hooks/src/createUpdateEffect/index.ts
|
||||
|
||||
type EffectHookType = typeof useEffect | typeof useLayoutEffect;
|
||||
|
||||
export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
|
||||
(hook) => (effect, deps) => {
|
||||
const isMounted = useRef(false);
|
||||
|
||||
// for react-refresh
|
||||
hook(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
hook(() => {
|
||||
if (!isMounted.current) {
|
||||
isMounted.current = true;
|
||||
} else {
|
||||
return effect();
|
||||
}
|
||||
}, deps);
|
||||
};
|
||||
85
client/shared/hooks/factory/createUseStorageState.ts
Normal file
85
client/shared/hooks/factory/createUseStorageState.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/* eslint-disable no-empty */
|
||||
import { useState } from 'react';
|
||||
import { useMemoizedFn } from '../useMemoizedFn';
|
||||
import { useUpdateEffect } from '../useUpdateEffect';
|
||||
import _isFunction from 'lodash/isFunction';
|
||||
import _isUndefined from 'lodash/isUndefined';
|
||||
|
||||
export interface IFuncUpdater<T> {
|
||||
(previousState?: T): T;
|
||||
}
|
||||
export interface IFuncStorage {
|
||||
(): Storage;
|
||||
}
|
||||
|
||||
export interface Options<T> {
|
||||
serializer?: (value: T) => string;
|
||||
deserializer?: (value: string) => T;
|
||||
defaultValue?: T | IFuncUpdater<T>;
|
||||
}
|
||||
|
||||
export function createUseStorageState(getStorage: () => Storage | undefined) {
|
||||
function useStorageState<T>(key: string, options?: Options<T>) {
|
||||
let storage: Storage | undefined;
|
||||
|
||||
// https://github.com/alibaba/hooks/issues/800
|
||||
try {
|
||||
storage = getStorage();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const serializer = (value: T) => {
|
||||
if (options?.serializer) {
|
||||
return options?.serializer(value);
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
};
|
||||
|
||||
const deserializer = (value: string) => {
|
||||
if (options?.deserializer) {
|
||||
return options?.deserializer(value);
|
||||
}
|
||||
return JSON.parse(value);
|
||||
};
|
||||
|
||||
function getStoredValue() {
|
||||
try {
|
||||
const raw = storage?.getItem(key);
|
||||
if (raw) {
|
||||
return deserializer(raw);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
if (_isFunction(options?.defaultValue)) {
|
||||
return options?.defaultValue();
|
||||
}
|
||||
return options?.defaultValue;
|
||||
}
|
||||
|
||||
const [state, setState] = useState<T>(() => getStoredValue());
|
||||
|
||||
useUpdateEffect(() => {
|
||||
setState(getStoredValue());
|
||||
}, [key]);
|
||||
|
||||
const updateState = (value: T | IFuncUpdater<T>) => {
|
||||
const currentState = _isFunction(value) ? value(state) : value;
|
||||
setState(currentState);
|
||||
|
||||
if (_isUndefined(currentState)) {
|
||||
storage?.removeItem(key);
|
||||
} else {
|
||||
try {
|
||||
storage?.setItem(key, serializer(currentState));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [state, useMemoizedFn(updateState)] as const;
|
||||
}
|
||||
return useStorageState;
|
||||
}
|
||||
24
client/shared/hooks/model/useAvailableServices.ts
Normal file
24
client/shared/hooks/model/useAvailableServices.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useEffect } from 'react';
|
||||
import { fetchAvailableServices } from '../../model/common';
|
||||
import { useAsyncFn } from '../useAsyncFn';
|
||||
import { useMemoizedFn } from '../useMemoizedFn';
|
||||
|
||||
/**
|
||||
* 用于监测服务是否可用的hooks
|
||||
*/
|
||||
export function useAvailableServices() {
|
||||
const [{ loading, value: availableServices }, fetch] = useAsyncFn(() =>
|
||||
fetchAvailableServices()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetch();
|
||||
}, []);
|
||||
|
||||
const refetch = useMemoizedFn(async () => {
|
||||
fetchAvailableServices.clearCache();
|
||||
fetch();
|
||||
});
|
||||
|
||||
return { loading, availableServices, refetch };
|
||||
}
|
||||
23
client/shared/hooks/model/useMessageNotifyEventFilter.tsx
Normal file
23
client/shared/hooks/model/useMessageNotifyEventFilter.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { sharedEvent, useSharedEventHandler } from '../../event';
|
||||
import { useUserNotifyMute } from './useUserSettings';
|
||||
|
||||
/**
|
||||
* 消息通知翻译
|
||||
* 检查用户设置,接受已读消息并发送未静音消息
|
||||
*
|
||||
* 接收到消息事件{receiveMessage} -> 检查是否被静音 -> 没有静音,发送{receiveUnmutedMessage}事件
|
||||
* -> 静音, 不做任何处理
|
||||
*/
|
||||
export function useMessageNotifyEventFilter() {
|
||||
const { checkIsMuted } = useUserNotifyMute();
|
||||
|
||||
useSharedEventHandler('receiveMessage', (payload) => {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkIsMuted(payload.converseId, payload.groupId)) {
|
||||
sharedEvent.emit('receiveUnmutedMessage', payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
19
client/shared/hooks/model/useUserInfo.ts
Normal file
19
client/shared/hooks/model/useUserInfo.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getCachedUserInfo } from '../../cache/cache';
|
||||
import type { UserBaseInfo } from '../../model/user';
|
||||
import { useAsync } from '../useAsync';
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export function useCachedUserInfo(
|
||||
userId: string,
|
||||
refetch = false
|
||||
): UserBaseInfo | Record<string, never> {
|
||||
const { value: userInfo = {} } = useAsync(async () => {
|
||||
const users = getCachedUserInfo(userId, refetch);
|
||||
|
||||
return users;
|
||||
}, [userId, refetch]);
|
||||
|
||||
return userInfo ?? {};
|
||||
}
|
||||
16
client/shared/hooks/model/useUserInfoList.ts
Normal file
16
client/shared/hooks/model/useUserInfoList.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getCachedUserInfo } from '../../cache/cache';
|
||||
import type { UserBaseInfo } from '../../model/user';
|
||||
import { useAsync } from '../useAsync';
|
||||
|
||||
/**
|
||||
* 用户信息列表
|
||||
*/
|
||||
export function useUserInfoList(userIds: string[] = []): UserBaseInfo[] {
|
||||
const { value: userInfoList = [] } = useAsync(async () => {
|
||||
const users = await Promise.all(userIds.map((id) => getCachedUserInfo(id)));
|
||||
|
||||
return users;
|
||||
}, [userIds.join(',')]);
|
||||
|
||||
return userInfoList;
|
||||
}
|
||||
110
client/shared/hooks/model/useUserSettings.ts
Normal file
110
client/shared/hooks/model/useUserSettings.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { CacheKey } from '../../cache/cache';
|
||||
import { useQuery, useQueryClient } from '../../cache/useCache';
|
||||
import { sharedEvent } from '../../event';
|
||||
import {
|
||||
getUserSettings,
|
||||
setUserSettings,
|
||||
UserSettings,
|
||||
} from '../../model/user';
|
||||
import { useAsyncRequest } from '../useAsyncRequest';
|
||||
import { useMemoizedFn } from '../useMemoizedFn';
|
||||
import _without from 'lodash/without';
|
||||
|
||||
/**
|
||||
* 用户设置hooks
|
||||
*/
|
||||
export function useUserSettings() {
|
||||
const client = useQueryClient();
|
||||
const { data: settings, isLoading } = useQuery(
|
||||
[CacheKey.userSettings],
|
||||
() => getUserSettings(),
|
||||
{
|
||||
staleTime: 10 * 60 * 1000, // 缓存10分钟
|
||||
}
|
||||
);
|
||||
|
||||
const [{ loading: saveLoading }, setSettings] = useAsyncRequest(
|
||||
async (_settings: UserSettings) => {
|
||||
client.setQueryData([CacheKey.userSettings], () => ({
|
||||
...settings,
|
||||
..._settings,
|
||||
})); // 让配置能够立即生效, 防止依赖配置的行为出现跳变(如GroupNav)
|
||||
|
||||
const newSettings = await setUserSettings(_settings);
|
||||
|
||||
client.setQueryData([CacheKey.userSettings], () => newSettings);
|
||||
sharedEvent.emit('userSettingsUpdate', newSettings);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return {
|
||||
settings: settings ?? {},
|
||||
setSettings,
|
||||
loading: isLoading || saveLoading,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个用户设置
|
||||
*/
|
||||
export function useSingleUserSetting<K extends keyof UserSettings>(
|
||||
name: K,
|
||||
defaultValue?: UserSettings[K]
|
||||
) {
|
||||
const { settings, setSettings, loading } = useUserSettings();
|
||||
|
||||
return {
|
||||
value: settings[name] ?? defaultValue,
|
||||
setValue: async (newVal: UserSettings[K]) =>
|
||||
setSettings({
|
||||
[name]: newVal,
|
||||
}),
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户消息通知免打扰设置
|
||||
*/
|
||||
export function useUserNotifyMute() {
|
||||
const { value: list = [], setValue: setList } = useSingleUserSetting(
|
||||
'messageNotificationMuteList',
|
||||
[]
|
||||
);
|
||||
|
||||
const mute = useMemoizedFn((converseOrGroupId: string) => {
|
||||
setList([...list, converseOrGroupId]);
|
||||
});
|
||||
|
||||
const unmute = useMemoizedFn((converseOrGroupId: string) => {
|
||||
setList(_without(list, converseOrGroupId));
|
||||
});
|
||||
|
||||
const toggleMute = useMemoizedFn((converseOrGroupId) => {
|
||||
if (list.includes(converseOrGroupId)) {
|
||||
unmute(converseOrGroupId);
|
||||
} else {
|
||||
mute(converseOrGroupId);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 检查是否被静音
|
||||
*/
|
||||
const checkIsMuted = useMemoizedFn((panelId: string, groupId?: string) => {
|
||||
if (groupId) {
|
||||
return list.includes(panelId) || list.includes(groupId);
|
||||
}
|
||||
|
||||
return list.includes(panelId);
|
||||
});
|
||||
|
||||
return {
|
||||
mutedList: list,
|
||||
mute,
|
||||
unmute,
|
||||
toggleMute,
|
||||
checkIsMuted,
|
||||
};
|
||||
}
|
||||
10
client/shared/hooks/model/useUsernames.ts
Normal file
10
client/shared/hooks/model/useUsernames.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useUserInfoList } from './useUserInfoList';
|
||||
|
||||
/**
|
||||
* 用户名列表
|
||||
*/
|
||||
export function useUsernames(userIds: string[]): string[] {
|
||||
const userInfoList = useUserInfoList(userIds);
|
||||
|
||||
return userInfoList.map((info) => info.nickname);
|
||||
}
|
||||
16
client/shared/hooks/useAlphaMode.ts
Normal file
16
client/shared/hooks/useAlphaMode.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useStorage } from '../manager/storage';
|
||||
|
||||
const alphaModeKey = 'alphaMode';
|
||||
|
||||
/**
|
||||
* 是否为 alpha 模式
|
||||
* 在 alpha 模式下可以看到一些可以被公开但是还在测试中的功能
|
||||
*/
|
||||
export function useAlphaMode() {
|
||||
const [isAlphaMode, { save: setAlphaMode }] = useStorage<boolean>(
|
||||
alphaModeKey,
|
||||
false
|
||||
);
|
||||
|
||||
return { isAlphaMode, setAlphaMode };
|
||||
}
|
||||
29
client/shared/hooks/useAsync.ts
Normal file
29
client/shared/hooks/useAsync.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { DependencyList, useEffect, useRef } from 'react';
|
||||
import type { FunctionReturningPromise } from '../types';
|
||||
import { useAsyncFn } from './useAsyncFn';
|
||||
|
||||
// Reference: https://github.com/streamich/react-use/blob/master/src/useAsync.ts
|
||||
|
||||
export function useAsync<T extends FunctionReturningPromise>(
|
||||
fn: T,
|
||||
deps: DependencyList = []
|
||||
) {
|
||||
const [state, callback] = useAsyncFn(fn, deps, {
|
||||
loading: true,
|
||||
});
|
||||
const lockRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (lockRef.current === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (deps.length === 0) {
|
||||
lockRef.current = true;
|
||||
}
|
||||
|
||||
callback();
|
||||
}, [callback]);
|
||||
|
||||
return state;
|
||||
}
|
||||
69
client/shared/hooks/useAsyncFn.ts
Normal file
69
client/shared/hooks/useAsyncFn.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { DependencyList, useCallback, useRef, useState } from 'react';
|
||||
import type { FunctionReturningPromise, PromiseType } from '../types';
|
||||
import { useMountedState } from './useMountedState';
|
||||
|
||||
// Reference: https://github.com/streamich/react-use/blob/master/src/useAsyncFn.ts
|
||||
|
||||
export type AsyncState<T> =
|
||||
| {
|
||||
loading: boolean;
|
||||
error?: undefined;
|
||||
value?: undefined;
|
||||
}
|
||||
| {
|
||||
loading: true;
|
||||
error?: Error | undefined;
|
||||
value?: T;
|
||||
}
|
||||
| {
|
||||
loading: false;
|
||||
error: Error;
|
||||
value?: undefined;
|
||||
}
|
||||
| {
|
||||
loading: false;
|
||||
error?: undefined;
|
||||
value: T;
|
||||
};
|
||||
|
||||
type StateFromFunctionReturningPromise<T extends FunctionReturningPromise> =
|
||||
AsyncState<PromiseType<ReturnType<T>>>;
|
||||
|
||||
export type AsyncFnReturn<
|
||||
T extends FunctionReturningPromise = FunctionReturningPromise
|
||||
> = [StateFromFunctionReturningPromise<T>, T];
|
||||
|
||||
export function useAsyncFn<T extends FunctionReturningPromise>(
|
||||
fn: T,
|
||||
deps: DependencyList = [],
|
||||
initialState: StateFromFunctionReturningPromise<T> = { loading: false }
|
||||
): AsyncFnReturn<T> {
|
||||
const lastCallId = useRef(0);
|
||||
const isMounted = useMountedState();
|
||||
const [state, set] =
|
||||
useState<StateFromFunctionReturningPromise<T>>(initialState);
|
||||
|
||||
const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
|
||||
const callId = ++lastCallId.current;
|
||||
set((prevState) => ({ ...prevState, loading: true }));
|
||||
|
||||
return fn(...args).then(
|
||||
(value) => {
|
||||
isMounted() &&
|
||||
callId === lastCallId.current &&
|
||||
set({ value, loading: false });
|
||||
|
||||
return value;
|
||||
},
|
||||
(error) => {
|
||||
isMounted() &&
|
||||
callId === lastCallId.current &&
|
||||
set({ error, loading: false });
|
||||
|
||||
return error;
|
||||
}
|
||||
) as ReturnType<T>;
|
||||
}, deps);
|
||||
|
||||
return [state, callback as unknown as T];
|
||||
}
|
||||
25
client/shared/hooks/useAsyncRefresh.ts
Normal file
25
client/shared/hooks/useAsyncRefresh.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { DependencyList, useCallback, useEffect } from 'react';
|
||||
import type { FunctionReturningPromise } from '../types';
|
||||
import { useAsyncFn } from './useAsyncFn';
|
||||
|
||||
export function useAsyncRefresh<T extends FunctionReturningPromise>(
|
||||
fn: T,
|
||||
deps: DependencyList = []
|
||||
) {
|
||||
const [state, callback] = useAsyncFn(fn, deps, {
|
||||
loading: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
callback();
|
||||
}, [callback]);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
return callback();
|
||||
}, [callback]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
21
client/shared/hooks/useAsyncRequest.ts
Normal file
21
client/shared/hooks/useAsyncRequest.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { DependencyList } from 'react';
|
||||
import { showErrorToasts } from '../manager/ui';
|
||||
import type { FunctionReturningPromise } from '../types';
|
||||
import { useAsyncFn } from './useAsyncFn';
|
||||
|
||||
export function useAsyncRequest<T extends FunctionReturningPromise>(
|
||||
fn: T,
|
||||
deps: DependencyList = []
|
||||
) {
|
||||
const [{ loading, value }, call] = useAsyncFn(async (...args: any[]) => {
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (err) {
|
||||
// showErrorToasts(isDevelopment ? err : t('系统忙, 请稍后再试'));
|
||||
showErrorToasts(err); // 暂时放开所有错误抛出,正确的做法应该是仅对于内置代码相关的逻辑显示placeholder报错
|
||||
console.error('[useAsyncRequest] error:', err);
|
||||
}
|
||||
}, deps);
|
||||
|
||||
return [{ loading, value }, call as T] as const;
|
||||
}
|
||||
27
client/shared/hooks/useDataReady.ts
Normal file
27
client/shared/hooks/useDataReady.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { DependencyList, useLayoutEffect, useRef } from 'react';
|
||||
import { useEvent } from './useEvent';
|
||||
|
||||
/**
|
||||
* Call once on data ready(validator return true)
|
||||
*/
|
||||
export function useDataReady(
|
||||
validator: () => boolean,
|
||||
cb: () => void,
|
||||
deps?: DependencyList
|
||||
) {
|
||||
const isReadyRef = useRef(false);
|
||||
|
||||
const _validator = useEvent(validator);
|
||||
const _callback = useEvent(cb);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isReadyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_validator() === true) {
|
||||
_callback();
|
||||
isReadyRef.current = true;
|
||||
}
|
||||
}, deps);
|
||||
}
|
||||
17
client/shared/hooks/useDebounce.ts
Normal file
17
client/shared/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { DependencyList, useEffect } from 'react';
|
||||
import { useTimeoutFn } from './useTimeoutFn';
|
||||
|
||||
export type UseDebounceReturn = [() => boolean | null, () => void];
|
||||
|
||||
export function useDebounce(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
fn: Function,
|
||||
ms = 0,
|
||||
deps: DependencyList = []
|
||||
): UseDebounceReturn {
|
||||
const [isReady, cancel, reset] = useTimeoutFn(fn, ms);
|
||||
|
||||
useEffect(reset, deps);
|
||||
|
||||
return [isReady, cancel];
|
||||
}
|
||||
15
client/shared/hooks/useEditValue.ts
Normal file
15
client/shared/hooks/useEditValue.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
|
||||
export function useEditValue<T>(value: T, onChange: (val: T) => void) {
|
||||
const [inner, setInner] = useState(value);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setInner(value);
|
||||
}, [value]);
|
||||
|
||||
const onSave = useCallback(() => {
|
||||
onChange(inner);
|
||||
}, [inner, onChange]);
|
||||
|
||||
return [inner, setInner, onSave] as const;
|
||||
}
|
||||
7
client/shared/hooks/useEffectOnce.ts
Normal file
7
client/shared/hooks/useEffectOnce.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { EffectCallback, useEffect } from 'react';
|
||||
|
||||
// Reference: https://github.com/streamich/react-use/blob/master/src/useEffectOnce.ts
|
||||
|
||||
export const useEffectOnce = (effect: EffectCallback) => {
|
||||
useEffect(effect, []);
|
||||
};
|
||||
3
client/shared/hooks/useEvent.ts
Normal file
3
client/shared/hooks/useEvent.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { useMemoizedFn } from './useMemoizedFn';
|
||||
|
||||
export const useEvent = useMemoizedFn;
|
||||
41
client/shared/hooks/useInterval.ts
Normal file
41
client/shared/hooks/useInterval.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useUpdateRef } from './useUpdateRef';
|
||||
|
||||
export function useInterval(
|
||||
fn: () => void,
|
||||
delay: number | undefined,
|
||||
options?: {
|
||||
immediate?: boolean;
|
||||
}
|
||||
) {
|
||||
const immediate = options?.immediate;
|
||||
|
||||
const fnRef = useUpdateRef(fn);
|
||||
const timerRef = useRef<number>();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof delay !== 'number' || delay < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
fnRef.current();
|
||||
}
|
||||
timerRef.current = window.setInterval(() => {
|
||||
fnRef.current();
|
||||
}, delay);
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
window.clearInterval(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, [delay]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
if (timerRef.current) {
|
||||
window.clearInterval(timerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return clear;
|
||||
}
|
||||
17
client/shared/hooks/useLazyValue.ts
Normal file
17
client/shared/hooks/useLazyValue.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import { useEvent } from './useEvent';
|
||||
|
||||
export function useLazyValue<T>(value: T, onChange: (val: T) => void) {
|
||||
const [inner, setInner] = useState(value);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setInner(value);
|
||||
}, [value]);
|
||||
|
||||
const handleChange = useEvent((val: T) => {
|
||||
setInner(val);
|
||||
onChange(val);
|
||||
});
|
||||
|
||||
return [inner, handleChange] as const;
|
||||
}
|
||||
36
client/shared/hooks/useMemoizedFn.ts
Normal file
36
client/shared/hooks/useMemoizedFn.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import _isFunction from 'lodash/isFunction';
|
||||
|
||||
// From https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useMemoizedFn/index.ts
|
||||
|
||||
type Noop = (this: any, ...args: any[]) => any;
|
||||
|
||||
type PickFunction<T extends Noop> = (
|
||||
this: ThisParameterType<T>,
|
||||
...args: Parameters<T>
|
||||
) => ReturnType<T>;
|
||||
|
||||
export function useMemoizedFn<T extends Noop>(fn: T) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (!_isFunction(fn)) {
|
||||
console.error(
|
||||
`useMemoizedFn expected parameter is a function, got ${typeof fn}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fnRef = useRef<T>(fn);
|
||||
|
||||
// why not write `fnRef.current = fn`?
|
||||
// https://github.com/alibaba/hooks/issues/728
|
||||
fnRef.current = useMemo(() => fn, [fn]);
|
||||
|
||||
const memoizedFn = useRef<PickFunction<T>>();
|
||||
if (!memoizedFn.current) {
|
||||
memoizedFn.current = function (this, ...args) {
|
||||
return fnRef.current.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
return memoizedFn.current as T;
|
||||
}
|
||||
18
client/shared/hooks/useMountedState.ts
Normal file
18
client/shared/hooks/useMountedState.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
// Reference: https://github.com/streamich/react-use/blob/master/src/useMountedState.ts
|
||||
|
||||
export function useMountedState(): () => boolean {
|
||||
const mountedRef = useRef<boolean>(false);
|
||||
const get = useCallback(() => mountedRef.current, []);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return get;
|
||||
}
|
||||
11
client/shared/hooks/usePrevious.ts
Normal file
11
client/shared/hooks/usePrevious.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function usePrevious<T>(state: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = state;
|
||||
});
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
26
client/shared/hooks/useRafState.ts
Normal file
26
client/shared/hooks/useRafState.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { useUnmount } from './useUnmount';
|
||||
|
||||
// Reference: https://github.com/streamich/react-use/blob/master/src/useRafState.ts
|
||||
|
||||
export const useRafState = <S>(
|
||||
initialState: S | (() => S)
|
||||
): [S, Dispatch<SetStateAction<S>>] => {
|
||||
const frame = useRef(0);
|
||||
const [state, setState] = useState(initialState);
|
||||
|
||||
const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
|
||||
frame.current = requestAnimationFrame(() => {
|
||||
setState(value);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useUnmount(() => {
|
||||
cancelAnimationFrame(frame.current);
|
||||
});
|
||||
|
||||
return [state, setRafState];
|
||||
};
|
||||
57
client/shared/hooks/useSearch.ts
Normal file
57
client/shared/hooks/useSearch.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useFriendNicknameMap } from '../redux/hooks/useFriendNickname';
|
||||
import type { UserBaseInfo } from 'tailchat-types';
|
||||
|
||||
export interface UseSearchOptions<T> {
|
||||
dataSource: T[];
|
||||
filterFn: (item: T, searchText: string) => boolean;
|
||||
}
|
||||
|
||||
export function useSearch<T>(options: UseSearchOptions<T>) {
|
||||
const { dataSource, filterFn } = options;
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const isSearching = searchText !== '';
|
||||
|
||||
const searchResult = useMemo(() => {
|
||||
return dataSource.filter((item) => filterFn(item, searchText));
|
||||
}, [dataSource, searchText]);
|
||||
|
||||
return {
|
||||
searchText,
|
||||
setSearchText,
|
||||
isSearching,
|
||||
searchResult,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于搜索用户的封装函数
|
||||
*/
|
||||
export function useUserSearch(userInfos: UserBaseInfo[]) {
|
||||
const friendNicknameMap = useFriendNicknameMap();
|
||||
const validUserInfos = useMemo(() => userInfos.filter(Boolean), [userInfos]);
|
||||
|
||||
const { searchText, setSearchText, isSearching, searchResult } = useSearch({
|
||||
dataSource: validUserInfos,
|
||||
filterFn: (item, searchText) => {
|
||||
if (friendNicknameMap[item._id]) {
|
||||
if (friendNicknameMap[item._id].includes(searchText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.nickname.includes(searchText)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
searchText,
|
||||
setSearchText,
|
||||
isSearching,
|
||||
searchResult,
|
||||
};
|
||||
}
|
||||
17
client/shared/hooks/useShallowObject.ts
Normal file
17
client/shared/hooks/useShallowObject.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
|
||||
/**
|
||||
* 对输入对象增加一层浅比较, 如果对象浅比较结果一致则返回原对象(防止更新)
|
||||
*/
|
||||
export function useShallowObject<T>(object: T): T {
|
||||
const [state, setState] = useState<T>(object);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!shallowEqual(state, object)) {
|
||||
setState(object);
|
||||
}
|
||||
}, [object]);
|
||||
|
||||
return state;
|
||||
}
|
||||
44
client/shared/hooks/useTimeoutFn.ts
Normal file
44
client/shared/hooks/useTimeoutFn.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];
|
||||
|
||||
export function useTimeoutFn(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
fn: Function,
|
||||
ms = 0
|
||||
): UseTimeoutFnReturn {
|
||||
const ready = useRef<boolean | null>(false);
|
||||
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
const callback = useRef(fn);
|
||||
|
||||
const isReady = useCallback(() => ready.current, []);
|
||||
|
||||
const set = useCallback(() => {
|
||||
ready.current = false;
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
ready.current = true;
|
||||
callback.current();
|
||||
}, ms);
|
||||
}, [ms]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
ready.current = null;
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
}, []);
|
||||
|
||||
// update ref when function changes
|
||||
useEffect(() => {
|
||||
callback.current = fn;
|
||||
}, [fn]);
|
||||
|
||||
// set on mount, clear on unmount
|
||||
useEffect(() => {
|
||||
set();
|
||||
|
||||
return clear;
|
||||
}, [ms]);
|
||||
|
||||
return [isReady, clear, set];
|
||||
}
|
||||
13
client/shared/hooks/useUnmount.ts
Normal file
13
client/shared/hooks/useUnmount.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useRef } from 'react';
|
||||
import { useEffectOnce } from './useEffectOnce';
|
||||
|
||||
// Reference: https://github.com/streamich/react-use/blob/master/src/useUnmount.ts
|
||||
|
||||
export const useUnmount = (fn: () => any): void => {
|
||||
const fnRef = useRef(fn);
|
||||
|
||||
// update the ref each render so if it change the newest callback will be invoked
|
||||
fnRef.current = fn;
|
||||
|
||||
useEffectOnce(() => () => fnRef.current());
|
||||
};
|
||||
4
client/shared/hooks/useUpdateEffect.ts
Normal file
4
client/shared/hooks/useUpdateEffect.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { createUpdateEffect } from './factory/createUpdateEffect';
|
||||
|
||||
export const useUpdateEffect = createUpdateEffect(useEffect);
|
||||
8
client/shared/hooks/useUpdateRef.ts
Normal file
8
client/shared/hooks/useUpdateRef.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { useRef, MutableRefObject } from 'react';
|
||||
|
||||
export function useUpdateRef<T>(state: T): MutableRefObject<T> {
|
||||
const ref = useRef<T>(state);
|
||||
ref.current = state;
|
||||
|
||||
return ref;
|
||||
}
|
||||
12
client/shared/hooks/useWatch.ts
Normal file
12
client/shared/hooks/useWatch.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { DependencyList, useLayoutEffect } from 'react';
|
||||
import { useMemoizedFn } from './useMemoizedFn';
|
||||
|
||||
/**
|
||||
* 监听变更并触发回调
|
||||
*/
|
||||
export function useWatch(deps: DependencyList, cb: () => void) {
|
||||
const memoizedFn = useMemoizedFn(cb);
|
||||
useLayoutEffect(() => {
|
||||
memoizedFn();
|
||||
}, deps);
|
||||
}
|
||||
103
client/shared/hooks/useWhyDidYouUpdate.ts
Normal file
103
client/shared/hooks/useWhyDidYouUpdate.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 检查组件变动情况(是什么导致了组件刷新)
|
||||
* Fork from: https://github.com/devhubapp/devhub/blob/master/packages/components/src/hooks/use-why-did-you-update.ts
|
||||
*/
|
||||
|
||||
import { parse, stringify } from 'flatted';
|
||||
import _get from 'lodash/get';
|
||||
import _isEqual from 'lodash/isEqual';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { isDevelopment } from '../utils/environment';
|
||||
|
||||
interface UseWhyDidYouUpdateCallback<T> {
|
||||
onChangeFound?: (data: {
|
||||
changesObj: Record<
|
||||
string,
|
||||
{
|
||||
from: T;
|
||||
to: T;
|
||||
isDeepEqual: boolean;
|
||||
changedKeys?: string[];
|
||||
}
|
||||
>;
|
||||
}) => void;
|
||||
onNoChangeFound?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quickly see which prop changed
|
||||
* and caused a re-render by adding a single line to the component.
|
||||
*
|
||||
* USAGE:
|
||||
* function MyComponent(props) {
|
||||
* useWhyDidYouUpdate('MyComponent', props)
|
||||
*
|
||||
* return <div ... />
|
||||
* }
|
||||
*
|
||||
* OUTPUT:
|
||||
* [why-did-you-update] MyComponent { myProp: { from 'oldvalue', to: 'newvalue' } }
|
||||
*
|
||||
* SHARE:
|
||||
* This tip on Twitter: https://twitter.com/brunolemos/status/1090377532845801473
|
||||
* Also follow @brunolemos: https://twitter.com/brunolemos
|
||||
*/
|
||||
export function useWhyDidYouUpdate<T>(
|
||||
name: string,
|
||||
props: Record<string, T>,
|
||||
{ onChangeFound, onNoChangeFound }: UseWhyDidYouUpdateCallback<T> = {}
|
||||
) {
|
||||
const latestProps = useRef(props);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDevelopment) return;
|
||||
|
||||
const allKeys = Object.keys({ ...latestProps.current, ...props });
|
||||
|
||||
const changesObj: Record<
|
||||
string,
|
||||
{
|
||||
from: T;
|
||||
to: T;
|
||||
isDeepEqual: boolean;
|
||||
changedKeys?: string[];
|
||||
}
|
||||
> = {};
|
||||
allKeys.forEach((key) => {
|
||||
if (latestProps.current[key] !== props[key]) {
|
||||
changesObj[key] = {
|
||||
from: latestProps.current[key],
|
||||
to: props[key],
|
||||
changedKeys:
|
||||
props[key] && typeof props[key] === 'object'
|
||||
? Object.keys(latestProps.current[key] as object)
|
||||
.map((k) =>
|
||||
_get(latestProps.current, [key, k]) ===
|
||||
_get(props, [key, k])
|
||||
? ''
|
||||
: k
|
||||
)
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
isDeepEqual: _isEqual(latestProps.current[key], props[key]),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(changesObj).length) {
|
||||
if (onChangeFound) {
|
||||
onChangeFound({ changesObj });
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[why-did-you-update]', name, {
|
||||
changes: parse(stringify(changesObj)),
|
||||
props: { from: latestProps.current, to: props },
|
||||
});
|
||||
}
|
||||
} else if (onNoChangeFound) {
|
||||
onNoChangeFound();
|
||||
}
|
||||
|
||||
latestProps.current = props;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user