fix: stable navigate fn
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
a37628e029
commit
55a4cebee3
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()}`,
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue