## get api with zustand ```typescript // file folder src ├── App.css ├── App.tsx ├── assets │ └── react.svg ├── components │ └── State.tsx ├── hooks │ ├── useParams.ts │ └── useSearchStore.ts ├── index.css ├── main.tsx └── vite-env.d.ts // src/components/State.tsx import React from 'react' import { useSearchStore } from '../hooks/useSearchStore' import { ParamsKeys } from '../hooks/useParams' function State() { const { result, setParams, params } = useSearchStore() const handleSetParams = (k: ParamsKeys, value: string) => { setParams({ [k]: value }) } return ( <div> <div> {Object.entries(params).map(([k, value], index) => ( <div key={index}> <label htmlFor={`${k}-input`}>{k}</label> <input type="text" id={`${k}-input`} onChange={e => { handleSetParams(k as ParamsKeys, e.target.value) }} value={value} /> </div> ))} </div> <div> {result.data?.map(comment => ( <p key={comment.id}>{comment.id} - {comment.name}</p> ))} </div> </div> ) } export default State // src/hooks/useParams.ts import { createTrackedSelector } from 'react-tracked' import create from 'zustand' import { devtools } from 'zustand/middleware' import { persist, createJSONStorage } from 'zustand/middleware' export type ParamsKeys = | 'limit' | 'page' | 'fullText' interface LeftSidebarState { params: Partial<Record<ParamsKeys, string>> setParams: (params: Partial<Record<ParamsKeys, string>>) => void } const ParamsStore = create<LeftSidebarState>()( devtools( persist( (set, get) => ({ params: { limit: '', page: '', fullText: '' }, setParams: (params) => { const previousParams = get().params set({ params: { ...previousParams, ...params } }) } }), { name: 'Params store', // storage: createJSONStorage(() => sessionStorage) } ), { name: 'Params store', serialize: { options: true } }, ), ) export const useParams = createTrackedSelector(ParamsStore) // src/hooks/useSearchStore.ts import { QueryFunction, useQuery } from "@tanstack/react-query" import { useParams } from "./useParams"; interface Comments { postId: number; id: number; name: string; email: string; body: string; } const getComments: QueryFunction<Comments[], (string | undefined)[]> = async ({ queryKey }) => { const [type, page, limit, fullText] = queryKey const data = await fetch(`https://jsonplaceholder.typicode.com/comments?_page=${page}&_limit=${limit}&q=${fullText}`).then(res => res.json()) return data } export const useSearchStore = () => { const { params, setParams } = useParams() const result = useQuery({ queryKey: ['comments', params.page, params.limit, params.fullText], queryFn: getComments, }) return { result, setParams, params } } ``` staleTime 在 reqct query 預設是 0 ,這樣每次都會去重新 fetch data,staleTime 在 react query中表示 `什麼時候資料過期,然後過期的資料去觸發 react query 重新fetch`,因為預設是0 所以每次 user進入 page 都會重新 fetch data。 `staleTime: 多久時間內不會再發一次 request` `cacheTime: 多久時間清除 query cache 資料` ```typescript= const commentDataQuery = useQuery({ queryKey: commentKeys.lists(), queryFn: async () => { const data = await fetch('https://jsonplaceholder.typicode.com/comments').then(res => res.json()) return data }, // 這裡代表 10s 內我只會 fetch 一次,data 直接從 query cache 中返回,當10s過後 user 在重新造訪頁面就會去重新 fetch data staleTime: 10 * 1000 }) const commentDataQuery = useQuery({ queryKey: commentKeys.lists(), queryFn: async () => { const data = await fetch('https://jsonplaceholder.typicode.com/comments').then(res => res.json()) return data }, // Infinity 代表data 只會fetch一次不會因為 windowOnfocus 就重新 fetch 只能透過 queryClient.invalidateQuery 更新 cache 資料同時 refetch staleTime: Infinity }) ``` ![](https://hackmd.io/_uploads/B156b5Cu2.png) ### react query 狀態 * fresh: cache data 是新的所以不會因為 user 換頁或是 change tab而重新觸發request,通常 fresh 時間會是根據 staleTime 去算預設 staleTime = 0 ,所以每次 user 瀏覽 page 時都會重新觸發 request。 * fetching: 資料在獲取中,會在 queryClient.invalidateQueries() 中去觸發。 * paused: 失效的 request ,在 cancel request時會出現的狀態或是 offline 等到 user 重新連線時 request 重新觸發,狀態就會從paused 變到 fetching 。 * stale: stale 過期的資料會告訴 react query 說需要重新發 request 。 * inactive: 資料沒有被 react query 的 Observer 去監聽到,預設 15分鐘後會被 react query 垃圾回收掉 (下方補充更多) ### inactive query and. Observer ![](https://hackmd.io/_uploads/BJzQ1PDq3.png) 圖片中灰色的部分就是 inactive 的 query data ,數量代表這個 query 被多少 Observer 去監聽到,inactive 的狀態的 data 還是存在 cache 中並沒有消失,只是代表該 query 目前沒有被 component 中使用到。 在 react query 中有一個很重要的觀念就是 Observer ,當你在 component 中使用 useQuery 時就會生成對應的 Observer 到 userQuery 中,而 Observer 的作用在於監聽使用該 component 中的 userQuery 的資料是否有做變更,然後觀察是否需要重新渲染組件,每個 Observer都是透過 query keys去管理不同的 Observer,這也是為什麼我們可以透過 ` queryClient.invalidateQueries({queryKey:['your key']}) ` 去管理 component render的行為, Observer 他會知道這個 component 使用到多少 userQuery return info,例如你只使用 data 在 component,就算你的 useQuery isFetching 的值改變並在背景中重新 fetch,你的 component 並不會重新觸發 render。 ```typescript function Demo() { const { data, isFetching } = useQuery({ queryKey: ['your_key'], queryFn: () => fetch('./api/v1/') }) return ( <div> {JSON.stringify(data)} </div> ) } ``` 所以如果你希望 isFetching 後 data 也要重新 render就要改寫成以下: 這樣 data 才會同步更新 ```typescript function Demo() { const { data, isFetching } = useQuery({ queryKey: ['your_key'], queryFn: () => fetch('./api/v1/') }) if (isFetching) return '.....isFetching' return ( <div> {JSON.stringify(data)} </div> ) } ``` ### notifyOnChangeProps 這個 props 則是告訴 useQuery 中哪些 props 變化才需要 Observer 去監聽可以利用個 setting去優化效能,通常會搭配 select 去做使用: ```typescript interface Posts { userId: number; id: number; title: string; body: string; } const queryFn: QueryFunction<Posts[], string[]> = () => { return fetch('https://jsonplaceholder.typicode.com/posts').then(res => res.json()) } export const useTodoQuery = (select: (data: Posts[]) => any, notifyOnChangeProps: Array<keyof InfiniteQueryObserverResult> | 'all') => useQuery({ queryKey: ['todo'], queryFn, select, notifyOnChangeProps }) const { data: PostsData } = useTodoQuery((data) => data.filter(item => item.id === 1), ['data']) ``` 原因是 useTodoQuery 會去監聽兩次,一次是 useQuery 中 queryFunction的return 結果,其二則是select 的 return,就由設定 notifyOnChangeProps 是 ['data'],這樣就是告訴 useTodoQuery 我只關心 useQuery 中的 data有沒有改變,其他的 isFetch 或是 Error結果我不需要關心,預設是 'all'。 ### react query 中強大的 select 功能 ```typescript export const useTodosQuery = () => useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.map((todo) => todo.name.toUpperCase()), }) ``` 有時候我的 data response 資料很廣大這時我們可以透過 select 幫我們 res 的 data 做 transform,這邊要注意的是 select callback function 只會在 data 成功回傳時才會觸發,所以不用擔心 undefinded 狀況。 select 可以用來只訂閱特定的資料結果範例如下: ```typescript export const useTodosQuery = (select) => useQuery(['todos'], fetchTodos, { select }) export const useTodosCount = () => useTodosQuery((data) => data.length) export const useTodo = (status) => useTodosQuery((data) => data.find((todo) => todo.status === status)) ``` 這裡的 useTodosCount 是透過 useTodosQuery select的結果,因為 useTodosCount 是一個 selector ,他只會監聽 useTodosQuery 中的 data是否有數量上的改變,就算 useTodosQuery 中不管是 isFetch 變化獲釋 data 資料改變,只要數量不變 useTodosCount 就不會造成 component rerender的狀況,這是 react query 中 structuralSharing 的設定。 ```typescript const client = new QueryClient({ defaultOptions: { queries: { structuralSharing:true // default } } }) ``` structuralSharing 他會陸續追蹤 return 的 data資料的變化選擇是否要重新渲染,範例如下: 架設我有一個 response data ```typescript // before [ { "id": 1, "name": "Learn React", "status": "active" }, { "id": 2, "name": "Learn React Query", "status": "todo" }, { "id": 3, "name": "Learn JS", "status": "todo" } ] // after [ - { "id": 1, "name": "Learn React", "status": "active" }, + { "id": 1, "name": "Learn React", "status": "done" }, { "id": 2, "name": "Learn React Query", "status": "todo" } ] ``` 這時 react query 就會檢查 data 是否有更新,然後將最後的結果映射到 query cache中 ```typescript // ✅ will only re-render if _something_ within todo with status:'todo' changes // thanks to structural sharing const { data } = useTodo('todo') ``` useTodo 就是一種 structuralSharing 的 selector,要注意的是 structuralSharing 預設會執行兩次 render,一次是根據 queryFn return的結果去比較資料是否有變化,第二次則是 select 後 return 的結果。 ```typ { status: 'success', data: 2, isFetching: true } { status: 'success', data: 2, isFetching: false } ``` 所以如果架設今天你的 data 結構很龐大就可以考慮不要啟用這功能(預設是開啟) ```typescript const client = new QueryClient({ defaultOptions: { queries: { structuralSharing:false // default } } }) ```