diff --git a/src/app.tsx b/src/app.tsx index 7d791fb9..45d5571c 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -6,7 +6,7 @@ import { useLingui } from '@lingui/react'; import debounce from 'just-debounce-it'; import type { ReactElement } from 'react'; import { lazy, memo, Suspense } from 'react'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useEffectEvent, useMemo, useRef, useState } from 'react'; import { matchPath, Navigate, @@ -932,7 +932,7 @@ function SecondaryRoutes() { // Persist prevLocation to sessionStorage while on a status/post page so it // survives a page reload. Clear it when navigating away. - useEffect(() => { + const syncPrevLocation = useEffectEvent(() => { if (isModalPage) { if (states.prevLocation) { store.session.setJSON('prevLocation', { @@ -946,6 +946,9 @@ function SecondaryRoutes() { } store.session.del('prevLocation'); } + }); + useEffect(() => { + syncPrevLocation(); }, [isModalPage]); if (isModalPage) { diff --git a/src/components/compose.tsx b/src/components/compose.tsx index be2b93e5..8090fce8 100644 --- a/src/components/compose.tsx +++ b/src/components/compose.tsx @@ -6,7 +6,14 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { MenuDivider, MenuItem } from '@szhsin/react-menu'; import { deepEqual } from 'fast-equals'; import type { RefObject, SyntheticEvent } from 'react'; -import { useEffect, useId, useMemo, useRef, useState } from 'react'; +import { + useEffect, + useEffectEvent, + useId, + useMemo, + useRef, + useState, +} from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { uid } from 'uid/single'; import { useSnapshot } from 'valtio'; @@ -1080,13 +1087,10 @@ function Compose({ // every render. }, [draftStatus, editStatus, replyToStatus, replyMode]); - // Latest-value ref so the sharedData effect can dispatch the current - // processFiles without re-running on every render (the function is - // recreated each render but its observable behavior is stable). const processFilesRef = useRef(processFiles); processFilesRef.current = processFiles; - useEffect(() => { + const applySharedData = useEffectEvent(() => { if (sharedData) { const { initialText, files } = sharedData; @@ -1098,7 +1102,7 @@ function Compose({ if (files && files.length > 0) { void (async () => { try { - const mediaFiles = await processFilesRef.current(files); + const mediaFiles = await processFiles(files); if (mediaFiles) { setMediaAttachments(mediaFiles); } @@ -1108,6 +1112,9 @@ function Compose({ })(); } } + }); + useEffect(() => { + applySharedData(); }, [sharedData]); // focus textarea when state.composerState.minimized turns false diff --git a/src/components/related-actions.tsx b/src/components/related-actions.tsx index 6b36ba22..362e4e3e 100644 --- a/src/components/related-actions.tsx +++ b/src/components/related-actions.tsx @@ -5,7 +5,7 @@ import { MenuDivider, MenuItem } from '@szhsin/react-menu'; import type { mastodon } from 'masto'; import { toUnicode as punycodeToUnicode } from 'punycode/'; import type { HTMLAttributes, ReactElement } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useEffectEvent, useRef, useState } from 'react'; import { api } from '../utils/api'; import i18nDuration from '../utils/i18n-duration'; @@ -244,7 +244,7 @@ function RelatedActions({ }; }, [currentMasto, currentAuthenticated, sameInstance, id, instance]); - useEffect(() => { + const loadRelationshipForInfo = useEffectEvent(() => { if (info) { const { currentMasto: ctxMasto, @@ -319,6 +319,9 @@ function RelatedActions({ } })(); } + }); + useEffect(() => { + loadRelationshipForInfo(); }, [info, authenticated]); useEffect(() => { diff --git a/src/components/translation-block.tsx b/src/components/translation-block.tsx index 1bc24740..d7d63d4a 100644 --- a/src/components/translation-block.tsx +++ b/src/components/translation-block.tsx @@ -4,7 +4,7 @@ import { Trans, useLingui } from '@lingui/react/macro'; import PQueue from 'p-queue'; import pRetry from 'p-retry'; import type { ReactNode } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useEffectEvent, useRef, useState } from 'react'; import languages from '../data/translang-languages.json'; import { @@ -275,12 +275,13 @@ function TranslationBlock({ } }; - const translateRef = useRef(translate); - translateRef.current = translate; - useEffect(() => { + const runForcedTranslate = useEffectEvent(() => { if (forceTranslate) { - void translateRef.current(); + void translate(); } + }); + useEffect(() => { + runForcedTranslate(); }, [forceTranslate]); useEffect(() => { diff --git a/src/pages/catchup.tsx b/src/pages/catchup.tsx index 11ec2d9d..35945875 100644 --- a/src/pages/catchup.tsx +++ b/src/pages/catchup.tsx @@ -12,6 +12,7 @@ import { memo } from 'react'; import { useCallback, useEffect, + useEffectEvent, useLayoutEffect, useMemo, useRef, @@ -489,7 +490,7 @@ function Catchup() { [dtf, fetchHome, NS, setSearchParams], ); - useEffect(() => { + const syncRouteCatchup = useEffectEvent(() => { if (id) { void (async () => { const catchup = (await db.catchup.get(id)) as CatchupRecord | undefined; @@ -503,6 +504,9 @@ function Catchup() { setPosts([]); setUIState('start'); } + }); + useEffect(() => { + syncRouteCatchup(); }, [id, uiState]); const [reloadCatchupsCount, setReloadCatchupsCount] = useState(0); @@ -1059,7 +1063,7 @@ function Catchup() { t, ]); - useEffect(() => { + const scrollSelectedAuthorIntoView = useEffectEvent(() => { if (selectedAuthor) { if (authors[selectedAuthor]) { // Check if author is visible and within the scrollable area viewport @@ -1095,6 +1099,9 @@ function Catchup() { } } } + }); + useEffect(() => { + scrollSelectedAuthorIntoView(); }, [selectedAuthor, authors]); const [showHelp, setShowHelp] = useState(false); diff --git a/src/pages/notifications.tsx b/src/pages/notifications.tsx index 228243da..737ce713 100644 --- a/src/pages/notifications.tsx +++ b/src/pages/notifications.tsx @@ -6,7 +6,14 @@ import { Plural, Trans, useLingui } from '@lingui/react/macro'; import type { ComponentType, SyntheticEvent, ReactNode } from 'react'; import { Fragment } from 'react'; import { memo } from 'react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + useCallback, + useEffect, + useEffectEvent, + useMemo, + useRef, + useState, +} from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { InView as InViewUntyped } from 'react-intersection-observer'; import { useSearchParams } from 'react-router-dom'; @@ -589,10 +596,13 @@ function Notifications({ columnMode }: NotificationsProps) { useEffect(() => { loadNotificationsRef.current(true); }, []); - useEffect(() => { + const loadReachStartNotifications = useEffectEvent(() => { if (reachStart) { loadNotificationsRef.current(true); } + }); + useEffect(() => { + loadReachStartNotifications(); }, [reachStart]); // useEffect(() => { @@ -677,13 +687,16 @@ function Notifications({ columnMode }: NotificationsProps) { const announcementsListRef = useRef(null); - useEffect(() => { + const syncRouteNotification = useEffectEvent(() => { if (notificationID) { states.routeNotification = { id: notificationID, accessToken: atob(notificationAccessToken as string), }; } + }); + useEffect(() => { + syncRouteNotification(); }, [notificationID, notificationAccessToken]); // useEffect(() => {