Skip to content

Commit

Permalink
feat: should do hydration
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei committed Jun 19, 2023
1 parent 02c5e9e commit 538f0a2
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 34 deletions.
25 changes: 23 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import '../styles/index.css'
import { dehydrate } from '@tanstack/react-query'
import { Analytics } from '@vercel/analytics/react'
import { ToastContainer } from 'react-toastify'
import { headers } from 'next/dist/client/components/headers'

import { ClerkProvider } from '@clerk/nextjs'

import { Root } from '~/components/layout/root/Root'
import { REQUEST_PATHNAME } from '~/constants/system'
import { defineMetadata } from '~/lib/define-metadata'
import { sansFont, serifFont } from '~/lib/fonts'
import { getQueryClient } from '~/utils/query-client.server'
Expand Down Expand Up @@ -76,8 +78,27 @@ export default async function RootLayout(props: Props) {
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
if (query.state.error) return false
// TODO dehydrate by route, pass header to filter
return true
if (!query.meta) return true
const {
shouldHydration,
hydrationRoutePath,
skipHydration,
forceHydration,
} = query.meta

if (forceHydration) return true
if (hydrationRoutePath) {
const pathname = headers().get(REQUEST_PATHNAME)

if (pathname === query.meta?.hydrationRoutePath) {
if (!shouldHydration) return true
return (shouldHydration as Function)(query.state.data as any)
}
}

if (skipHydration) return false

return (shouldHydration as Function)?.(query.state.data as any) ?? false
},
})

Expand Down
119 changes: 93 additions & 26 deletions src/app/notes/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
'use client'

import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'
import { Balancer } from 'react-wrap-balancer'
import clsx from 'clsx'
import dayjs from 'dayjs'
Expand All @@ -9,6 +11,7 @@ import type { Image } from '@mx-space/api-client'
import type { MarkdownToJSX } from '~/components/ui/markdown'
import type { ReactNode } from 'react'

