From 55a4cebee38f6495fe680f94e653502d6917c19d Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 19 Jun 2024 21:26:17 +0800 Subject: [PATCH] fix: stable navigate fn Signed-off-by: Innei --- src/renderer/src/atoms/route.ts | 17 +++++- src/renderer/src/hooks/biz/useAsRead.ts | 10 ++-- .../src/hooks/biz/useNavigateEntry.ts | 53 ++++++++----------- src/renderer/src/hooks/biz/useRouteParams.ts | 38 ++++++------- .../src/modules/feed-column/category.tsx | 6 ++- src/renderer/src/modules/feed-column/item.tsx | 15 +++--- .../src/providers/biz-router-provider.tsx | 17 ++++++ src/renderer/src/providers/root-providers.tsx | 4 +- .../src/providers/router-prams-provider.tsx | 16 ------ 9 files changed, 93 insertions(+), 83 deletions(-) create mode 100644 src/renderer/src/providers/biz-router-provider.tsx delete mode 100644 src/renderer/src/providers/router-prams-provider.tsx diff --git a/src/renderer/src/atoms/route.ts b/src/renderer/src/atoms/route.ts index c5014f8dc..868b49268 100644 --- a/src/renderer/src/atoms/route.ts +++ b/src/renderer/src/atoms/route.ts @@ -3,7 +3,7 @@ import { createAtomHooks } from "@renderer/lib/jotai" import { atom, useAtomValue } from "jotai" import { selectAtom } from "jotai/utils" import { useMemo } from "react" -import type { Params } from "react-router-dom" +import type { NavigateFunction, Params } from "react-router-dom" interface RouteAtom { params: Readonly> @@ -17,9 +17,22 @@ export const [routeAtom, , , , getReadonlyRoute, setRoute] = createAtomHooks( }), ) +const noop = [] export const useReadonlyRouteSelector = ( selector: (route: RouteAtom) => T, + deps: any[] = noop, ): T => useAtomValue( - useMemo(() => selectAtom(routeAtom, (route) => selector(route)), []), + useMemo(() => selectAtom(routeAtom, (route) => selector(route)), deps), ) + +// VITE HMR will create new router instance, but RouterProvider always stable + +const [, , , , navigate, setNavigate] = createAtomHooks( + atom<{ fn: NavigateFunction | null }>({ fn() {} }), +) +const getStableRouterNavigate = () => navigate().fn +export { + getStableRouterNavigate, + setNavigate, +} diff --git a/src/renderer/src/hooks/biz/useAsRead.ts b/src/renderer/src/hooks/biz/useAsRead.ts index 713ec9f10..0e684331b 100644 --- a/src/renderer/src/hooks/biz/useAsRead.ts +++ b/src/renderer/src/hooks/biz/useAsRead.ts @@ -1,11 +1,11 @@ import { FEED_COLLECTION_LIST, levels } from "@renderer/lib/constants" import type { EntryModel } from "@renderer/models" -import { useRouteParms } from "./useRouteParams" +import { useRouteParamsSelector } from "./useRouteParams" export function useAsRead(entry?: EntryModel) { - const { feedId, level } = useRouteParms() - - if (!entry) return false - return entry.read && !(level === levels.folder && feedId === FEED_COLLECTION_LIST) + return useRouteParamsSelector(({ feedId, level }) => { + if (!entry) return false + return entry.read && !(level === levels.folder && feedId === FEED_COLLECTION_LIST) + }, [entry?.read]) } diff --git a/src/renderer/src/hooks/biz/useNavigateEntry.ts b/src/renderer/src/hooks/biz/useNavigateEntry.ts index d1dedafe9..d4f56aeb6 100644 --- a/src/renderer/src/hooks/biz/useNavigateEntry.ts +++ b/src/renderer/src/hooks/biz/useNavigateEntry.ts @@ -1,10 +1,9 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ -import { getReadonlyRoute } from "@renderer/atoms" +import { getReadonlyRoute, getStableRouterNavigate } from "@renderer/atoms" import { ROUTE_FEED_PENDING } from "@renderer/lib/constants" import type { FeedViewType } from "@renderer/lib/enum" import { isUndefined } from "lodash-es" import { useCallback } from "react" -import { useNavigate } from "react-router-dom" type NavigateEntryOptions = Partial<{ feedId: string | null @@ -17,37 +16,31 @@ type NavigateEntryOptions = Partial<{ /** * @description a hook to navigate to `feedId`, `entryId`, add search for `view`, `level` */ -export const useNavigateEntry = () => { - const navigate = useNavigate() - return useCallback( - (options: NavigateEntryOptions) => { - const { entryId, feedId, level, view, category } = options || {} - const { params, searchParams } = getReadonlyRoute() - let finalFeedId = feedId || params.feedId || ROUTE_FEED_PENDING +export const useNavigateEntry = () => useCallback((options: NavigateEntryOptions) => { + const { entryId, feedId, level, view, category } = options || {} + const { params, searchParams } = getReadonlyRoute() + let finalFeedId = feedId || params.feedId || ROUTE_FEED_PENDING - if ("feedId" in options && feedId === null) { - finalFeedId = ROUTE_FEED_PENDING - } + if ("feedId" in options && feedId === null) { + finalFeedId = ROUTE_FEED_PENDING + } - const nextSearchParams = new URLSearchParams(searchParams) + const nextSearchParams = new URLSearchParams(searchParams) - !isUndefined(view) && nextSearchParams.set("view", view.toString()) - level && nextSearchParams.set("level", level.toString()) + !isUndefined(view) && nextSearchParams.set("view", view.toString()) + level && nextSearchParams.set("level", level.toString()) - if ("category" in options) { - if (!category) { - nextSearchParams.delete("category") - } else { - nextSearchParams.set("category", category.toString()) - } - } + if ("category" in options) { + if (!category) { + nextSearchParams.delete("category") + } else { + nextSearchParams.set("category", category.toString()) + } + } - return navigate( - `/feeds/${finalFeedId}/${ - entryId || ROUTE_FEED_PENDING - }?${nextSearchParams.toString()}`, - ) - }, - [navigate], + return getStableRouterNavigate()?.( + `/feeds/${finalFeedId}/${ + entryId || ROUTE_FEED_PENDING + }?${nextSearchParams.toString()}`, ) -} +}, []) diff --git a/src/renderer/src/hooks/biz/useRouteParams.ts b/src/renderer/src/hooks/biz/useRouteParams.ts index fb953632b..e5b3b0b7c 100644 --- a/src/renderer/src/hooks/biz/useRouteParams.ts +++ b/src/renderer/src/hooks/biz/useRouteParams.ts @@ -47,7 +47,7 @@ export const useRouteParms = () => { category: search.get("category") || undefined, } } - +const noop = [] as any[] export const useRouteParamsSelector = ( selector: (params: { entryId: string | undefined @@ -56,28 +56,30 @@ export const useRouteParamsSelector = ( category: string | undefined view: FeedViewType }) => T, -): T => useReadonlyRouteSelector((route) => { - const { searchParams, params } = route + deps = noop, +): T => + useReadonlyRouteSelector((route) => { + const { searchParams, params } = route - let feedId: string | number = params.feedId! + let feedId: string | number = params.feedId! - // If feedId is a number, it's a FeedViewType - if (feedId && FeedViewTypeValues.includes(feedId as string)) { - feedId = Number.parseInt(feedId as string) - } + // If feedId is a number, it's a FeedViewType + if (feedId && FeedViewTypeValues.includes(feedId as string)) { + feedId = Number.parseInt(feedId as string) + } - const view = searchParams.get("view") + const view = searchParams.get("view") - const finalView = + const finalView = (view && FeedViewTypeValues.includes(view) ? +view : FeedViewType.Articles) || FeedViewType.Articles - return selector({ - entryId: params.entryId || undefined, - feedId: params.feedId || undefined, - level: searchParams.get("level") || undefined, - category: searchParams.get("category") || undefined, - view: finalView, - }) - }) + return selector({ + entryId: params.entryId || undefined, + feedId: params.feedId || undefined, + level: searchParams.get("level") || undefined, + category: searchParams.get("category") || undefined, + view: finalView, + }) + }, deps) diff --git a/src/renderer/src/modules/feed-column/category.tsx b/src/renderer/src/modules/feed-column/category.tsx index 9247eddee..e2ef0dabc 100644 --- a/src/renderer/src/modules/feed-column/category.tsx +++ b/src/renderer/src/modules/feed-column/category.tsx @@ -11,14 +11,14 @@ import { cn } from "@renderer/lib/utils" import type { FeedListModel } from "@renderer/models" import { useUnreadStore } from "@renderer/store" import { AnimatePresence, m } from "framer-motion" -import { useEffect, useState } from "react" +import { memo, useEffect, useState } from "react" import { useModalStack } from "../../components/ui/modal/stacked/hooks" import { CategoryRemoveDialogContent } from "./category-remove-dialog" import { CategoryRenameContent } from "./category-rename-dialog" import { FeedItem } from "./item" -export function FeedCategory({ +function FeedCategoryImpl({ data, view, expansion, @@ -166,3 +166,5 @@ export function FeedCategory({ ) } + +export const FeedCategory = memo(FeedCategoryImpl) diff --git a/src/renderer/src/modules/feed-column/item.tsx b/src/renderer/src/modules/feed-column/item.tsx index 6aac6b3b4..7038d55c1 100644 --- a/src/renderer/src/modules/feed-column/item.tsx +++ b/src/renderer/src/modules/feed-column/item.tsx @@ -9,9 +9,7 @@ import { TooltipTrigger, } from "@renderer/components/ui/tooltip" import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry" -import { - useRouteParamsSelector, -} from "@renderer/hooks/biz/useRouteParams" +import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams" import { apiClient } from "@renderer/lib/api-fetch" import { levels } from "@renderer/lib/constants" import dayjs from "@renderer/lib/dayjs" @@ -21,13 +19,13 @@ import { Queries } from "@renderer/queries" import type { SubscriptionPlainModel } from "@renderer/store" import { getFeedById, useFeedById, useUnreadStore } from "@renderer/store" import { useMutation } from "@tanstack/react-query" -import { useCallback } from "react" +import { memo, useCallback } from "react" import { toast } from "sonner" import { FeedForm } from "../discover/feed-form" type FeedItemData = SubscriptionPlainModel -export function FeedItem({ +const FeedItemImpl = ({ subscription, view, className, @@ -35,7 +33,7 @@ export function FeedItem({ subscription: FeedItemData view?: number className?: string -}) { +}) => { const navigate = useNavigateEntry() const handleNavigate: React.MouseEventHandler = useCallback( (e) => { @@ -121,8 +119,7 @@ export function FeedItem({
) } + +export const FeedItem = memo(FeedItemImpl) diff --git a/src/renderer/src/providers/biz-router-provider.tsx b/src/renderer/src/providers/biz-router-provider.tsx new file mode 100644 index 000000000..68ef8b612 --- /dev/null +++ b/src/renderer/src/providers/biz-router-provider.tsx @@ -0,0 +1,17 @@ +import { setNavigate, setRoute } from "@renderer/atoms" +import { useLayoutEffect } from "react" +import { useNavigate, useParams, useSearchParams } from "react-router-dom" + +export const BizRouterProvider = () => { + const [searchParams] = useSearchParams() + const params = useParams() + const nav = useNavigate() + useLayoutEffect(() => { + setRoute({ + params, + searchParams, + }) + setNavigate({ fn: nav }) + }, [searchParams, params, nav]) + return null +} diff --git a/src/renderer/src/providers/root-providers.tsx b/src/renderer/src/providers/root-providers.tsx index e7cf517ca..4a5d362f8 100644 --- a/src/renderer/src/providers/root-providers.tsx +++ b/src/renderer/src/providers/root-providers.tsx @@ -9,7 +9,7 @@ import { Provider } from "jotai" import type { FC, PropsWithChildren } from "react" import { HelmetProvider } from "react-helmet-async" -import { RouterParamsProvider } from "./router-prams-provider" +import { BizRouterProvider } from "./biz-router-provider" const loadFeatures = () => import("../framer-lazy-feature").then((res) => res.default) @@ -30,7 +30,7 @@ export const RootProviders: FC = ({ children }) => ( {children} - + diff --git a/src/renderer/src/providers/router-prams-provider.tsx b/src/renderer/src/providers/router-prams-provider.tsx deleted file mode 100644 index f17c22c78..000000000 --- a/src/renderer/src/providers/router-prams-provider.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { setRoute } from "@renderer/atoms" -import { useLayoutEffect } from "react" -import { useParams, useSearchParams } from "react-router-dom" - -export const RouterParamsProvider = () => { - const [searchParams] = useSearchParams() - const params = useParams() - - useLayoutEffect(() => { - setRoute({ - params, - searchParams, - }) - }, [searchParams, params]) - return null -}