很愛捉弄人的 reference。
簡述
這個觀念說難不難,但如果沒注意的話會很容易疏忽掉,所以才特別開一篇來紀錄一下。
假設我有一個 custom hook 是用來處理 request 的,長這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { useState, useEffect } from "react"
export const useFetch = (url) => { const [data, setData] = useState(null) const [isPending, setIsPending] = useState(false) const [error, setError] = useState(null)
useEffect(() => { const fetchData = async () => { const res = await fetch(url) const json = await res.json() setData(json) } fetchData() }, [url])
return { data, isPending, error } }
|
這邊想說的是,useEffect
只要用了外部的變數或 state,就會要你把它們放到 dependencies 中,好讓值被更新時能夠重新執行一次 Effect。
在使用上面的 custom hook 時需要傳入 url,來執行不同的 request,所以把 url 放到 dependencies 合情合理,也沒有任何問題。
但請注意,這是建立在:
- 當參數不是 referece type(non-primitive)」的情況下才算數
- 當參數不是 referece type(non-primitive)」的情況下才算數
- 當參數不是 referece type(non-primitive)」的情況下才算數
怎麼說?
你可以試著加入一個 option 參數,用 Object 的形式傳進去,看看會發生什麼事:
1 2 3 4 5 6 7
| function App () { const [url, setUrl] = useState('https://example.com') const { data, isPending, error } = useFetch(url, { type: 'GET' }) ... ... ... }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { useState, useEffect, useRef } from "react"
export const useFetch = (url, options) => { const [data, setData] = useState(null) const [isPending, setIsPending] = useState(false) const [error, setError] = useState(null)
useEffect(() => { console.log(options) const fetchData = async () => { const res = await fetch(url) const json = await res.json() setData(json) } fetchData() }, [url, options])
return { data, isPending, error } }
|
按下存檔後,恭喜你又陷入無限迴圈了。
為什麼前面特別強調參數不可以 reference type?就是因為這樣,每一次執行 custom hook 時都會重新宣告一個新的 Object,重新宣告代表什麼?試著思考 {} === {}
會得到什麼?
沒有錯,只會是 false
。
所以只要是 reference type 的變數都要特別注意,你不可以直接這樣傳給 custom hook,接著又把它放到 dependencies 裡。
正確的做法
如果想避免掉這種陷入無限迴圈的問題,你有兩種選擇:
- 把要「傳入」的參數用
useState
來儲存
- 把要「接收」的參數用
useRef
來儲存
先來看第一種作法:
1 2 3 4 5 6 7 8 9 10 11
| export default function App() { const [url, setUrl] = useState('https://example.com') const [optios, setOptions] = useState({type: 'GET'}) const { data, isPending, error } = useFetch(url, optios) ... ... ... }
|
利用 state 是 Immutable 的特性來存狀態就可以避免掉不同 reference 的問題。
接著是第二種作法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { useState, useEffect, useRef } from "react"
export const useFetch = (url, _options) => { const [data, setData] = useState(null) const [isPending, setIsPending] = useState(false) const [error, setError] = useState(null) const options = useRef(_options).current
useEffect(() => { console.log(options) const fetchData = async () => { const res = await fetch(url) const json = await res.json() setData(json) } fetchData() }, [url, options])
return { data, isPending, error } }
|
跟剛剛的道理一樣,利用 useRef
不會在 re-render 時被重新宣告的特性,就能避免不同 reference 的問題。
總之使用 custom hook 時一定要特別注意 reference 的問題,不然很容易陷入可怕的無窮迴圈。