今天要來講 trpc
cache 機制,在 trpc
中有兩種方式 :
因為 getInitialProps
是早期在 getServerSideProps 還沒出現前的替代方式,trpc
為了整合這部分功能所以才加上,如果需要啟用只需要在 trpc client
中設定就好。
getInitialProps
是一種獲取頁面資料的一種方式,也別於在 client side
透過 useEffect
fetch data,getInitialProps
讓你可以在 server side
中提前幫你 fetch
data ,不用等到 client side
才發送 get data
的 request
而達到 preFetch
效果,而 getInitialProps
這個 function
執行時機點可以在 client side
跟 server side
。
server side: 當 user
第一次進到頁面時,會在 server side
先執行第一次,透過序列化以及緩序列化將 data
存到 window.__NEXT_DATA__.props
中,並寫入 html
的 inline script
,<script>window.__NEXT_DATA__={props:{xxx}}</script>
這樣 client side
的 component
就可以透過 inline script
吃到這個 page 中會用到的 data,而不是去發 request api
。
client side: 當 user
是透過連結跳轉過去或是點選上一頁的時候,而不是透過貼上連結的方式時, getInitialProps
會在 client side
中去觸發,但因為 user
已經訪問過第一次,所以這時在 client 端中執行的 getInitialProps
會去讀取 window.__NEXT_DATA__.props
內容然後給 component 中使用。
至於 getInitialProps
怎麼更新,則是透過設置 request
的 cache-control
決定 revalidate
的時間點,那 trpc
怎麼做到下面 demo
會解釋。
getInitialProps
目前的生態系中已經不是主流的寫法,取而代之的是透過 getServerSideProps
、getStaticProps
等方法,差別在於後者只會在 server
中拿取頁面會用到的資料以及 revalidae cache
。
getInitialProps
中還有一個致命缺點就是因為他會在 client
跟 server
都去執行,所以 getInitialProps
中會用到的 function
也會一並打包到 client
中,這會大大增加 js boundle size
的大小,所以這部分筆者不建議去用他。
透過 ssr: true
設置,如此一來 trpc
預設將透過 getInitialProps
preFetch
每個頁面調用的 useQuery
。
export const trpc = createTRPCNext<AppRouter>({
config(opts) {
if (typeof window !== 'undefined') {
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: 'http://localhost:3000/api/trpc';
return {
links: {
http: httpBatchLink({
url,
}),
},
};
},
ssr: true,
如果你需要設置 revalidate
, trpc
有一個 responseMeta
可以設置 cache-control
時機點,但這部分必須使用 ssr: true
的功能。
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers/_app';
export const trpc = createTRPCNext<AppRouter>({
config(opts) {
if (typeof window !== 'undefined') {
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: 'http://localhost:3000/api/trpc';
return {
links: {
http: httpBatchLink({
url,
}),
},
};
},
ssr: true,
responseMeta(opts) {
const { clientErrors } = opts;
if (clientErrors.length) {
// propagate http first error from API calls
return {
status: clientErrors[0].data?.httpStatus ?? 500,
};
}
// 每秒進行一次 revalidate , cache 最多存一天
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: {
'cache-control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
},
};
},
});
這部分是本人比較推薦的用法,有個 helper
就不需要依靠 getInitialProps
幫我們做 preFetch
而是能在 getServerSideProps
、getStaticProps
中自由使用。
首先先新增 posts/[id].tsx
page ,然後在 posts page
加一個 click 轉址到 post details
// ~src/posts/[id].tsx
const PostDetail = ({ id }: PostDetailProps) => {
const router = useRouter()
const { data: post, status, ...rest } = api.posts.getPost.useQuery({ post_id: id })
if (status !== 'success') {
return <>Loading...</>
}
return (
<>
<h1>Post details Page</h1>
<p>title : {post.title}</p>
<p>id : {post.id}</p>
<p>content : {post.content}</p>
<button onClick={() => router.back()}>back to home</button>
</>
)
}
添加 IoIosArchive
icon 做導頁。
// ~src/page/index.tsx
import { IoIosArchive } from "react-icons/io";
export default function Home() {
// ..
return (
// ..
{posts.map((post, index) => (
<li
key={post.id}
className="flex items-center justify-between cursor-pointer"
>
//..
<div className="flex items-center gap-[1rem]">
<AiFillDelete
color="red"
className="cursor-pointer"
size={20}
onClick={async () => {
await deletePost({ id: post.id })
}}
/>
<IoIosArchive
onClick={() => router.push(`/posts/${post.id}`)}
/>
//..
);
}
然後呼叫 createServerSideHelpers
這樣你就可以在 server Side
中使用 trpc
的內容了。
export async function getStaticProps(
context: GetStaticPropsContext<{ id: string }>,
) {
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
prisma
},
});
// const id = context.params?.id as string;
const id = context.params?.id as string
// preFetch post data to PostDetail
await helpers.posts.getPost.prefetch({ post_id: id })
return {
props: {
trpcState: helpers.dehydrate(),
id,
},
revalidate: 1,
};
}
這邊 trpc
先做 prefetch
將資料送到 query cache
中。
await helpers.posts.getPost.prefetch({ post_id: id })
並將 pararms.id
一並傳到 client。
return {
props: {
trpcState: helpers.dehydrate(),
id,
},
//..
因為 trpc
已經先 preFetch
過,所以在 PostDetail
中的 api.posts.getPost.useQuery
不會再重新發送一次 request
而是直接從 query cahce
中拿資料,所以 if (status !== 'success')
這裡的 function
永遠不會去觸發,也就不會再有 loading
的畫面。
// ~src/posts/[id].tsx
const PostDetail = ({ id }: PostDetailProps) => {
const router = useRouter()
const { data: post, status, ...rest } = api.posts.getPost.useQuery({ post_id: id })
if (status !== 'success') {
return <>Loading...</>
}
//..
那因為有加 revalidate: 1
,所以只要 user
只要重新回到頁面時每秒都會幫你重新 update data
。
export async function getStaticProps(
context: GetStaticPropsContext<{ id: string }>,
) {
//..
return {
props: {
trpcState: helpers.dehydrate(),
id,
},
revalidate: 1,
};
}
但假如如果頁面中資料並不太需要太頻繁的 revalidate data
,你可以把 revalidate
的時間調高,或是把 user
觸發 revalidate
行為關閉如下:
如果一來就不會因為 WindowReFocus
就打一次 api
,但這部分還是取決於你的需求是什麼去做調整,並沒有一定的答案。
const data = trpc.example.useQuery(
// if your query takes no input, make sure that you don't
// accidentally pass the query options as the first argument
undefined,
{ refetchOnMount: false, refetchOnWindowFocus: false },
);
或是你可以在 trpc client
中設定。
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import superjson from 'superjson';
import type { AppRouter } from './api/trpc/[trpc]';
export const trpc = createTRPCNext<AppRouter>({
config(opts) {
return {
transformer: superjson,
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
// Change options globally
queryClientConfig: {
defaultOptions: {
queries: {
refetchOnMount: false,
refetchOnWindowFocus: false,
},
},
},
},
},
});
最後我們透過 npm build
來看看結果吧~
這樣 data
跟 initial html
都先被 server side
build 完成了,如此一來你就完成 preLoad
跟preFetch
的功能摟~
https://github.com/Danny101201/next_demo/tree/main
✅ 前端社群 :
https://lihi3.cc/kBe0Y