[dev] Define custom static dependencies (#2462)

This commit is contained in:
Alexander Zinchuk 2023-02-28 18:43:28 +01:00
parent ad576da6ce
commit 568b23b6fe
39 changed files with 63 additions and 46 deletions

View File

@ -10,6 +10,7 @@
"no-async-without-await",
"teactn",
"no-null",
"react-hooks-static-deps",
"eslint-multitab-tt"
],
"rules": {
@ -40,10 +41,17 @@
"no-console": "error",
"semi": "error",
"no-implicit-coercion": "error",
"react-hooks/exhaustive-deps": [
"react-hooks/exhaustive-deps": "off",
"react-hooks-static-deps/exhaustive-deps": [
"error",
{
"additionalHooks": "(useSyncEffect|useAsync|useDebouncedCallback|useThrottledCallback|useEffectWithPrevDeps|useLayoutEffectWithPrevDeps|useDerivedState|useDerivedSignal|useThrottledResolver|useDebouncedResolver)$"
"additionalHooks": "(useSyncEffect|useAsync|useDebouncedCallback|useThrottledCallback|useEffectWithPrevDeps|useLayoutEffectWithPrevDeps|useDerivedState|useDerivedSignal|useThrottledResolver|useDebouncedResolver)$",
"staticHooks": {
"getActions": true,
"useFlag": [false, true, true],
"useForceUpdate": true,
"useReducer": [false, true]
}
}
],
"arrow-body-style": "off",

7
package-lock.json generated
View File

@ -66,6 +66,7 @@
"eslint-plugin-no-null": "^1.0.2",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-hooks-static-deps": "^1.0.7",
"eslint-plugin-teactn": "git+https://github.com/korenskoy/eslint-plugin-teactn#c2c39dd005d58c07c24c4361de804dce1c6261b5",
"git-revision-webpack-plugin": "^5.0.0",
"gitlog": "^4.0.4",
@ -7413,6 +7414,12 @@
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
}
},
"node_modules/eslint-plugin-react-hooks-static-deps": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks-static-deps/-/eslint-plugin-react-hooks-static-deps-1.0.7.tgz",
"integrity": "sha512-K34LqdZaeqt9y9jsOmvkOUJT5ViTMYsHYm/7eV0WBGztOU36xxDTBZJS3hfJBW8tzzZcxSgsMvZIORzOkyojKQ==",
"dev": true
},
"node_modules/eslint-plugin-react/node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",

View File

@ -86,6 +86,7 @@
"eslint-plugin-no-null": "^1.0.2",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-hooks-static-deps": "^1.0.7",
"eslint-plugin-teactn": "git+https://github.com/korenskoy/eslint-plugin-teactn#c2c39dd005d58c07c24c4361de804dce1c6261b5",
"git-revision-webpack-plugin": "^5.0.0",
"gitlog": "^4.0.4",

View File

@ -80,7 +80,7 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
? VOLUME_ZERO
: ((participant?.volume || GROUP_CALL_DEFAULT_VOLUME) / GROUP_CALL_VOLUME_MULTIPLIER));
// We only want to initialize local volume when switching participants and ignore following updates from server
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [id]);
const runThrottled = useRunThrottled(VOLUME_CHANGE_THROTTLE);

View File

@ -43,6 +43,7 @@ const StatusButton: FC<StateProps> = ({ emojiStatus }) => {
showEffect();
unmarkShouldShowEffect();
}
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [emojiStatus, shouldShowEffect, showEffect, unmarkShouldShowEffect] as const);
const handleEmojiStatusSet = useCallback((sticker: ApiSticker) => {

View File

@ -54,7 +54,7 @@ const AudioResults: FC<OwnProps & StateProps> = ({
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
}, [currentType, lastSyncTime, searchMessagesGlobal, searchQuery]);
const foundMessages = useMemo(() => {

View File

@ -64,7 +64,7 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
}, [lastSyncTime, searchMessagesGlobal, searchQuery]);
const handleTopicClick = useCallback(

View File

@ -84,7 +84,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
}, [lastSyncTime, searchMessagesGlobal, searchQuery]);
const handleChatClick = useCallback(

View File

@ -66,7 +66,7 @@ const FileResults: FC<OwnProps & StateProps> = ({
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
}, [lastSyncTime, searchMessagesGlobal, searchQuery]);
const foundMessages = useMemo(() => {

View File

@ -64,7 +64,7 @@ const LinkResults: FC<OwnProps & StateProps> = ({
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
}, [lastSyncTime, searchMessagesGlobal, searchQuery]);
const foundMessages = useMemo(() => {

View File

@ -62,7 +62,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
}, [lastSyncTime, searchMessagesGlobal, searchQuery]);
const foundMessages = useMemo(() => {

View File

@ -181,7 +181,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
return () => {
clearTimeout(hideTimeout);
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- Old timeout should be cleared only if new confetti is generated
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- Old timeout should be cleared only if new confetti is generated
}, [lastConfettiTime, forceUpdate, updateCanvas]);
if (!lastConfettiTime || Date.now() - lastConfettiTime > CONFETTI_FADEOUT_TIMEOUT) {

View File

@ -268,7 +268,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
}
return debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Around }), 1000, true, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [loadViewportMessages, messageIds]);
const { isScrolled, updateStickyDates } = useStickyDates();
@ -491,7 +491,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
console.timeEnd('scrollTop');
}
// This should match deps for `useSyncEffect` above
// eslint-disable-next-line react-hooks/exhaustive-deps -- `as const` not yet supported by linter
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `as const` not yet supported by linter
}, [messageIds, isViewportNewest, containerHeight, hasTools] as const);
useEffectWithPrevDeps(([prevIsSelectModeActive]) => {

View File

@ -582,9 +582,9 @@ const Composer: FC<OwnProps & StateProps> = ({
const stopRecordingVoiceRef = useStateRef(stopRecordingVoice);
useEffect(() => {
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
stopRecordingVoiceRef.current();
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
resetComposerRef.current();
};
}, [chatId, threadId, resetComposerRef, stopRecordingVoiceRef]);

View File

@ -98,7 +98,7 @@ const useDraft = (
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [
chatId, threadId, draft, setHtml, editedMessage, loadCustomEmojis,
] as const);
@ -106,9 +106,9 @@ const useDraft = (
// Save draft on chat change
useEffect(() => {
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
if (!isEditing) {
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
updateDraftRef.current({ chatId, threadId });
}

View File

@ -58,7 +58,7 @@ const useEditing = (
focusEditableElement(messageInput, true);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps -- `as const` not yet supported by linter
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `as const` not yet supported by linter
}, [editedMessage, replyingToId, setHtml] as const);
useEffect(() => {

View File

@ -45,7 +45,7 @@ export default function useInlineBotTooltip(
if (prevUsername) {
resetInlineBot({ username: prevUsername });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [username, resetInlineBot] as const);
useEffect(() => {

View File

@ -34,7 +34,7 @@ export default function useScrollHooks(
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Backwards }), 1000, true, false),
debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Forwards }), 1000, true, false),
] : []),
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
[loadViewportMessages, messageIds],
);

View File

@ -235,7 +235,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
if (isOpen && isOverlaying) {
close();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [isOverlaying]);
// We need to clear profile state and management screen state, when changing chats

View File

@ -20,7 +20,7 @@ export default function useAsyncRendering<T extends any[]>(dependencies: T, dela
clearTimeout(timeoutRef.current);
timeoutRef.current = undefined;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, dependencies);
useEffect(() => {
@ -38,7 +38,7 @@ export default function useAsyncRendering<T extends any[]>(dependencies: T, dela
} else {
exec();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, dependencies);
return shouldRenderRef.current;

View File

@ -86,7 +86,7 @@ const InfiniteScroll: FC<OwnProps> = ({
onLoadMore({ direction: LoadMoreDirection.Forwards });
}, 1000, true, false),
];
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [onLoadMore, items]);
// Initial preload

View File

@ -19,7 +19,7 @@ const useAsync = <T>(fn: () => Promise<T>, deps: any[], defaultValue?: T) => {
return () => {
wasCancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, deps);
return { isLoading, error, result };
};

View File

@ -4,13 +4,13 @@ import useDebouncedCallback from './useDebouncedCallback';
export function useThrottledResolver<T>(resolver: () => T, deps: any[], ms: number, noFirst = false) {
return useThrottledCallback((setValue: (newValue: T) => void) => {
setValue(resolver());
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, deps, ms, noFirst);
}
export function useDebouncedResolver<T>(resolver: () => T, deps: any[], ms: number, noFirst = false, noLast = false) {
return useDebouncedCallback((setValue: (newValue: T) => void) => {
setValue(resolver());
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, deps, ms, noFirst, noLast);
}

View File

@ -106,7 +106,7 @@ const useAudioPlayer = (
if (!isPlaying && !proxy.paused) {
setIsPlaying(true);
// `isPlayingSync` is only needed to help `setIsPlaying` because it is asynchronous
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
isPlayingSync = true;
}

View File

@ -14,7 +14,7 @@ export default function useBlurSync(dataUri: string | false | undefined) {
let isChanged = false;
useSyncEffect(() => {
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
isChanged = true;
blurredRef.current = undefined;

View File

@ -9,7 +9,7 @@ export default function useDebouncedCallback<T extends AnyToVoidFunction>(
noFirst = false,
noLast = false,
) {
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
const fnMemo = useCallback(fn, deps);
return useMemo(() => {

View File

@ -21,7 +21,7 @@ export default function useDebouncedMemo<R extends any, D extends any[]>(
runDebounced(() => {
setValue(resolverFn());
});
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [...dependencies, isFrozen]);
return value;

View File

@ -31,10 +31,10 @@ function useDerivedSignal<T>(resolverOrDependency: Resolver<T> | T, dependencies
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
useSyncEffect(runCurrentResolver, dependencies);
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
useSignalEffect(runCurrentResolver, dependencies);
return getValue as Signal<T>;

View File

@ -48,10 +48,10 @@ function useDerivedState<T>(resolverOrSignal: Resolver<T> | T, dependencies?: re
useSyncEffect(() => {
runCurrentResolver(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, dependencies);
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
useSignalEffect(runCurrentResolver, dependencies);
return valueRef.current as T;

View File

@ -1,7 +1,7 @@
import { useEffect } from '../lib/teact/teact';
function useEffectOnce(effect: React.EffectCallback) {
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
useEffect(effect, []);
}

View File

@ -7,7 +7,7 @@ const useEffectWithPrevDeps = <T extends readonly any[]>(
const prevDeps = usePrevious<T>(dependencies);
return useEffect(() => {
return cb(prevDeps || []);
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, dependencies, debugKey);
};

View File

@ -147,7 +147,7 @@ export function useIntersectionObserver({
controller.observer.unobserve(target);
};
// Arguments should never change
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, [isDisabled]);
return { observe, freeze, unfreeze };

View File

@ -7,7 +7,7 @@ const useLayoutEffectWithPrevDeps = <T extends readonly any[]>(
const prevDeps = usePrevious<T>(dependencies);
return useLayoutEffect(() => {
return cb(prevDeps || []);
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, dependencies, debugKey);
};

View File

@ -17,7 +17,7 @@ export default function useReducer<State, Actions>(
const dispatch = useCallback((action: ReducerAction<Actions>) => {
state.current = reducerRef.current(state.current, action);
forceUpdate();
}, [forceUpdate]);
}, []);
return [
state.current,

View File

@ -3,6 +3,6 @@ import useDebouncedCallback from './useDebouncedCallback';
export default function useRunDebounced(ms: number, noFirst?: boolean, noLast?: boolean, deps: any = []) {
return useDebouncedCallback((cb: NoneToVoidFunction) => {
cb();
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, deps, ms, noFirst, noLast);
}

View File

@ -3,6 +3,6 @@ import useThrottledCallback from './useThrottledCallback';
export default function useRunThrottled(ms: number, noFirst?: boolean, deps: any = []) {
return useThrottledCallback((cb: NoneToVoidFunction) => {
cb();
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, deps, ms, noFirst);
}

View File

@ -9,7 +9,7 @@ export default function useThrottledCallback<T extends AnyToVoidFunction>(
msOrRaf: number | typeof fastRaf,
noFirst = false,
) {
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
const fnMemo = useCallback(fn, deps);
return useMemo(() => {

View File

@ -16,6 +16,6 @@ export default function useVideoCleanup(videoRef: RefObject<HTMLVideoElement>, d
});
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
}, dependencies);
}

View File

@ -759,7 +759,7 @@ export function useMemo<T extends any>(resolver: () => T, dependencies: any[], d
}
export function useCallback<F extends AnyFunction>(newCallback: F, dependencies: any[], debugKey?: string): F {
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
return useMemo(() => newCallback, dependencies, debugKey);
}
@ -782,7 +782,7 @@ export function useRef<T>(initial?: T | null) {
export function memo<T extends FC>(Component: T, debugKey?: string) {
return function TeactMemoWrapper(props: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
return useMemo(() => createElement(Component, props), Object.values(props), debugKey);
} as T;
}