使用 custom hook 時要注意的事情

很愛捉弄人的 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]) // 放到 dependencies

return { data, isPending, error }
}

按下存檔後,恭喜你又陷入無限迴圈了。

為什麼前面特別強調參數不可以 reference type?就是因為這樣,每一次執行 custom hook 時都會重新宣告一個新的 Object,重新宣告代表什麼?試著思考 {} === {} 會得到什麼?

沒有錯,只會是 false

所以只要是 reference type 的變數都要特別注意,你不可以直接這樣傳給 custom hook,接著又把它放到 dependencies 裡。

正確的做法

如果想避免掉這種陷入無限迴圈的問題,你有兩種選擇:

  1. 把要「傳入」的參數用 useState 來儲存
  2. 把要「接收」的參數用 useRef 來儲存

先來看第一種作法:

1
2
3
4
5
6
7
8
9
10
11

export default function App() {
const [url, setUrl] = useState('https://example.com')
// 建立 state
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)
// 改用 useRef 來儲存
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 的問題,不然很容易陷入可怕的無窮迴圈。

JavaScript-shuffle(洗牌的方法) CSS-Flip Card
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×