import { useIsLogged } from '~/atoms/owner'
import { ClientOnly } from '~/components/common/ClientOnly'
import { PageDataHolder } from '~/components/common/PageHolder'
import { MdiClockOutline } from '~/components/icons/clock'
Expand All @@ -20,12 +23,15 @@ import { Markdown } from '~/components/ui/markdown'
import { NoteTopic } from '~/components/widgets/note/NoteTopic'
import { TocAside, TocAutoScroll } from '~/components/widgets/toc'
import { XLogInfoForNote, XLogSummaryForNote } from '~/components/widgets/xlog'
import { useBeforeMounted } from '~/hooks/common/use-before-mounted'
import { useNoteByNidQuery, useNoteData } from '~/hooks/data/use-note'
import { mood2icon, weather2icon } from '~/lib/meta-icon'
import { toast } from '~/lib/toast'
import { ArticleElementProvider } from '~/providers/article/article-element-provider'
import { MarkdownImageRecordProvider } from '~/providers/article/markdown-image-record-provider'
import { CurrentNoteIdProvider, useSetCurrentNoteId } from '~/providers/note/current-note-id-provider'
import {
CurrentNoteIdProvider,
useSetCurrentNoteId,
} from '~/providers/note/current-note-id-provider'
import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider'
import { parseDate } from '~/utils/datetime'
import { springScrollToTop } from '~/utils/scroller'
Expand All @@ -44,9 +50,9 @@ const PageImpl = () => {
// For example, `ComA` use `useParams()` just want to get value `id`,
// but if router params or query changes `page` params, will cause `CompA` re - render.
const setNoteId = useSetCurrentNoteId()
useBeforeMounted(() => {
useEffect(() => {
setNoteId(id)
})
}, [id])

const note = data?.data
const setHeaderMetaInfo = useSetHeaderMetaInfo()
Expand Down Expand Up @@ -85,27 +91,28 @@ const PageImpl = () => {
</span>
</header>

<XLogSummaryForNote />

<ArticleElementProvider>
<MarkdownImageRecordProvider images={note.images || noopArr}>
<Markdown
as="main"
renderers={MarkdownRenderers}
value={note.text}
/>
</MarkdownImageRecordProvider>

<NoteLayoutRightSidePortal>
<TocAside
className="sticky top-[120px] ml-4 mt-[120px]"
treeClassName="max-h-[calc(100vh-6rem-4.5rem-300px)] h-[calc(100vh-6rem-4.5rem-300px)] min-h-[120px] relative"
>
<NoteActionAside className="translate-y-full" />
</TocAside>
<TocAutoScroll />
</NoteLayoutRightSidePortal>
</ArticleElementProvider>
<NoteHideIfSecret>
<XLogSummaryForNote />
<ArticleElementProvider>
<MarkdownImageRecordProvider images={note.images || noopArr}>
<Markdown
as="main"
renderers={MarkdownRenderers}
value={note.text}
/>
</MarkdownImageRecordProvider>

<NoteLayoutRightSidePortal>
<TocAside
className="sticky top-[120px] ml-4 mt-[120px]"
treeClassName="max-h-[calc(100vh-6rem-4.5rem-300px)] h-[calc(100vh-6rem-4.5rem-300px)] min-h-[120px] relative"
>
<NoteActionAside className="translate-y-full" />
</TocAside>
<TocAutoScroll />
</NoteLayoutRightSidePortal>
</ArticleElementProvider>
</NoteHideIfSecret>
</article>
{!!note.topic && <NoteTopic topic={note.topic} />}
<XLogInfoForNote />
Expand Down Expand Up @@ -193,6 +200,66 @@ const NoteDateMeta = () => {
)
}

const NoteHideIfSecret: Component = ({ children }) => {
const note = useNoteData()
const secretDate = useMemo(() => new Date(note?.secret!), [note?.secret])
const isSecret = note?.secret
? dayjs(note?.secret).isAfter(new Date())
: false

const isLogged = useIsLogged()

useEffect(() => {
if (!note?.id) return
let timer: any
const timeout = +secretDate - +new Date()
// https://stackoverflow.com/questions/3468607/why-does-settimeout-break-for-large-millisecond-delay-values
const MAX_TIMEOUT = (2 ^ 31) - 1
if (isSecret && timeout && timeout < MAX_TIMEOUT) {
timer = setTimeout(() => {
toast('刷新以查看解锁的文章', 'info', { autoClose: false })
}, timeout)
}

return () => {
clearTimeout(timer)
}
}, [isSecret, secretDate, note?.id])

if (!note) return null

if (isSecret) {
const dateFormat = note.secret
? Intl.DateTimeFormat('zh-cn', {
hour12: false,
hour: 'numeric',
minute: 'numeric',
year: 'numeric',
day: 'numeric',
month: 'long',
}).format(new Date(note.secret))
: ''

if (isLogged) {
return (
<>
<div className="my-6 text-center">
<p>这是一篇非公开的文章。(将在 {dateFormat} 解锁)</p>
<p>现在处于登录状态,预览模式:</p>
</div>
{children}
</>
)
}
return (
<div className="my-6 text-center">
这篇文章暂时没有公开呢,将会在 {dateFormat} 解锁,再等等哦
</div>
)
}
return children
}

const MarkdownRenderers: { [name: string]: Partial<MarkdownToJSX.Rule> } = {
text: {
react(node, _, state) {
Expand Down
10 changes: 6 additions & 4 deletions src/atoms/owner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { atom } from 'jotai'
import { atom, useAtomValue } from 'jotai'

import { jotaiStore } from '~/lib/store'
import { toast } from '~/lib/toast'
Expand All @@ -9,7 +9,7 @@ import { apiClient } from '~/utils/request'
const ownerAtom = atom((get) => {
return get(aggregationDataAtom)?.user
})
const tokenAtom = atom(null as string | null)
const isLoggedAtom = atom(false)

export const login = async (username?: string, password?: string) => {
if (username && password) {
Expand All @@ -21,7 +21,7 @@ export const login = async (username?: string, password?: string) => {
if (user) {
const token = user.token
setToken(token)
jotaiStore.set(tokenAtom, token)
jotaiStore.set(isLoggedAtom, true)

toast(`欢迎回来,${jotaiStore.get(ownerAtom)?.name}`, 'success')
}
Expand Down Expand Up @@ -50,8 +50,10 @@ export const login = async (username?: string, password?: string) => {
}

apiClient.user.proxy.login.put<{ token: string }>().then((res) => {
jotaiStore.set(tokenAtom, res.token)
jotaiStore.set(isLoggedAtom, true)
toast(`欢迎回来,${jotaiStore.get(ownerAtom)?.name}`, 'success')
setToken(res.token)
})
}

export const useIsLogged = () => useAtomValue(isLoggedAtom)
2 changes: 1 addition & 1 deletion src/components/widgets/xlog/XLogSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const XLogSummary: FC<{
return (
<div
className={clsxm(
`mt-4 space-y-2 rounded-xl border border-slate-100 p-4 dark:border-neutral-800`,
`mt-4 space-y-2 rounded-xl border border-slate-200 p-4 dark:border-neutral-800`,
props.className,
)}
>
Expand Down
1 change: 1 addition & 0 deletions src/constants/system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const REQUEST_PATHNAME = 'request_pathname'
29 changes: 29 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

import { REQUEST_PATHNAME } from './constants/system'

export default async function middleware(req: NextRequest) {
const { pathname } = req.nextUrl

// console.debug(`${req.method} ${req.nextUrl.pathname}${req.nextUrl.search}`)

if (
pathname.startsWith('/api/') ||
pathname.match(/^\/(workbox|worker|fallback)-\w+\.js(\.map)?$/) ||
pathname === '/sw.js' ||
pathname === '/sw.js.map'
) {
return NextResponse.next()
}

// https://github.com/vercel/next.js/issues/46618#issuecomment-1450416633
const requestHeaders = new Headers(req.headers)
requestHeaders.set(REQUEST_PATHNAME, pathname)

return NextResponse.next({
request: {
headers: requestHeaders,
},
})
}
5 changes: 4 additions & 1 deletion src/providers/root/aggregation-data-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ export const AggregationProvider: FC<PropsWithChildren> = ({ children }) => {

useEffect(() => {
if (!data) return
login()
jotaiStore.set(aggregationDataAtom, data)
}, [data])

useEffect(() => {
login()
}, [])

return children
}

Expand Down
3 changes: 3 additions & 0 deletions src/queries/definition/aggregation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export const aggregation = {
queryFn: async () =>
apiClient.aggregate.getAggregateData().then((res) => res.$serialized),
cacheTime: 1000 * 60 * 10,
meta: {
forceHydration: true,
},
staleTime: isServer ? 1000 * 60 * 10 : undefined,
}),
}
15 changes: 15 additions & 0 deletions src/queries/definition/note.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import dayjs from 'dayjs'
import type { NoteWrappedPayload } from '@mx-space/api-client'

import { routeBuilder, Routes } from '~/lib/route-builder'
import { apiClient } from '~/utils/request'

import { defineQuery } from './helper'
Expand All @@ -7,13 +11,24 @@ export const note = {
byNid: (nid: string) =>
defineQuery({
queryKey: ['note', nid],
meta: {
hydrationRoutePath: routeBuilder(Routes.Note, { id: nid }),
shouldHydration: (data: NoteWrappedPayload) => {
const note = data?.data
const isSecret = note?.secret
? dayjs(note?.secret).isAfter(new Date())
: false
return !isSecret
},
},
queryFn: async ({ queryKey }) => {
const [, id] = queryKey

if (id === LATEST_KEY) {
return (await apiClient.note.getLatest()).$serialized
}
const data = await apiClient.note.getNoteById(+queryKey[1])

return { ...data }
},
}),
Expand Down

0 comments on commit 538f0a2

Please sign in to comment.