我們已經理解 react-query 在什麼狀態下會怎麼取資料,如第一次結果回來前(loading) data 會是 undefined,而第二次發出請求時 data 會暫時給你前一次 cache 的值,直到請求成功,如此循環。
在這個前提下你大概會預期 query status 的 data 會在什麼情況下會觸發改變,在假設 cache 都存在的情況下,同一個 useQuery 相同 queryKey 不斷 refetch 以下情況會改變
- 第一次從 API 取到 data 時
undefined -> data
- 第二次重新 refetch 成功後拿到新值
oldData -> newData
- 第 N 次重新 refetch 成功後拿到新值
oldData -> newData
function App() {
const status = useQuery({
queryKey: ["status"],
queryFn: fetchData,
});
useEffect(() => {
console.log("data change", status.data);
}, [status.data]);
return (<div>
<button type="button" onClick={() => status.refetch()}>
refetch
</button>
</div>);
}
再假設另一種情境,我有一個 flag 當作 key 切換,我重複的來回切換 status 的 data 會怎麼觸發更新,一樣,這次也假設 cache 都存在的情況
- flag =
false
情況下第一次從 API 取到 data 時undefined -> data(false)
- flag 由
false
轉成true
時重發 API 等待時間data(false) -> undefined
- flag =
true
API 成功undefined -> data(true)
- flag 由
true
轉成false
時重發 API 等待時間data(true) -> cache data(false)
- flag =
false
API 成功cache data(false) -> newData(false)
- flag 由
false
轉成true
時重發 API 等待時間data(false) -> cache data(true)
- flag =
true
API 成功cache data(true) -> newData(true)
- 在 cache 未消失的情況一直切換都會是 4 ~ 7 的循環
function App() {
const [flag, setFlag] = useState(false);
const status = useQuery({
queryKey: ["status", flag],
queryFn: fetchData,
});
useEffect(() => {
console.log("data change", status.data, flag);
}, [status.data]);
return (
<div>
<button type="button" onClick={() => setFlag(!flag)}>
click
</button>
<button type="button" onClick={() => status.refetch()}>
refetch
</button>
</div>
);
}
事情沒那麼簡單
當你真的實際去測我給的範例的時候,你會發現好像有些情況跟你認知不同,尤其是你自己做了一個模擬 API 的函數時
const fetchData = () => {
return Promise.resolve({ data: { name: "content" } });
};
第一個範例在 refetch 情況下會無法觸發更新,而第二個範例在每次切轉預期會有兩次但變成一次。
你以為你的預期是錯的嘛?不,前面所有預期是都對的,只是 react query 會對資料作結構共享(structural sharing),因為這個原因你的資料在某些情況下 useEffect
會偵測不到 data 的更新
結構共享
比對新舊資料將相異的資料更新,但相同的資料保留舊參考(Object, Array),來舉幾個範例
結構相同但部份資訊不同
const oldData = {
name: 'john',
age: 20
}
const newData = {
name: 'john',
age: 30
}
const result = replaceEqualDeep(oldData, newData);
flowchart TB classDef changed stroke:#0f0 subgraph result direction TB res[data]:::changed --> name[name] res[data] --> age[age]:::changed end subgraph "origin(oldData)" direction TB old[data] --> oldName[name] old[data] --> oldAge[age] end
綠框就是 oldData 與 newData 作結構共享後被異動的結構或值。
我們把結構弄複雜一點
const oldData = {
name: 'john',
age: 20,
arr: [1, 2],
subObj: {
data: "content"
}
}
const newData = {
name: 'john',
age: 20,
arr: [1, 2, 3],
subObj: {
data: "content"
}
}
const result = replaceEqualDeep(oldData, newData);
flowchart LR classDef changed stroke:#0f0 subgraph result direction TB res[data]:::changed --> resName[name] res[data] --> resAge[age] res[data] --> resArr[arr]:::changed res[data] --> resSubObj[subObj] resSubObj --> resData[data] end subgraph "origin(oldData)" direction TB old[data] --> oldName[name] old[data] --> oldAge[age] old[data] --> oldArr[arr] old[data] --> oldSubObj[subObj] oldSubObj --> oldData[data] end
這樣我們可以看出在部份資料修改時整個父層結構都會被更新,詳細可以看 replaceEqualDeep 實作,或是參考測試案例
到此,剛剛的疑惑可以解開了,因為我們 API data 一直沒變,結構共享後的參考也會維持,所以 useEffect
會偵測不到更新。
哪些更新因為結構共享被省略了?
第一個範例
- 第一次從 API 取到 data 時
undefined -> data
- ❌ 第二次重新 refetch 成功後拿到新值
oldData -> newData
- ❌ 第 N 次重新 refetch 成功後拿到新值
oldData -> newData
第二個
- flag =
false
情況下第一次從 API 取到 data 時undefined -> data(false)
- flag 由
false
轉成true
時重發 API 等待時間data(false) -> undefined
- flag =
true
API 成功undefined -> data(true)
- flag 由
true
轉成false
時重發 API 等待時間data(true) -> cache data(false)
- ❌ flag =
false
API 成功cache data(false) -> newData(false)
- flag 由
false
轉成true
時重發 API 等待時間data(false) -> cache data(true)
- ❌ flag =
true
API 成功cache data(true) -> newData(true)
- 在 cache 未消失的情況一直切換都會是 4 ~ 7 的循環
還是太天真了
今天在你的 useQuery 多了加了select 函數,情況又不一樣了,因為 react query 會自動再執行一次結構共享,只是時機點不同,所以一次的 data 更新總共會執行兩次,但在第一個範例你看不出差異,需要第二個範例才能察覺。
function App() {
const [flag, setFlag] = useState(false);
const status = useQuery({
queryKey: ["status", flag],
queryFn: fetchData,
select: (v) => v // 新增 select 函數
});
useEffect(() => {
console.log("data change", status.data, flag);
}, [status.data]);
return (
<div>
<button type="button" onClick={() => setFlag(!flag)}>
click
</button>
<button type="button" onClick={() => status.refetch()}>
refetch
</button>
</div>
);
}
在試著去切換你會發現有哪些更新被省略了
- flag =
false
情況下第一次從 API 取到 data 時undefined -> data(false)
- flag 由
false
轉成true
時重發 API 等待時間data(false) -> undefined
- flag =
true
API 成功undefined -> data(true)
- ❌ flag 由
true
轉成false
時重發 API 等待時間data(true) -> cache data(false)
- ❌ flag =
false
API 成功cache data(false) -> newData(false)
- ❌ flag 由
false
轉成true
時重發 API 等待時間data(false) -> cache data(true)
- ❌ flag =
true
API 成功cache data(true) -> newData(true)
- 在 cache 未消失的情況一直切換都會是 4 ~ 7 的循環
兩次結構共享時機點
第一次的結構共享會是 react-core 裡的 Query
fetch API 拿到值的當下會做一次去更新狀態,第二次會從我準備要更新狀態根據你有沒有放 select 再做一次,否則就是直接替換。
flowchart TB subgraph setData one[replaceEqualDeep] one --> dispatchUpdate end dispatchUpdate --有 select--> two[replaceEqualDeep] dispatchUpdate --無 select--> replace two & replace --> newData[new data state]
那不帶 select 會使切換時 data 偵測到更新的原因是什麼?
首先要先知道,react-query 由 Observer 管理是否更新狀態,更新有可能會抓取 query client 不同的 Query 實體,所以並不是一個 Observer 永遠只配對同一個 Query 實體。
flowchart TB QueryClient --get target query--> Observer Observer --update--> state[React State]
因此 query key 的切換會導致 Observer 取到的 Query 實體不同,即便 Query class setData
本身會作結構共享,但實際 Observable 在更新 data 是兩個不同 Query 實體資料的切換,所以 data 會偵測到更新,如果是直接執行 refetch
就不會偵測到變更。
相關連結
重點參考函數
- useBaseQuery
observer.getOptimisticResult
observer.getCurrentResult
observer.subscribe
observer.setOptions
- queryObserver
setOptions
#executeFetch
updateResult
getOptimisticResult
createResult
- query
fetch
setData