Skip to content

Commit

Permalink
fix: stable navigate fn
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Jun 19, 2024
1 parent a37628e commit 55a4ceb
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 83 deletions.
17 changes: 15 additions & 2 deletions src/renderer/src/atoms/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>>
Expand All @@ -17,9 +17,22 @@ export const [routeAtom, , , , getReadonlyRoute, setRoute] = createAtomHooks(
}),
)

const noop = []
export const useReadonlyRouteSelector = <T>(
selector: (route: RouteAtom) => T,
deps: any[] = noop,

Check warning on line 23 in src/renderer/src/atoms/route.ts

View workflow job for this annotation

GitHub Actions / Lint and Typecheck (18.x)

Unexpected any. Specify a different type
): T =>
useAtomValue(
useMemo(() => selectAtom(routeAtom, (route) => selector(route)), []),
useMemo(() => selectAtom(routeAtom, (route) => selector(route)), deps),

Check warning on line 26 in src/renderer/src/atoms/route.ts

View workflow job for this annotation

GitHub Actions / Lint and Typecheck (18.x)

Expected the dependency list for useMemo to be an array literal

Check warning on line 26 in src/renderer/src/atoms/route.ts

View workflow job for this annotation

GitHub Actions / Lint and Typecheck (18.x)

React Hook useMemo was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies

Check warning on line 26 in src/renderer/src/atoms/route.ts

View workflow job for this annotation

GitHub Actions / Lint and Typecheck (18.x)

React Hook useMemo has a missing dependency: 'selector'. Either include it or remove the dependency array. If 'selector' changes too often, find the parent component that defines it and wrap that definition in useCallback
)

// 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,
}
10 changes: 5 additions & 5 deletions src/renderer/src/hooks/biz/useAsRead.ts
Original file line number Diff line number Diff line change
@@ -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])
}
53 changes: 23 additions & 30 deletions src/renderer/src/hooks/biz/useNavigateEntry.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()}`,
)
}
}, [])
38 changes: 20 additions & 18 deletions src/renderer/src/hooks/biz/useRouteParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
6 changes: 4 additions & 2 deletions src/renderer/src/modules/feed-column/category.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -166,3 +166,5 @@ export function FeedCategory({
</Collapsible>
)
}

export const FeedCategory = memo(FeedCategoryImpl)
15 changes: 7 additions & 8 deletions src/renderer/src/modules/feed-column/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -21,21 +19,21 @@ 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,
}: {
subscription: FeedItemData
view?: number
className?: string
}) {
}) => {
const navigate = useNavigateEntry()
const handleNavigate: React.MouseEventHandler<HTMLDivElement> = useCallback(
(e) => {
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -238,3 +235,5 @@ export function FeedItem({
</div>
)
}

export const FeedItem = memo(FeedItemImpl)
17 changes: 17 additions & 0 deletions src/renderer/src/providers/biz-router-provider.tsx
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions src/renderer/src/providers/root-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -30,7 +30,7 @@ export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
<Provider store={jotaiStore}>
<ModalStackProvider />
<HelmetProvider>{children}</HelmetProvider>
<RouterParamsProvider />
<BizRouterProvider />
</Provider>
</TooltipProvider>
</PersistQueryClientProvider>
Expand Down
16 changes: 0 additions & 16 deletions src/renderer/src/providers/router-prams-provider.tsx

This file was deleted.

0 comments on commit 55a4ceb

Please sign in to comment.