From 9c25abbd9ac2b39ea5ca76296549d2467a8b14b3 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 8 Feb 2023 00:43:25 +0100 Subject: [PATCH] [Refactoring] Revise hook dependencies (#2424) --- .eslintrc | 7 ++++- src/components/auth/CountryCodeInput.tsx | 10 +++---- src/components/common/AnimatedSticker.tsx | 6 ++--- src/components/common/UiLoader.tsx | 13 ++++----- src/components/left/LeftColumn.tsx | 4 +-- src/components/left/main/ChatList.tsx | 2 +- src/components/main/ConfettiContainer.tsx | 21 +++++++-------- src/components/main/Main.tsx | 18 ++++++------- src/components/main/WebAppModal.tsx | 10 +++---- .../main/premium/PremiumMainModal.tsx | 6 ++--- .../mediaViewer/MediaViewerSlides.tsx | 2 +- src/components/middle/MessageList.tsx | 19 ++++++------- src/components/middle/MiddleColumn.tsx | 6 ++--- src/components/middle/composer/Composer.tsx | 4 +-- .../middle/composer/MessageInput.tsx | 2 +- .../middle/composer/WebPagePreview.tsx | 6 ++--- .../middle/composer/hooks/useEditing.ts | 1 + src/components/middle/hooks/useScrollHooks.ts | 12 +++++---- src/components/middle/message/Invoice.tsx | 2 +- src/components/middle/message/Location.tsx | 2 +- src/components/middle/message/Photo.tsx | 2 +- .../right/hooks/useAsyncRendering.ts | 5 ++-- src/components/right/hooks/useProfileState.ts | 4 +-- .../right/hooks/useProfileViewportIds.ts | 6 ++--- .../right/management/ManageInvite.tsx | 4 +-- src/components/ui/OptimizedVideo.tsx | 6 ++--- src/hooks/useAudioPlayer.ts | 6 +++-- src/hooks/useBlurSync.ts | 5 ++-- src/hooks/useCanvasBlur.ts | 4 +-- src/hooks/useDebouncedMemo.ts | 5 ++-- src/hooks/useEffectOnce.ts | 8 ++++++ src/hooks/useForumPanelRender.ts | 4 +-- src/hooks/useHistoryBack.ts | 27 ++++++++++--------- src/hooks/useIntersectionObserver.ts | 6 ++--- src/hooks/useLang.ts | 4 +-- src/hooks/usePrevDuringAnimation.ts | 6 ++--- src/hooks/useStateRef.ts | 4 +-- .../{useOnChange.ts => useSyncEffect.ts} | 4 +-- src/hooks/useWindowSize.ts | 2 +- 39 files changed, 145 insertions(+), 120 deletions(-) create mode 100644 src/hooks/useEffectOnce.ts rename src/hooks/{useOnChange.ts => useSyncEffect.ts} (58%) diff --git a/.eslintrc b/.eslintrc index ed0ca4ea0..e45a0b8d4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -40,7 +40,12 @@ "no-console": "error", "semi": "error", "no-implicit-coercion": "error", - "react-hooks/exhaustive-deps": "error", + "react-hooks/exhaustive-deps": [ + "error", + { + "additionalHooks": "(useSyncEffect|useAsync|useDebouncedCallback|useThrottledCallback|useEffectWithPrevDeps|useLayoutEffectWithPrevDeps)$" + } + ], "arrow-body-style": "off", "no-else-return": "off", "no-plusplus": "off", diff --git a/src/components/auth/CountryCodeInput.tsx b/src/components/auth/CountryCodeInput.tsx index 67a299353..708df3405 100644 --- a/src/components/auth/CountryCodeInput.tsx +++ b/src/components/auth/CountryCodeInput.tsx @@ -12,7 +12,7 @@ import buildClassName from '../../util/buildClassName'; import renderText from '../common/helpers/renderText'; import useLang from '../../hooks/useLang'; import { isoToEmoji } from '../../util/emoji'; -import useOnChange from '../../hooks/useOnChange'; +import useSyncEffect from '../../hooks/useSyncEffect'; import DropdownMenu from '../ui/DropdownMenu'; import MenuItem from '../ui/MenuItem'; @@ -53,11 +53,11 @@ const CountryCodeInput: FC = ({ setFilteredList(getFilteredList(phoneCodeList, filterValue)); }, [phoneCodeList]); - useOnChange(([prevPhoneCodeList]) => { - if (prevPhoneCodeList?.length === 0 && phoneCodeList.length > 0) { - updateFilter(filter); + useSyncEffect(([prevPhoneCodeList]) => { + if (!prevPhoneCodeList?.length && phoneCodeList.length) { + setFilteredList(getFilteredList(phoneCodeList, filter)); } - }, [phoneCodeList, updateFilter]); + }, [phoneCodeList, filter]); const handleChange = useCallback((country: ApiCountryCode) => { onChange(country); diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index 61ff8dbd5..09804f2c9 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -12,7 +12,7 @@ import generateIdFor from '../../util/generateIdFor'; import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck'; import useBackgroundMode from '../../hooks/useBackgroundMode'; -import useOnChange from '../../hooks/useOnChange'; +import useSyncEffect from '../../hooks/useSyncEffect'; import useAppLayout from '../../hooks/useAppLayout'; export type OwnProps = { @@ -229,13 +229,13 @@ const AnimatedSticker: FC = ({ fastRaf(unfreezeAnimation); }, [unfreezeAnimation]); - useOnChange(([prevNoLoop]) => { + useSyncEffect(([prevNoLoop]) => { if (prevNoLoop !== undefined && noLoop !== prevNoLoop) { animation?.setNoLoop(noLoop); } }, [noLoop, animation]); - useOnChange(([prevSharedCanvasCoords, prevIsMobile]) => { + useSyncEffect(([prevSharedCanvasCoords, prevIsMobile]) => { if ( (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) || (prevIsMobile !== undefined && isMobile !== prevIsMobile) diff --git a/src/components/common/UiLoader.tsx b/src/components/common/UiLoader.tsx index 8f19421a0..a1cb255b6 100644 --- a/src/components/common/UiLoader.tsx +++ b/src/components/common/UiLoader.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from '../../lib/teact/teact'; +import React from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; import { ApiMediaFormat } from '../../api/types'; @@ -13,8 +13,6 @@ import { selectTabState, } from '../../global/selectors'; import { DARK_THEME_BG_COLOR, LIGHT_THEME_BG_COLOR } from '../../config'; -import useFlag from '../../hooks/useFlag'; -import useShowTransition from '../../hooks/useShowTransition'; import { pause } from '../../util/schedulers'; import { preloadImage } from '../../util/files'; import preloadFonts from '../../util/fonts'; @@ -22,6 +20,10 @@ import * as mediaLoader from '../../util/mediaLoader'; import { Bundles, loadModule } from '../../util/moduleLoader'; import buildClassName from '../../util/buildClassName'; +import useFlag from '../../hooks/useFlag'; +import useShowTransition from '../../hooks/useShowTransition'; +import useEffectOnce from '../../hooks/useEffectOnce'; + import styles from './UiLoader.module.scss'; import telegramLogoPath from '../../assets/telegram-logo.svg'; @@ -114,7 +116,7 @@ const UiLoader: FC = ({ shouldRender: shouldRenderMask, transitionClassNames, } = useShowTransition(!isReady, undefined, true); - useEffect(() => { + useEffectOnce(() => { let timeout: number | undefined; const safePreload = async () => { @@ -145,8 +147,7 @@ const UiLoader: FC = ({ setIsUiReady({ uiReadyState: 0 }); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }); return (
= ({ } }, [clearTwoFaError, loadPasswordInfo, settingsScreen]); - useOnChange(() => { + useSyncEffect(() => { if (nextSettingsScreen !== undefined) { setContent(LeftColumnContent.Settings); setSettingsScreen(nextSettingsScreen); diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx index a6ca83222..d3a52955b 100644 --- a/src/components/left/main/ChatList.tsx +++ b/src/components/left/main/ChatList.tsx @@ -117,7 +117,7 @@ const ChatList: FC = ({ return; } openChat({ id: chatId, shouldReplaceHistory: true }); - }, [], DRAG_ENTER_DEBOUNCE, true); + }, [openChat], DRAG_ENTER_DEBOUNCE, true); const handleDragLeave = useCallback((e: React.DragEvent) => { const rect = e.currentTarget.getBoundingClientRect(); diff --git a/src/components/main/ConfettiContainer.tsx b/src/components/main/ConfettiContainer.tsx index be76569c8..8a576877c 100644 --- a/src/components/main/ConfettiContainer.tsx +++ b/src/components/main/ConfettiContainer.tsx @@ -1,4 +1,4 @@ -import React, { memo, useRef } from '../../lib/teact/teact'; +import React, { memo, useCallback, useRef } from '../../lib/teact/teact'; import { withGlobal } from '../../global'; import type { TabState } from '../../global/types'; @@ -9,7 +9,7 @@ import buildStyle from '../../util/buildStyle'; import { selectTabState } from '../../global/selectors'; import useWindowSize from '../../hooks/useWindowSize'; -import useOnChange from '../../hooks/useOnChange'; +import useSyncEffect from '../../hooks/useSyncEffect'; import useForceUpdate from '../../hooks/useForceUpdate'; import useAppLayout from '../../hooks/useAppLayout'; @@ -55,7 +55,7 @@ const ConfettiContainer: FC = ({ confetti }) => { lastConfettiTime, top, width, left, height, } = confetti || {}; - function generateConfetti(w: number, h: number, amount = defaultConfettiAmount) { + const generateConfetti = useCallback((w: number, h: number, amount = defaultConfettiAmount) => { for (let i = 0; i < amount; i++) { const leftSide = i % 2; const pos = { @@ -83,9 +83,9 @@ const ConfettiContainer: FC = ({ confetti }) => { frameCount: 0, }); } - } + }, [defaultConfettiAmount]); - const updateCanvas = () => { + const updateCanvas = useCallback(() => { if (!canvasRef.current || !isRafStartedRef.current) { return; } @@ -166,9 +166,9 @@ const ConfettiContainer: FC = ({ confetti }) => { } else { isRafStartedRef.current = false; } - }; + }, []); - useOnChange(([prevConfettiTime]) => { + useSyncEffect(([prevConfettiTime]) => { let hideTimeout: ReturnType; if (prevConfettiTime !== lastConfettiTime) { generateConfetti(width || windowSize.width, height || windowSize.height); @@ -179,11 +179,10 @@ const ConfettiContainer: FC = ({ confetti }) => { } } return () => { - if (hideTimeout) { - clearTimeout(hideTimeout); - } + clearTimeout(hideTimeout); }; - }, [lastConfettiTime, updateCanvas]); + // eslint-disable-next-line react-hooks/exhaustive-deps -- Old timeout should be cleared only if new confetti is generated + }, [lastConfettiTime, forceUpdate, updateCanvas]); if (!lastConfettiTime || Date.now() - lastConfettiTime > CONFETTI_FADEOUT_TIMEOUT) { return undefined; diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 7dee6414c..3f5d2878f 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -35,7 +35,7 @@ import { fastRaf } from '../../util/schedulers'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useBackgroundMode from '../../hooks/useBackgroundMode'; import useBeforeUnload from '../../hooks/useBeforeUnload'; -import useOnChange from '../../hooks/useOnChange'; +import useSyncEffect from '../../hooks/useSyncEffect'; import usePreventPinchZoomGesture from '../../hooks/usePreventPinchZoomGesture'; import useForceUpdate from '../../hooks/useForceUpdate'; import useShowTransition from '../../hooks/useShowTransition'; @@ -272,7 +272,7 @@ const Main: FC = ({ ignoreCache: true, }); } - }, [lastSyncTime, isMasterTab] as const); + }, [lastSyncTime, isMasterTab, loadCustomEmojis]); // Sticker sets useEffect(() => { @@ -324,7 +324,7 @@ const Main: FC = ({ type: parsedLocationHash.type, }); } - }, [lastSyncTime] as const); + }, [lastSyncTime, openChat]); const leftColumnTransition = useShowTransition( isLeftColumnOpen, undefined, true, undefined, shouldSkipHistoryAnimations, @@ -333,8 +333,8 @@ const Main: FC = ({ const forceUpdate = useForceUpdate(); // Handle opening middle column - useOnChange(([prevIsLeftColumnOpen]) => { - if (prevIsLeftColumnOpen === undefined || animationLevel === 0) { + useSyncEffect(([prevIsLeftColumnOpen]) => { + if (prevIsLeftColumnOpen === undefined || isLeftColumnOpen === prevIsLeftColumnOpen || animationLevel === 0) { return; } @@ -353,7 +353,7 @@ const Main: FC = ({ willAnimateLeftColumnRef.current = false; forceUpdate(); }); - }, [isLeftColumnOpen]); + }, [animationLevel, forceUpdate, isLeftColumnOpen]); const rightColumnTransition = useShowTransition( isRightColumnOpen, undefined, true, undefined, shouldSkipHistoryAnimations, @@ -362,8 +362,8 @@ const Main: FC = ({ const [isNarrowMessageList, setIsNarrowMessageList] = useState(isRightColumnOpen); // Handle opening right column - useOnChange(([prevIsRightColumnOpen]) => { - if (prevIsRightColumnOpen === undefined) { + useSyncEffect(([prevIsRightColumnOpen]) => { + if (prevIsRightColumnOpen === undefined || isRightColumnOpen === prevIsRightColumnOpen) { return; } @@ -382,7 +382,7 @@ const Main: FC = ({ forceUpdate(); setIsNarrowMessageList(isRightColumnOpen); }); - }, [isRightColumnOpen]); + }, [animationLevel, forceUpdate, isRightColumnOpen]); const className = buildClassName( leftColumnTransition.hasShownClass && 'left-column-shown', diff --git a/src/components/main/WebAppModal.tsx b/src/components/main/WebAppModal.tsx index ba11bd5c3..556c28db0 100644 --- a/src/components/main/WebAppModal.tsx +++ b/src/components/main/WebAppModal.tsx @@ -18,7 +18,7 @@ import { extractCurrentThemeParams, validateHexColor } from '../../util/themeSty import useInterval from '../../hooks/useInterval'; import useLang from '../../hooks/useLang'; -import useOnChange from '../../hooks/useOnChange'; +import useSyncEffect from '../../hooks/useSyncEffect'; import useWebAppFrame from './hooks/useWebAppFrame'; import usePrevious from '../../hooks/usePrevious'; import useFlag from '../../hooks/useFlag'; @@ -258,20 +258,20 @@ const WebAppModal: FC = ({ }, [handlePopupClose]); // Notify view that height changed - useOnChange(() => { + useSyncEffect(() => { setTimeout(() => { sendViewport(); }, ANIMATION_WAIT); }, [mainButton?.isVisible, sendViewport]); // Notify view that theme changed - useOnChange(() => { + useSyncEffect(() => { setTimeout(() => { sendTheme(); }, ANIMATION_WAIT); }, [theme, sendTheme]); - useOnChange(([prevIsPaymentModalOpen]) => { + useSyncEffect(([prevIsPaymentModalOpen]) => { if (isPaymentModalOpen === prevIsPaymentModalOpen) return; if (webApp?.slug && !isPaymentModalOpen && paymentStatus) { sendEvent({ @@ -285,7 +285,7 @@ const WebAppModal: FC = ({ slug: undefined, }); } - }, [isPaymentModalOpen, paymentStatus, sendEvent, setWebAppPaymentSlug, webApp] as const); + }, [isPaymentModalOpen, paymentStatus, sendEvent, setWebAppPaymentSlug, webApp]); const handleToggleClick = useCallback(() => { toggleAttachBot({ diff --git a/src/components/main/premium/PremiumMainModal.tsx b/src/components/main/premium/PremiumMainModal.tsx index 577f35eeb..b035b0802 100644 --- a/src/components/main/premium/PremiumMainModal.tsx +++ b/src/components/main/premium/PremiumMainModal.tsx @@ -22,7 +22,7 @@ import renderText from '../../common/helpers/renderText'; import { getUserFullName } from '../../../global/helpers'; import useLang from '../../../hooks/useLang'; -import useOnChange from '../../../hooks/useOnChange'; +import useSyncEffect from '../../../hooks/useSyncEffect'; import Modal from '../../ui/Modal'; import Button from '../../ui/Button'; @@ -170,11 +170,11 @@ const PremiumMainModal: FC = ({ } }, [isSuccess, showConfetti]); - useOnChange(([prevIsPremium]) => { + useSyncEffect(([prevIsPremium]) => { if (prevIsPremium === isPremium) return; showConfetti(); - }, [isPremium]); + }, [isPremium, showConfetti]); if (!promo) return undefined; diff --git a/src/components/mediaViewer/MediaViewerSlides.tsx b/src/components/mediaViewer/MediaViewerSlides.tsx index 62bafc599..1923bf17c 100644 --- a/src/components/mediaViewer/MediaViewerSlides.tsx +++ b/src/components/mediaViewer/MediaViewerSlides.tsx @@ -120,7 +120,7 @@ const MediaViewerSlides: FC = ({ forceUpdate(); }, [forceUpdate]); - const selectMediaDebounced = useDebouncedCallback(selectMedia, [], DEBOUNCE_MESSAGE, true); + const selectMediaDebounced = useDebouncedCallback(selectMedia, [selectMedia], DEBOUNCE_MESSAGE, true); const clearSwipeDirectionDebounced = useDebouncedCallback(() => { swipeDirectionRef.current = undefined; }, [], DEBOUNCE_SWIPE, true); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 635817cf6..041b7f247 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -50,7 +50,7 @@ import resetScroll, { patchChromiumScroll } from '../../util/resetScroll'; import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll'; import renderText from '../common/helpers/renderText'; -import useOnChange from '../../hooks/useOnChange'; +import useSyncEffect from '../../hooks/useSyncEffect'; import useStickyDates from './hooks/useStickyDates'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import useLang from '../../hooks/useLang'; @@ -196,7 +196,7 @@ const MessageList: FC = ({ const areMessagesLoaded = Boolean(messageIds); - useOnChange(() => { + useSyncEffect(() => { // We only need it first time when message list appears if (areMessagesLoaded) { onTickEnd(() => { @@ -206,24 +206,24 @@ const MessageList: FC = ({ }, [areMessagesLoaded]); // Updated every time (to be used from intersection callback closure) - useOnChange(() => { + useSyncEffect(() => { memoFirstUnreadIdRef.current = firstUnreadId; }, [firstUnreadId]); - useOnChange(() => { + useEffect(() => { if (!isCurrentUserPremium && isChannelChat && isReady && lastSyncTime) { loadSponsoredMessages({ chatId }); } - }, [isCurrentUserPremium, chatId, isReady, isChannelChat, lastSyncTime]); + }, [isCurrentUserPremium, chatId, isReady, isChannelChat, lastSyncTime, loadSponsoredMessages]); // Updated only once when messages are loaded (as we want the unread divider to keep its position) - useOnChange(() => { + useSyncEffect(() => { if (areMessagesLoaded) { memoUnreadDividerBeforeIdRef.current = memoFirstUnreadIdRef.current; } }, [areMessagesLoaded]); - useOnChange(() => { + useSyncEffect(() => { memoFocusingIdRef.current = focusingId; }, [focusingId]); @@ -344,7 +344,7 @@ const MessageList: FC = ({ }, [isChatLoaded, messageIds, loadMoreAround, focusingId, isRestricted]); // Remember scroll position before repositioning it - useOnChange(() => { + useSyncEffect(() => { if (!messageIds || !listItemElementsRef.current) { return; } @@ -488,7 +488,8 @@ const MessageList: FC = ({ // eslint-disable-next-line no-console console.timeEnd('scrollTop'); } - // This should match deps for `useOnChange` above + // This should match deps for `useSyncEffect` above + // eslint-disable-next-line react-hooks/exhaustive-deps -- `as const` not yet supported by linter }, [messageIds, isViewportNewest, containerHeight, hasTools] as const); useEffectWithPrevDeps(([prevIsSelectModeActive]) => { diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index c91ba1072..0d94c859a 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -61,7 +61,7 @@ import useLang from '../../hooks/useLang'; import useHistoryBack from '../../hooks/useHistoryBack'; import usePrevious from '../../hooks/usePrevious'; import useForceUpdate from '../../hooks/useForceUpdate'; -import useOnChange from '../../hooks/useOnChange'; +import useSyncEffect from '../../hooks/useSyncEffect'; import useAppLayout from '../../hooks/useAppLayout'; import Transition from '../ui/Transition'; @@ -243,7 +243,7 @@ const MiddleColumn: FC = ({ : undefined; }, [chatId, openChat]); - useOnChange(() => { + useSyncEffect(() => { setDropAreaState(DropAreaState.None); setIsNotchShown(undefined); }, [chatId]); @@ -704,7 +704,7 @@ function useIsReady( } } - useOnChange(() => { + useSyncEffect(() => { if (!withAnimations) { setIsReady(true); } diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 30fdfc1c2..dc18fd36e 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -88,7 +88,7 @@ import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useLang from '../../../hooks/useLang'; import useSendMessageAction from '../../../hooks/useSendMessageAction'; import useInterval from '../../../hooks/useInterval'; -import useOnChange from '../../../hooks/useOnChange'; +import useSyncEffect from '../../../hooks/useSyncEffect'; import { useStateRef } from '../../../hooks/useStateRef'; import useVoiceRecording from './hooks/useVoiceRecording'; import useClipboardPaste from './hooks/useClipboardPaste'; @@ -338,7 +338,7 @@ const Composer: FC = ({ }, [chat, chatId, isReady, lastSyncTime, loadSendAs, sendAsPeerIds]); const shouldAnimateSendAsButtonRef = useRef(false); - useOnChange(([prevChatId, prevSendAsPeerIds]) => { + useSyncEffect(([prevChatId, prevSendAsPeerIds]) => { // We only animate send-as button if `sendAsPeerIds` was missing when opening the chat shouldAnimateSendAsButtonRef.current = Boolean(chatId === prevChatId && sendAsPeerIds && !prevSendAsPeerIds); }, [chatId, sendAsPeerIds]); diff --git a/src/components/middle/composer/MessageInput.tsx b/src/components/middle/composer/MessageInput.tsx index 2118e0a61..384bcd0fb 100644 --- a/src/components/middle/composer/MessageInput.tsx +++ b/src/components/middle/composer/MessageInput.tsx @@ -185,7 +185,7 @@ const MessageInput: FC = ({ if (prevHtml !== undefined && prevHtml !== html) { updateInputHeight(!html.length); } - }, [html]); + }, [html, updateInputHeight]); const chatIdRef = useRef(chatId); chatIdRef.current = chatId; diff --git a/src/components/middle/composer/WebPagePreview.tsx b/src/components/middle/composer/WebPagePreview.tsx index d945fa0dc..e69e36c85 100644 --- a/src/components/middle/composer/WebPagePreview.tsx +++ b/src/components/middle/composer/WebPagePreview.tsx @@ -9,7 +9,7 @@ import type { ISettings } from '../../../types'; import { RE_LINK_TEMPLATE } from '../../../config'; import { selectTabState, selectNoWebPage, selectTheme } from '../../../global/selectors'; import parseMessageInput from '../../../util/parseMessageInput'; -import useOnChange from '../../../hooks/useOnChange'; +import useSyncEffect from '../../../hooks/useSyncEffect'; import useShowTransition from '../../../hooks/useShowTransition'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useDebouncedMemo from '../../../hooks/useDebouncedMemo'; @@ -78,10 +78,10 @@ const WebPagePreview: FC = ({ } }, [chatId, toggleMessageWebPage, clearWebPagePreview, link, loadWebPagePreview, threadId]); - useOnChange(() => { + useSyncEffect(() => { clearWebPagePreview(); toggleMessageWebPage({ chatId, threadId }); - }, [chatId]); + }, [chatId, clearWebPagePreview, threadId, toggleMessageWebPage]); const isShown = Boolean(webPagePreview && messageText.length && !noWebPage && !disabled); const { shouldRender, transitionClassNames } = useShowTransition(isShown); diff --git a/src/components/middle/composer/hooks/useEditing.ts b/src/components/middle/composer/hooks/useEditing.ts index 3b1071486..b5ad9652e 100644 --- a/src/components/middle/composer/hooks/useEditing.ts +++ b/src/components/middle/composer/hooks/useEditing.ts @@ -56,6 +56,7 @@ const useEditing = ( focusEditableElement(messageInput, true); } }); + // eslint-disable-next-line react-hooks/exhaustive-deps -- `as const` not yet supported by linter }, [editedMessage, replyingToId, setHtml] as const); useEffect(() => { diff --git a/src/components/middle/hooks/useScrollHooks.ts b/src/components/middle/hooks/useScrollHooks.ts index f985f61a7..0115d848b 100644 --- a/src/components/middle/hooks/useScrollHooks.ts +++ b/src/components/middle/hooks/useScrollHooks.ts @@ -9,7 +9,7 @@ import { LOCAL_MESSAGE_MIN_ID, MESSAGE_LIST_SLICE } from '../../../config'; import { IS_SCROLL_PATCH_NEEDED, MESSAGE_LIST_SENSITIVE_AREA } from '../../../util/environment'; import { debounce } from '../../../util/schedulers'; import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver'; -import useOnChange from '../../../hooks/useOnChange'; +import useSyncEffect from '../../../hooks/useSyncEffect'; const FAB_THRESHOLD = 50; const NOTCH_THRESHOLD = 1; // Notch has zero height so we at least need a 1px margin to intersect @@ -136,14 +136,16 @@ export default function useScrollHooks( useOnIntersect(fabTriggerRef, observeIntersectionForNotch); - useOnChange(() => { + const toggleScrollToolsRef = useRef(); + toggleScrollToolsRef.current = toggleScrollTools; + useSyncEffect(() => { if (isReady) { - toggleScrollTools(); + toggleScrollToolsRef.current!(); } }, [isReady]); // Workaround for FAB and notch flickering with tall incoming message - useOnChange(() => { + useSyncEffect(() => { freezeForFab(); freezeForNotch(); @@ -151,7 +153,7 @@ export default function useScrollHooks( unfreezeForNotch(); unfreezeForFab(); }, TOOLS_FREEZE_TIMEOUT); - }, [messageIds]); + }, [freezeForFab, freezeForNotch, messageIds, unfreezeForFab, unfreezeForNotch]); return { backwardsTriggerRef, forwardsTriggerRef, fabTriggerRef }; } diff --git a/src/components/middle/message/Invoice.tsx b/src/components/middle/message/Invoice.tsx index d14a60933..2d4b11fef 100644 --- a/src/components/middle/message/Invoice.tsx +++ b/src/components/middle/message/Invoice.tsx @@ -65,7 +65,7 @@ const Invoice: FC = ({ contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, ''); }); } - }, [shouldAffectAppendix, photoUrl, isInSelectMode, isSelected, theme] as const); + }, [shouldAffectAppendix, photoUrl, isInSelectMode, isSelected, theme]); return (
= ({ contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, ''); }); } - }, [shouldRenderText, isOwn, isInSelectMode, isSelected, theme, mapBlobUrl] as const); + }, [shouldRenderText, isOwn, isInSelectMode, isSelected, theme, mapBlobUrl]); useEffect(() => { // Prevent map refetching for slight location changes diff --git a/src/components/middle/message/Photo.tsx b/src/components/middle/message/Photo.tsx index ec968dc80..21d4c9948 100644 --- a/src/components/middle/message/Photo.tsx +++ b/src/components/middle/message/Photo.tsx @@ -161,7 +161,7 @@ const Photo: FC = ({ } else { contentEl.classList.add('has-appendix-thumb'); } - }, [shouldAffectAppendix, fullMediaData, isOwn, isInSelectMode, isSelected, theme] as const); + }, [shouldAffectAppendix, fullMediaData, isOwn, isInSelectMode, isSelected, theme]); const { width, height, isSmall } = dimensions || calculateMediaDimensions(message, asForwarded, noAvatars, isMobile); diff --git a/src/components/right/hooks/useAsyncRendering.ts b/src/components/right/hooks/useAsyncRendering.ts index 7001bc71a..c62c1c132 100644 --- a/src/components/right/hooks/useAsyncRendering.ts +++ b/src/components/right/hooks/useAsyncRendering.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from '../../../lib/teact/teact'; -import useOnChange from '../../../hooks/useOnChange'; +import useSyncEffect from '../../../hooks/useSyncEffect'; import useForceUpdate from '../../../hooks/useForceUpdate'; export default function useAsyncRendering(dependencies: T, delay?: number) { @@ -9,7 +9,7 @@ export default function useAsyncRendering(dependencies: T, dela const timeoutRef = useRef(); const forceUpdate = useForceUpdate(); - useOnChange(() => { + useSyncEffect(() => { if (isDisabled) { return; } @@ -20,6 +20,7 @@ export default function useAsyncRendering(dependencies: T, dela clearTimeout(timeoutRef.current); timeoutRef.current = undefined; } + // eslint-disable-next-line react-hooks/exhaustive-deps }, dependencies); useEffect(() => { diff --git a/src/components/right/hooks/useProfileState.ts b/src/components/right/hooks/useProfileState.ts index f146680a5..7b6f17476 100644 --- a/src/components/right/hooks/useProfileState.ts +++ b/src/components/right/hooks/useProfileState.ts @@ -38,7 +38,7 @@ export default function useProfileState( }, PROGRAMMATIC_SCROLL_TIMEOUT_MS); } } - }, [tabType, isFirstTab, onProfileStateChange]); + }, [tabType, isFirstTab, onProfileStateChange, containerRef]); // Scroll to top useEffectWithPrevDeps(([prevProfileState]) => { @@ -70,7 +70,7 @@ export default function useProfileState( }, PROGRAMMATIC_SCROLL_TIMEOUT_MS); onProfileStateChange(profileState); - }, [profileState]); + }, [profileState, containerRef, onProfileStateChange]); const determineProfileState = useCallback(() => { const container = containerRef.current; diff --git a/src/components/right/hooks/useProfileViewportIds.ts b/src/components/right/hooks/useProfileViewportIds.ts index d4bb8ac91..bdaf484d6 100644 --- a/src/components/right/hooks/useProfileViewportIds.ts +++ b/src/components/right/hooks/useProfileViewportIds.ts @@ -7,7 +7,7 @@ import type { ProfileTabType, SharedMediaType } from '../../../types'; import { MEMBERS_SLICE, MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config'; import { getMessageContentIds, sortChatIds, sortUserIds } from '../../../global/helpers'; -import useOnChange from '../../../hooks/useOnChange'; +import useSyncEffect from '../../../hooks/useSyncEffect'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; export default function useProfileViewportIds( @@ -150,11 +150,11 @@ function useInfiniteScrollForSharedMedia( ) { const messageIdsRef = useRef(); - useOnChange(() => { + useSyncEffect(() => { messageIdsRef.current = undefined; }, [topicId]); - useOnChange(() => { + useSyncEffect(() => { if (currentResultType === forSharedMediaType && chatMessages && foundIds) { messageIdsRef.current = getMessageContentIds( chatMessages, diff --git a/src/components/right/management/ManageInvite.tsx b/src/components/right/management/ManageInvite.tsx index 0cc8ede29..509c438a2 100644 --- a/src/components/right/management/ManageInvite.tsx +++ b/src/components/right/management/ManageInvite.tsx @@ -18,7 +18,7 @@ import InputText from '../../ui/InputText'; import RadioGroup from '../../ui/RadioGroup'; import Button from '../../ui/Button'; import FloatingActionButton from '../../ui/FloatingActionButton'; -import useOnChange from '../../../hooks/useOnChange'; +import useSyncEffect from '../../../hooks/useSyncEffect'; import CalendarModal from '../../common/CalendarModal'; const DEFAULT_USAGE_LIMITS = [1, 10, 100]; @@ -64,7 +64,7 @@ const ManageInvite: FC = ({ onBack: onClose, }); - useOnChange(([oldEditingInvite]) => { + useSyncEffect(([oldEditingInvite]) => { if (oldEditingInvite === editingInvite) return; if (!editingInvite) { setTitle(''); diff --git a/src/components/ui/OptimizedVideo.tsx b/src/components/ui/OptimizedVideo.tsx index 729fe6591..8a648b5d4 100644 --- a/src/components/ui/OptimizedVideo.tsx +++ b/src/components/ui/OptimizedVideo.tsx @@ -3,7 +3,7 @@ import React, { memo, useCallback, useRef } from '../../lib/teact/teact'; import useVideoAutoPause from '../middle/message/hooks/useVideoAutoPause'; import useVideoCleanup from '../../hooks/useVideoCleanup'; import useBuffering from '../../hooks/useBuffering'; -import useOnChange from '../../hooks/useOnChange'; +import useSyncEffect from '../../hooks/useSyncEffect'; type OwnProps = { @@ -40,13 +40,13 @@ function OptimizedVideo({ // This is only needed for browsers not allowing autoplay const { isBuffered, bufferingHandlers } = useBuffering(true, onTimeUpdate); const { onPlaying: handlePlayingForBuffering, ...otherBufferingHandlers } = bufferingHandlers; - useOnChange(([prevIsBuffered]) => { + useSyncEffect(([prevIsBuffered]) => { if (prevIsBuffered === undefined) { return; } handleReady(); - }, [isBuffered]); + }, [isBuffered, handleReady]); const handlePlaying = useCallback((e) => { handlePlayingForAutoPause(); diff --git a/src/hooks/useAudioPlayer.ts b/src/hooks/useAudioPlayer.ts index 7ea18022d..6c439118f 100644 --- a/src/hooks/useAudioPlayer.ts +++ b/src/hooks/useAudioPlayer.ts @@ -14,7 +14,7 @@ import { import { selectTabState } from '../global/selectors'; import useEffectWithPrevDeps from './useEffectWithPrevDeps'; -import useOnChange from './useOnChange'; +import useSyncEffect from './useSyncEffect'; type Handler = (e: Event) => void; @@ -47,7 +47,7 @@ const useAudioPlayer = ( if (onTrackChange) onTrackChange(); }, [onTrackChange]); - useOnChange(() => { + useSyncEffect(() => { controllerRef.current = register(trackId, trackType, (eventName, e) => { switch (eventName) { case 'onPlay': { @@ -105,6 +105,8 @@ 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 isPlayingSync = true; } diff --git a/src/hooks/useBlurSync.ts b/src/hooks/useBlurSync.ts index b9c7ee571..856c0418d 100644 --- a/src/hooks/useBlurSync.ts +++ b/src/hooks/useBlurSync.ts @@ -1,7 +1,7 @@ import { useRef } from '../lib/teact/teact'; import fastBlur from '../lib/fastBlur'; -import useOnChange from './useOnChange'; +import useSyncEffect from './useSyncEffect'; import useBlur from './useBlur'; import { imgToCanvas } from '../util/files'; @@ -13,7 +13,8 @@ export default function useBlurSync(dataUri: string | false | undefined) { let isChanged = false; - useOnChange(() => { + useSyncEffect(() => { + // eslint-disable-next-line react-hooks/exhaustive-deps isChanged = true; blurredRef.current = undefined; diff --git a/src/hooks/useCanvasBlur.ts b/src/hooks/useCanvasBlur.ts index 5c0497e59..48875d180 100644 --- a/src/hooks/useCanvasBlur.ts +++ b/src/hooks/useCanvasBlur.ts @@ -2,7 +2,7 @@ import { useEffect, useRef } from '../lib/teact/teact'; import { IS_CANVAS_FILTER_SUPPORTED } from '../util/environment'; import fastBlur from '../lib/fastBlur'; -import useOnChange from './useOnChange'; +import useSyncEffect from './useSyncEffect'; const RADIUS = 2; const ITERATIONS = 2; @@ -19,7 +19,7 @@ export default function useCanvasBlur( const canvasRef = useRef(null); const isStarted = useRef(); - useOnChange(() => { + useSyncEffect(() => { if (!isDisabled) { isStarted.current = false; } diff --git a/src/hooks/useDebouncedMemo.ts b/src/hooks/useDebouncedMemo.ts index 1767840de..0d256b760 100644 --- a/src/hooks/useDebouncedMemo.ts +++ b/src/hooks/useDebouncedMemo.ts @@ -1,7 +1,7 @@ import { useCallback, useRef, useState } from '../lib/teact/teact'; import useRunDebounced from './useRunDebounced'; -import useOnChange from './useOnChange'; +import useSyncEffect from './useSyncEffect'; import useHeavyAnimationCheck, { isHeavyAnimating } from './useHeavyAnimationCheck'; import useForceUpdate from './useForceUpdate'; @@ -12,7 +12,7 @@ export default function useDebouncedMemo( const { isFrozen, updateWhenUnfrozen } = useHeavyAnimationFreeze(); const runDebounced = useRunDebounced(ms, true); - useOnChange(() => { + useSyncEffect(() => { if (isFrozen) { updateWhenUnfrozen(); return; @@ -21,6 +21,7 @@ export default function useDebouncedMemo( runDebounced(() => { setValue(resolverFn()); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [...dependencies, isFrozen]); return value; diff --git a/src/hooks/useEffectOnce.ts b/src/hooks/useEffectOnce.ts new file mode 100644 index 000000000..503300a14 --- /dev/null +++ b/src/hooks/useEffectOnce.ts @@ -0,0 +1,8 @@ +import { useEffect } from '../lib/teact/teact'; + +function useEffectOnce(effect: React.EffectCallback) { + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(effect, []); +} + +export default useEffectOnce; diff --git a/src/hooks/useForumPanelRender.ts b/src/hooks/useForumPanelRender.ts index bf0a739c7..71367be5a 100644 --- a/src/hooks/useForumPanelRender.ts +++ b/src/hooks/useForumPanelRender.ts @@ -1,13 +1,13 @@ import { useCallback, useRef } from '../lib/teact/teact'; import useForceUpdate from './useForceUpdate'; -import useOnChange from './useOnChange'; +import useSyncEffect from './useSyncEffect'; export default function useForumPanelRender(isForumPanelOpen = false) { const shouldRenderForumPanelRef = useRef(isForumPanelOpen); const forceUpdate = useForceUpdate(); - useOnChange(() => { + useSyncEffect(() => { if (isForumPanelOpen) { shouldRenderForumPanelRef.current = true; } diff --git a/src/hooks/useHistoryBack.ts b/src/hooks/useHistoryBack.ts index 381691ed5..083801da1 100644 --- a/src/hooks/useHistoryBack.ts +++ b/src/hooks/useHistoryBack.ts @@ -1,9 +1,12 @@ -import useOnChange from './useOnChange'; -import { useEffect, useRef } from '../lib/teact/teact'; +import { useCallback, useRef } from '../lib/teact/teact'; +import { getActions } from '../lib/teact/teactn'; + import { IS_TEST } from '../config'; import { fastRaf } from '../util/schedulers'; import { IS_IOS } from '../util/environment'; -import { getActions } from '../lib/teact/teactn'; + +import useSyncEffect from './useSyncEffect'; +import useEffectOnce from './useEffectOnce'; export const LOCATION_HASH = window.location.hash; const PATH_BASE = `${window.location.pathname}${window.location.search}`; @@ -241,7 +244,7 @@ export default function useHistoryBack({ const isFirstRender = useRef(true); - const pushState = (forceReplace = false) => { + const pushState = useCallback((forceReplace = false) => { // Check if the old state should be replaced const shouldReplace = forceReplace || historyState[historyCursor].shouldBeReplaced; indexRef.current = shouldReplace ? historyCursor : ++historyCursor; @@ -276,9 +279,9 @@ export default function useHistoryBack({ }, hash: hash ? `#${hash}` : undefined, }); - }; + }, [hash, onBack, shouldBeReplaced]); - const processBack = () => { + const processBack = useCallback(() => { // Only process back on open records if (indexRef.current && historyState[indexRef.current] && !wasReplaced.current) { historyState[indexRef.current].isClosed = true; @@ -287,19 +290,19 @@ export default function useHistoryBack({ historyCursor -= cleanupClosed(); } } - }; + }, [shouldBeReplaced]); // Process back navigation when element is unmounted - useEffect(() => { + useEffectOnce(() => { isFirstRender.current = false; return () => { if (!isActive || wasReplaced.current) return; processBack(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }); - useOnChange(() => { + useSyncEffect(([prevIsActive]) => { + if (prevIsActive === isActive) return; if (isFirstRender.current && !isActive) return; if (isActive) { @@ -307,5 +310,5 @@ export default function useHistoryBack({ } else { processBack(); } - }, [isActive]); + }, [isActive, processBack, pushState]); } diff --git a/src/hooks/useIntersectionObserver.ts b/src/hooks/useIntersectionObserver.ts index 629674a1c..b37c9ac40 100644 --- a/src/hooks/useIntersectionObserver.ts +++ b/src/hooks/useIntersectionObserver.ts @@ -4,6 +4,7 @@ import { } from '../lib/teact/teact'; import { throttle, debounce } from '../util/schedulers'; +import useEffectOnce from './useEffectOnce'; import useHeavyAnimationCheck from './useHeavyAnimationCheck'; type TargetCallback = (entry: IntersectionObserverEntry) => void; @@ -155,11 +156,10 @@ export function useIntersectionObserver({ export function useOnIntersect( targetRef: RefObject, observe?: ObserveFn, callback?: TargetCallback, ) { - useEffect(() => { + useEffectOnce(() => { return observe ? observe(targetRef.current!, callback) : undefined; // Arguments should never change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }); } export function useIsIntersecting( diff --git a/src/hooks/useLang.ts b/src/hooks/useLang.ts index 6ccfc8e69..5c5866b42 100644 --- a/src/hooks/useLang.ts +++ b/src/hooks/useLang.ts @@ -1,13 +1,13 @@ import * as langProvider from '../util/langProvider'; import useForceUpdate from './useForceUpdate'; -import useOnChange from './useOnChange'; +import useSyncEffect from './useSyncEffect'; export type LangFn = langProvider.LangFn; const useLang = (): LangFn => { const forceUpdate = useForceUpdate(); - useOnChange(() => { + useSyncEffect(() => { return langProvider.addCallback(forceUpdate); }, [forceUpdate]); diff --git a/src/hooks/usePrevDuringAnimation.ts b/src/hooks/usePrevDuringAnimation.ts index a01016242..c9ac6c3ab 100644 --- a/src/hooks/usePrevDuringAnimation.ts +++ b/src/hooks/usePrevDuringAnimation.ts @@ -2,7 +2,7 @@ import { useRef } from '../lib/teact/teact'; import usePrevious from './usePrevious'; import useForceUpdate from './useForceUpdate'; -import useOnChange from './useOnChange'; +import useSyncEffect from './useSyncEffect'; export default function usePrevDuringAnimation(current: any, duration?: number) { const prev = usePrevious(current, true); @@ -18,7 +18,7 @@ export default function usePrevDuringAnimation(current: any, duration?: number) timeoutRef.current = undefined; } - useOnChange(() => { + useSyncEffect(() => { // When `current` becomes empty if (duration && !isCurrentPresent && isPrevPresent && !timeoutRef.current) { timeoutRef.current = window.setTimeout(() => { @@ -26,7 +26,7 @@ export default function usePrevDuringAnimation(current: any, duration?: number) forceUpdate(); }, duration); } - }, [current]); + }, [duration, forceUpdate, isCurrentPresent, isPrevPresent]); return !timeoutRef.current || !duration || isCurrentPresent ? current : prev; } diff --git a/src/hooks/useStateRef.ts b/src/hooks/useStateRef.ts index 5b4247624..efc4ef807 100644 --- a/src/hooks/useStateRef.ts +++ b/src/hooks/useStateRef.ts @@ -1,13 +1,13 @@ import { useRef } from '../lib/teact/teact'; -import useOnChange from './useOnChange'; +import useSyncEffect from './useSyncEffect'; // Allows to use state value as "silent" dependency in hooks (not causing updates). // Useful for state values that update frequently (such as controlled input value). export function useStateRef(value: T) { const ref = useRef(value); - useOnChange(() => { + useSyncEffect(() => { ref.current = value; }, [value]); diff --git a/src/hooks/useOnChange.ts b/src/hooks/useSyncEffect.ts similarity index 58% rename from src/hooks/useOnChange.ts rename to src/hooks/useSyncEffect.ts index ca1f171c1..3f8e83670 100644 --- a/src/hooks/useOnChange.ts +++ b/src/hooks/useSyncEffect.ts @@ -1,10 +1,10 @@ import usePrevious from './usePrevious'; -const useOnChange = (cb: (args: T | readonly []) => void, dependencies: T) => { +const useSyncEffect = (cb: (args: T | readonly []) => void, dependencies: T) => { const prevDeps = usePrevious(dependencies); if (!prevDeps || dependencies.some((d, i) => d !== prevDeps[i])) { cb(prevDeps || []); } }; -export default useOnChange; +export default useSyncEffect; diff --git a/src/hooks/useWindowSize.ts b/src/hooks/useWindowSize.ts index ab9e15c0a..993f8ffc6 100644 --- a/src/hooks/useWindowSize.ts +++ b/src/hooks/useWindowSize.ts @@ -10,7 +10,7 @@ const THROTTLE = 250; const useWindowSize = () => { const [size, setSize] = useState(windowSize.get()); const [isResizing, setIsResizing] = useState(false); - const setIsResizingDebounced = useDebouncedCallback(setIsResizing, [], THROTTLE, true); + const setIsResizingDebounced = useDebouncedCallback(setIsResizing, [setIsResizing], THROTTLE, true); const result = useMemo(() => ({ ...size, isResizing }), [isResizing, size]);