feat: refetch latest data

This commit is contained in:
Ou 2024-09-30 01:51:28 +08:00
parent 26c1c7cf92
commit 7bd0b762b5
6 changed files with 79 additions and 21 deletions

View File

@ -1,14 +1,28 @@
import { atom } from "jotai" import { atom } from "jotai"
import type { SectionID, SourceID } from "@shared/types" import type { SectionID, SourceID } from "@shared/types"
import { metadata, sourceList } from "@shared/data" import { metadata, sourceList } from "@shared/data"
import { atomWithLocalStorage } from "./utils/atom" import { atomWithLocalStorage } from "./hooks/atomWithLocalStorage"
export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => { export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => {
return stored.filter(item => item in sourceList) return stored.filter(item => item in sourceList)
}) })
const currentSectionIDAtom = atom<SectionID>("focus") function initRefetchSource() {
let time = 0
// useOnReload
// 没有放在 useOnReload 里面, 可以避免初始化后再修改 refetchSourceAtom导致多次请求 API
const _ = localStorage.getItem("quitTime")
const now = Date.now()
const quitTime = _ ? Number(_) : 0
if (!Number.isNaN(quitTime) && now - quitTime < 1000) {
time = now
}
return Object.fromEntries(Object.keys(sourceList).map(k => [k, time])) as Record<SourceID, number>
}
export const refetchSourceAtom = atom(initRefetchSource())
const currentSectionIDAtom = atom<SectionID>("focus")
export const currentSectionAtom = atom((get) => { export const currentSectionAtom = atom((get) => {
const id = get(currentSectionIDAtom) const id = get(currentSectionIDAtom)
if (id === "focus") { if (id === "focus") {

View File

@ -1,11 +1,9 @@
import { Link } from "@tanstack/react-router" import { Link } from "@tanstack/react-router"
import { useCallback } from "react" import { useCallback } from "react"
import { useAtomValue } from "jotai" import { useAtomValue, useSetAtom } from "jotai"
import type { SourceID } from "@shared/types"
import { queryClient } from "~/main"
import logo from "~/assets/react.svg" import logo from "~/assets/react.svg"
import { useDark } from "~/hooks/useDark" import { useDark } from "~/hooks/useDark"
import { currentSectionAtom } from "~/atoms" import { currentSectionAtom, refetchSourceAtom } from "~/atoms"
function ThemeToggle() { function ThemeToggle() {
const { toggleDark } = useDark() const { toggleDark } = useDark()
@ -21,13 +19,14 @@ function ThemeToggle() {
function RefreshButton() { function RefreshButton() {
const currentSection = useAtomValue(currentSectionAtom) const currentSection = useAtomValue(currentSectionAtom)
const refreshAll = useCallback(async () => { const setRefetchSource = useSetAtom(refetchSourceAtom)
await queryClient.refetchQueries({ const refreshAll = useCallback(() => {
predicate(query) { const obj = Object.fromEntries(currentSection.sourceList.map(id => [id, Date.now()]))
return currentSection.sourceList.includes(query.queryKey[0] as SourceID) setRefetchSource(prev => ({
}, ...prev,
}) ...obj,
}, [currentSection]) }))
}, [currentSection, setRefetchSource])
return ( return (
<button type="button" className="i-ph:arrow-clockwise btn-pure" onClick={refreshAll} /> <button type="button" className="i-ph:arrow-clockwise btn-pure" onClick={refreshAll} />

View File

@ -5,7 +5,7 @@ import clsx from "clsx"
import { useInView } from "react-intersection-observer" import { useInView } from "react-intersection-observer"
import { useAtom, useAtomValue } from "jotai" import { useAtom, useAtomValue } from "jotai"
import { useCallback } from "react" import { useCallback } from "react"
import { currentSectionAtom, focusSourcesAtom } from "~/atoms" import { currentSectionAtom, focusSourcesAtom, refetchSourceAtom } from "~/atoms"
export function Main() { export function Main() {
const currentSection = useAtomValue(currentSectionAtom) const currentSection = useAtomValue(currentSectionAtom)
@ -40,15 +40,32 @@ function NewsCard({ id, inView }: { id: SourceID, inView: boolean }) {
const addFocusList = useCallback(() => { const addFocusList = useCallback(() => {
setFocusSources(focusSources.includes(id) ? focusSources.filter(i => i !== id) : [...focusSources, id]) setFocusSources(focusSources.includes(id) ? focusSources.filter(i => i !== id) : [...focusSources, id])
}, [setFocusSources, focusSources, id]) }, [setFocusSources, focusSources, id])
const { isPending, error, isFetching, data, refetch } = useQuery({ const [refetchSource, setRefetchSource] = useAtom(refetchSourceAtom)
queryKey: [id], const { isPending, error, isFetching, data } = useQuery({
queryFn: async () => { queryKey: [id, refetchSource[id]],
const response = await fetch(`/api/${id}?latest`) queryFn: async ({ queryKey }) => {
const [_id, _refetchTime] = queryKey as [SourceID, number]
let url = `/api/${_id}`
if (Date.now() - _refetchTime < 1000) {
url = `/api/${_id}?latest`
}
const response = await fetch(url)
return await response.json() as SourceInfo return await response.json() as SourceInfo
}, },
// refetch 时显示原有的数据
placeholderData: prev => prev,
staleTime: 1000 * 60 * 5,
enabled: inView, enabled: inView,
refetchOnWindowFocus: false, refetchOnWindowFocus: true,
}) })
const manualRefetch = useCallback(() => {
setRefetchSource(prev => ({
...prev,
[id]: Date.now(),
}))
}, [setRefetchSource, id])
if (isPending || !data) { if (isPending || !data) {
return ( return (
<> <>
@ -84,8 +101,14 @@ function NewsCard({ id, inView }: { id: SourceID, inView: boolean }) {
<span> <span>
{formatTime(data!.updateTime)} {formatTime(data!.updateTime)}
</span> </span>
<div className="flex gap-1">
<button
type="button"
className={clsx("i-ph:arrow-clockwise", isFetching && "animate-spin")}
onClick={manualRefetch}
/>
<button type="button" className={clsx(focusSources.includes(id) ? "i-ph:star-fill" : "i-ph:star")} onClick={addFocusList} /> <button type="button" className={clsx(focusSources.includes(id) ? "i-ph:star-fill" : "i-ph:star")} onClick={addFocusList} />
<button type="button" className={clsx("i-ph:arrow-clockwise", isFetching && "animate-spin")} onClick={() => refetch()} /> </div>
</div> </div>
</> </>
) )

20
src/hooks/useOnReload.ts Normal file
View File

@ -0,0 +1,20 @@
import { useEffect } from "react"
import { useBeforeUnload } from "react-use"
export function useOnReload(fn?: () => Promise<void> | void, fallback?: () => Promise<void> | void) {
useBeforeUnload(() => {
localStorage.setItem("quitTime", Date.now().toString())
return false
})
useEffect(() => {
const _ = localStorage.getItem("quitTime")
const quitTime = _ ? Number(_) : 0
if (!Number.isNaN(quitTime) && Date.now() - quitTime < 1000) {
fn?.()
} else {
fallback?.()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}

View File

@ -6,6 +6,7 @@ import "@unocss/reset/tailwind.css"
import "virtual:uno.css" import "virtual:uno.css"
import type { QueryClient } from "@tanstack/react-query" import type { QueryClient } from "@tanstack/react-query"
import { Header } from "~/components/Header" import { Header } from "~/components/Header"
import { useOnReload } from "~/hooks/useOnReload"
export const Route = createRootRouteWithContext<{ export const Route = createRootRouteWithContext<{
queryClient: QueryClient queryClient: QueryClient
@ -21,6 +22,7 @@ export const Route = createRootRouteWithContext<{
}) })
export function RootComponent() { export function RootComponent() {
useOnReload()
return ( return (
<div className="p-10"> <div className="p-10">
<Header /> <Header />