fix: stable navigate fn

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-06-19 21:26:17 +08:00
parent a37628e029
commit 55a4cebee3
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
9 changed files with 93 additions and 83 deletions

View File

@ -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<Params<string>>
@ -17,9 +17,22 @@ export const [routeAtom, , , , getReadonlyRoute, setRoute] = createAtomHooks(
}),
)
const noop = []
export const useReadonlyRouteSelector = <T>(
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,
}

View File

@ -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])
}

View File

@ -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()}`,
)
}
}, [])

View File

@ -47,7 +47,7 @@ export const useRouteParms = () => {
category: search.get("category") || undefined,
}
}
const noop = [] as any[]
export const useRouteParamsSelector = <T>(
selector: (params: {
entryId: string | undefined
@ -56,28 +56,30 @@ export const useRouteParamsSelector = <T>(
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)

View File

@ -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({
</Collapsible>
)
}
export const FeedCategory = memo(FeedCategoryImpl)

View File

@ -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<HTMLDivElement> = useCallback(
(e) => {
@ -121,8 +119,7 @@ export function FeedItem({
<div
className={cn(
"flex w-full items-center justify-between rounded-md py-[2px] pr-2.5 text-sm font-medium leading-loose",
isActive &&
"bg-native-active",
isActive && "bg-native-active",
className,
)}
onClick={handleNavigate}
@ -238,3 +235,5 @@ export function FeedItem({
</div>
)
}
export const FeedItem = memo(FeedItemImpl)

View File

@ -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
}

View File

@ -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<PropsWithChildren> = ({ children }) => (
<Provider store={jotaiStore}>
<ModalStackProvider />
<HelmetProvider>{children}</HelmetProvider>
<RouterParamsProvider />
<BizRouterProvider />
</Provider>
</TooltipProvider>
</PersistQueryClientProvider>

View File

@ -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
}