React Hooks 使用注意事项
Area: Hooks
Date: February 10, 2025
Important: 🌟🌟🌟
React Hooks 注意事项
要点:
useState
的初始化值 只在第一次渲染时计算,并且这个值不会随着组件重新渲染而更新。useEffect
可能出现死循环,比如处理请求接口时,依赖项绑定对象类型数据,会死循环。
useState 的初始值问题
Case:useState
的初始化值 只在第一次渲染时计算,并且这个值不会随着组件重新渲染时更新。
理解:useState
的初始化值只在组件的第一次渲染时生效,不会随着 props
或 state
的变化而更新。
import React, { useState } from 'react'
// 子组件
function Child({ userInfo }) {
// render: 初始化 state
// re-render: 只恢复初始化的 state 值,不会再重新设置新的值
// 只能用 setName 修改
const [name, setName] = useState(userInfo.name)
return (
<div>
<p>Child, props name: {userInfo.name}</p>
<p>Child, state name: {name}</p>
</div>
)
}
function App() {
const [name, setName] = useState('双越')
const userInfo = { name }
return (
<div>
<div>
Parent
<button onClick={() => setName('慕课网')}>setName</button>
</div>
<Child userInfo={userInfo} />
</div>
)
}
export default App
解决方案:使用 useEffect
来监听 userInfo
的变化,并在变化时更新 name
。
import React, { useState, useEffect } from 'react'
function Child({ userInfo }) {
const [name, setName] = useState(userInfo.name)
// 使用 useEffect 来监听 userInfo.name 的变化并更新 name
useEffect(() => {
setName(userInfo.name)
}, [userInfo.name]) // 当 userInfo.name 发生变化时更新 name
return (
<div>
<h1>Child, Props name: {userInfo.name}</h1>
<h1>Child, state name: {name}</h1>
</div>
)
}
function App() {
const [name, setName] = useState('Nathan')
const userInfo = {
name,
}
return (
<div>
<h1>UserName</h1>
<button onClick={() => setName('XXXX')}>Change Name</button>
<Child userInfo={userInfo} />
</div>
)
}
export default App
useEffect 的死循环问题
Case:useEffect
死循环
理解:处理请求接口时,依赖项绑定对象类型数据,会死循环。
import { useState, useEffect } from 'react'
import axios from 'axios'
// 传入 config,类型为对象
function useAxios(url, config = {}) {
const [loading, setLoading] = useState(false)
const [data, setData] = useState()
const [error, setError] = useState()
useEffect(() => {
setLoading(true)
axios
.get(url) // 发送一个 get 请求
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url, config])
return [loading, data, error]
}
export default useAxios
分析:
1-useEffect
和依赖项的关系:
在 useEffect
中,[url, config]
是依赖项数组。这意味着当 url
或 config
发生变化时,useEffect
会重新执行。
2-对象 config
的变化:
在 JavaScript 中,对象(如 config
)是引用类型。如果每次重新渲染时,config
的引用发生了变化,即使它的内容相同,React 也会认为它发生了变化,从而触发 useEffect
的重新执行。
这就导致了一个问题:每次组件重新渲染时,config
对象的引用可能会发生变化,即使它的值没有改变。这会导致 useEffect
被反复调用,进而每次发起请求。
注:这里 React 底层使用的是 Object.is()
进行比较
总结无限请求的原因:
- 每次组件渲染时,
config
会被认为是新的对象(即使它的内容没有变化),从而触发useEffect
重新执行。 useEffect
执行时会发起请求,且请求可能会导致config
的变化(如果在请求完成后更新了config
,即使没有显式更新也可能影响父组件中的config
)。- 因此,
useEffect
会再次被触发,导致请求不断发起,形成无限请求的循环。
解决方案:
基础方案:
useEffect
依赖项不要放置对象类型。可在之前将对象解构,useEffect
依赖项中放置基本类型值
另一种方案:
如果你希望在 config
变化时也重新发送请求,你可以使用 useMemo
来确保只有在 config
内容变化时才触发请求:
import { useState, useEffect, useMemo } from 'react'
import axios from 'axios'
function useAxios(url, config = {}) {
const [loading, setLoading] = useState(false)
const [data, setData] = useState()
const [error, setError] = useState()
// 使用 useMemo 来确保 config 引用只有在实际变化时才会更新
const memoizedConfig = useMemo(() => config, [JSON.stringify(config)])
useEffect(() => {
setLoading(true)
axios
.get(url, memoizedConfig) // 使用 memoizedConfig 而非原始 config
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url, memoizedConfig])
return [loading, data, error]
}
export default useAxios