---
# System prepended metadata

title: '# react query （ stale while invalidate library）'

---

## 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`，因為預設是０ 所以每次 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
    }
  }
})
```

