React Hooks
一、useState
声明状态变量:
jsx
import React, { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
// 复杂初始值使用函数
const [rows, setRows] = useState(() => createRows(props.count))
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
)
}import React, { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
// 复杂初始值使用函数
const [rows, setRows] = useState(() => createRows(props.count))
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
)
}引用类型修改
jsx
const [item, setItem] = useState([
{ id: 1, name: 'zs' }
])
// 删除(使用函数式修改)
setItem(items => items.filter(item => item.id !== id))
// 添加
setItem(items => [...items, { id: 2, name: 'ww' }])const [item, setItem] = useState([
{ id: 1, name: 'zs' }
])
// 删除(使用函数式修改)
setItem(items => items.filter(item => item.id !== id))
// 添加
setItem(items => [...items, { id: 2, name: 'ww' }])注意事项
- 不能通过
if...else调用,Hooks 依赖调用顺序 - 初始值是函数返回值时,使用函数形式:
useState(() => createRows(props.count)) - 修改部分属性时使用函数形式:
jsx
setState(prev => ({ ...prev, name: 'xxxx' }))setState(prev => ({ ...prev, name: 'xxxx' }))二、useEffect
处理副作用,代替 componentDidMount、componentDidUpdate、componentWillUnmount:
jsx
import React, { useEffect } from 'react'
// 每次渲染后执行
useEffect(() => {
console.log('执行副作用')
})
// 只执行一次(空依赖)
useEffect(() => {
fetch('/api/data').then(res => setData(res))
}, [])
// 依赖变化时执行
useEffect(() => {
console.log('count变化了')
}, [count])
// 返回清理函数
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])import React, { useEffect } from 'react'
// 每次渲染后执行
useEffect(() => {
console.log('执行副作用')
})
// 只执行一次(空依赖)
useEffect(() => {
fetch('/api/data').then(res => setData(res))
}, [])
// 依赖变化时执行
useEffect(() => {
console.log('count变化了')
}, [count])
// 返回清理函数
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])async 处理
jsx
useEffect(() => {
async function fetch() {
const res = await getData()
setData(res)
}
fetch()
}, [])useEffect(() => {
async function fetch() {
const res = await getData()
setData(res)
}
fetch()
}, [])处理无序响应
jsx
useEffect(() => {
let ignore = false
async function fetch() {
const response = await fetch('/api/data')
const json = await response.json()
if (!ignore) setList(json)
}
fetch()
return () => { ignore = true }
}, [])useEffect(() => {
let ignore = false
async function fetch() {
const response = await fetch('/api/data')
const json = await response.json()
if (!ignore) setList(json)
}
fetch()
return () => { ignore = true }
}, [])useEffect vs componentDidUpdate
useEffect 通过闭包保存过程值,componentDidUpdate 始终获取最新值:
jsx
useEffect(() => {
setTimeout(() => console.log(count), 2000) // 1,2,3,4,5
})
componentDidUpdate() {
setTimeout(() => console.log(this.state.count), 2000) // 5,5,5,5,5
}useEffect(() => {
setTimeout(() => console.log(count), 2000) // 1,2,3,4,5
})
componentDidUpdate() {
setTimeout(() => console.log(this.state.count), 2000) // 5,5,5,5,5
}useLayoutEffect
同步执行副作用,在 DOM 更新后同步触发,比 useEffect 先执行:
jsx
function App() {
const [width, setWidth] = useState(0)
useLayoutEffect(() => {
const title = document.querySelector('#title')
setWidth(title.getBoundingClientRect().width)
})
return (
<div>
<h1 id="title">hello</h1>
<h2>{width}</h2>
</div>
)
}function App() {
const [width, setWidth] = useState(0)
useLayoutEffect(() => {
const title = document.querySelector('#title')
setWidth(title.getBoundingClientRect().width)
})
return (
<div>
<h1 id="title">hello</h1>
<h2>{width}</h2>
</div>
)
}区别:
useEffect— 异步执行,不阻塞 DOM 更新useLayoutEffect— 同步执行,阻塞 DOM 更新,适合 DOM 操作
三、useCallback
缓存函数,依赖变化时才重新创建:
jsx
const memoizedCallback = useCallback(() => {
console.log(a, b)
}, [a, b])
// 配合 memo 阻止子组件重复渲染
const Child = memo(() => {
console.log('子组件渲染')
return <div>Child</div>
})
function Parent() {
const fn = useCallback(() => {
console.log('函数被缓存')
}, [])
return <Child onClick={fn} />
}const memoizedCallback = useCallback(() => {
console.log(a, b)
}, [a, b])
// 配合 memo 阻止子组件重复渲染
const Child = memo(() => {
console.log('子组件渲染')
return <div>Child</div>
})
function Parent() {
const fn = useCallback(() => {
console.log('函数被缓存')
}, [])
return <Child onClick={fn} />
}四、useMemo
缓存计算结果,依赖变化时重新计算:
jsx
const memoValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
// 等价于 useCallback
const fn = useMemo(() => () => {
console.log(123)
}, [])const memoValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
// 等价于 useCallback
const fn = useMemo(() => () => {
console.log(123)
}, [])注意:使用 useMemo 和 useCallback 优化子组件时,必须同时满足:
- 子组件已通过
React.memo缓存 - 子组件所有 prop 都被缓存
五、useRef
获取 DOM 元素
jsx
function TextInput() {
const inputEl = useRef(null)
const focus = () => {
inputEl.current.focus()
}
return (
<>
<input ref={inputEl} />
<button onClick={focus}>获取焦点</button>
</>
)
}function TextInput() {
const inputEl = useRef(null)
const focus = () => {
inputEl.current.focus()
}
return (
<>
<input ref={inputEl} />
<button onClick={focus}>获取焦点</button>
</>
)
}保存可变值
jsx
function Counter() {
const [count, setCount] = useState(0)
const ref = useRef(count)
useEffect(() => {
ref.current = count // 手动更新 ref
setTimeout(() => console.log(ref.current), 2000) // 始终是最新值
})
}function Counter() {
const [count, setCount] = useState(0)
const ref = useRef(count)
useEffect(() => {
ref.current = count // 手动更新 ref
setTimeout(() => console.log(ref.current), 2000) // 始终是最新值
})
}保存定时器
jsx
function Timer() {
const timerRef = useRef(null)
const start = () => {
timerRef.current = setInterval(() => {
console.log('tick')
}, 1000)
}
const stop = () => {
clearInterval(timerRef.current)
}
}function Timer() {
const timerRef = useRef(null)
const start = () => {
timerRef.current = setInterval(() => {
console.log('tick')
}, 1000)
}
const stop = () => {
clearInterval(timerRef.current)
}
}惰性初始化
jsx
function getObserver() {
const ref = useRef(null)
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect)
}
return ref.current
}function getObserver() {
const ref = useRef(null)
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect)
}
return ref.current
}六、useImperativeHandle
配合 forwardRef 向父组件暴露方法:
jsx
// 子组件
const Child = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
myMethod: () => console.log('子组件方法')
}), [])
return <div>Child</div>
})
// 父组件
function Parent() {
const childRef = useRef(null)
useEffect(() => {
childRef.current.myMethod()
}, [])
return <Child ref={childRef} />
}// 子组件
const Child = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
myMethod: () => console.log('子组件方法')
}), [])
return <div>Child</div>
})
// 父组件
function Parent() {
const childRef = useRef(null)
useEffect(() => {
childRef.current.myMethod()
}, [])
return <Child ref={childRef} />
}七、useReducer
适用于复杂 state 逻辑:
jsx
const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
throw new Error()
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
)
}const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
throw new Error()
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
)
}八、useContext
简化 Context 消费:
jsx
const ThemeContext = createContext('light')
// 父组件
function App() {
return (
<ThemeContext.Provider value="dark">
<Child />
</ThemeContext.Provider>
)
}
// 子组件
function Child() {
const theme = useContext(ThemeContext)
return <div>当前主题:{theme}</div>
}const ThemeContext = createContext('light')
// 父组件
function App() {
return (
<ThemeContext.Provider value="dark">
<Child />
</ThemeContext.Provider>
)
}
// 子组件
function Child() {
const theme = useContext(ThemeContext)
return <div>当前主题:{theme}</div>
}九、Custom Hooks
自定义 Hook 是以 use 开头的函数,内部可调用其他 Hook:
jsx
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null)
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange)
return () => ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange)
})
return isOnline
}
// 使用
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id)
return <li style={{ color: isOnline ? 'green' : 'black' }}>{props.friend.name}</li>
}function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null)
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange)
return () => ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange)
})
return isOnline
}
// 使用
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id)
return <li style={{ color: isOnline ? 'green' : 'black' }}>{props.friend.name}</li>
}常用自定义 Hooks
usePersistFn
替代 useCallback,不需要添加依赖:
ts
function usePersistFn<T extends (...args: any[]) => any>(fn: T) {
const ref = useRef(fn)
ref.current = fn
const persistFn = useCallback(((...args) => ref.current(...args)) as T, [ref])
return persistFn
}function usePersistFn<T extends (...args: any[]) => any>(fn: T) {
const ref = useRef(fn)
ref.current = fn
const persistFn = useCallback(((...args) => ref.current(...args)) as T, [ref])
return persistFn
}useUpdateEffect
忽略首次执行,只在依赖更新时执行:
ts
function useUpdateEffect(effect: () => void, deps: any[]) {
const isMounted = useRef(false)
useEffect(() => {
return () => { isMounted.current = false }
}, [])
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true
} else {
return effect()
}
}, deps)
}function useUpdateEffect(effect: () => void, deps: any[]) {
const isMounted = useRef(false)
useEffect(() => {
return () => { isMounted.current = false }
}, [])
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true
} else {
return effect()
}
}, deps)
}useCacheData
缓存数据到模块级别,组件卸载再挂载时恢复数据:
ts
let cacheData: any = null
function useCacheData(defaultData: any) {
const [data, setData] = useState(() => cacheData || defaultData)
const prevData = useRef(data)
useEffect(() => { prevData.current = data })
const update = useCallback((data) => {
cacheData = data
setData(data)
}, [])
return { data, update, prevData: prevData.current }
}let cacheData: any = null
function useCacheData(defaultData: any) {
const [data, setData] = useState(() => cacheData || defaultData)
const prevData = useRef(data)
useEffect(() => { prevData.current = data })
const update = useCallback((data) => {
cacheData = data
setData(data)
}, [])
return { data, update, prevData: prevData.current }
}useSetState
类组件的 setState 合并行为:
ts
function useSetState(initialState: any) {
const [state, setState] = useState(initialState)
const setMergeState = useCallback((patch) => {
setState(prev => ({
...prev,
...(typeof patch === 'function' ? patch(prev) : patch)
}))
}, [])
return [state, setMergeState]
}function useSetState(initialState: any) {
const [state, setState] = useState(initialState)
const setMergeState = useCallback((patch) => {
setState(prev => ({
...prev,
...(typeof patch === 'function' ? patch(prev) : patch)
}))
}, [])
return [state, setMergeState]
}十、Hooks 规则
- 只在最顶层调用 Hooks — 不要在循环、条件或嵌套函数中调用
- 只在 React 函数组件或自定义 Hook 中调用 Hooks
- 组件名必须首字母大写
依赖项设置
- props、import 的变量、创建的变量在依赖项中使用了就必须设置
- 当依赖被频繁调用时,使用函数形式设置值:
jsx
useEffect(() => {
setCount(prev => prev + 1)
}, [])useEffect(() => {
setCount(prev => prev + 1)
}, [])依赖列表中不能省略函数
jsx
function Example({ someProp }) {
function doSomething() {
console.log(someProp)
}
// 不安全 — 依赖列表不完整
useEffect(() => { doSomething() }, [])
// 正确
useEffect(() => { doSomething() }, [someProp])
// 或用 useCallback 包裹
const doSomething = useCallback(() => console.log(someProp), [someProp])
useEffect(() => { doSomething() }, [doSomething])
}function Example({ someProp }) {
function doSomething() {
console.log(someProp)
}
// 不安全 — 依赖列表不完整
useEffect(() => { doSomething() }, [])
// 正确
useEffect(() => { doSomething() }, [someProp])
// 或用 useCallback 包裹
const doSomething = useCallback(() => console.log(someProp), [someProp])
useEffect(() => { doSomething() }, [doSomething])
}useReducer 解决 state 依赖 state 频繁执行
当 state 值依赖另一个 state 值且频繁执行时,使用 useReducer:
jsx
function reducer(state, [type, payload]) {
switch (type) {
case '': return {}
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
dispatch(['', action])
}, [])
}function reducer(state, [type, payload]) {
switch (type) {
case '': return {}
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
dispatch(['', action])
}, [])
}使用 ref 保存可变变量
万不得已时使用:
jsx
function Example(props) {
const latestProps = useRef(props)
useEffect(() => { latestProps.current = props })
useEffect(() => {
function tick() { console.log(latestProps.current) }
const id = setInterval(tick, 1000)
return () => clearInterval(id)
}, [])
}function Example(props) {
const latestProps = useRef(props)
useEffect(() => { latestProps.current = props })
useEffect(() => {
function tick() { console.log(latestProps.current) }
const id = setInterval(tick, 1000)
return () => clearInterval(id)
}, [])
}eslint 插件
使用 eslint-plugin-react-hooks 自动检测依赖数组:
bash
npm install eslint-plugin-react-hooksnpm install eslint-plugin-react-hooks十一、路由 Hooks
useHistory / useNavigate
jsx
// v5
import { useHistory } from 'react-router-dom'
const history = useHistory()
history.push('/home')
// v6 — useNavigate 代替 useHistory
import { useNavigate } from 'react-router-dom'
const navigate = useNavigate()
navigate('/home')
navigate('/home', { replace: true })// v5
import { useHistory } from 'react-router-dom'
const history = useHistory()
history.push('/home')
// v6 — useNavigate 代替 useHistory
import { useNavigate } from 'react-router-dom'
const navigate = useNavigate()
navigate('/home')
navigate('/home', { replace: true })useParams
jsx
import { useParams } from 'react-router-dom'
const { id } = useParams()import { useParams } from 'react-router-dom'
const { id } = useParams()useLocation
jsx
import { useLocation } from 'react-router-dom'
const location = useLocation()
// { pathname, search, hash, state }import { useLocation } from 'react-router-dom'
const location = useLocation()
// { pathname, search, hash, state }useRouteMatch
jsx
function About() {
const match = useRouteMatch('/about')
return match && <div>About</div>
}function About() {
const match = useRouteMatch('/about')
return match && <div>About</div>
}十二、Redux Hooks
useSelector — 获取 state
jsx
import { useSelector, shallowEqual } from 'react-redux'
const count = useSelector(state => state.count)
// 复杂提取,使用浅比较避免重复渲染
const data = useSelector(
state => ({ title: state.title, content: state.content }),
shallowEqual
)import { useSelector, shallowEqual } from 'react-redux'
const count = useSelector(state => state.count)
// 复杂提取,使用浅比较避免重复渲染
const data = useSelector(
state => ({ title: state.title, content: state.content }),
shallowEqual
)useDispatch — 派发 action
jsx
import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
const increment = useCallback(() => dispatch({ type: 'increment' }), [dispatch])import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
const increment = useCallback(() => dispatch({ type: 'increment' }), [dispatch])useStore
jsx
import { useStore } from 'react-redux'
const store = useStore()
store.getState()import { useStore } from 'react-redux'
const store = useStore()
store.getState()十三、useEvent(实验性)
缓存事件处理函数,始终访问最新值:
jsx
function Chat() {
const [text, setText] = useState('')
const onClick = useEvent(() => {
sendMessage(text)
})
return <SendButton onClick={onClick} />
}function Chat() {
const [text, setText] = useState('')
const onClick = useEvent(() => {
sendMessage(text)
})
return <SendButton onClick={onClick} />
}常见问题
倒计时 Hook
ts
function useCountdown(seconds: number) {
const [timeLeft, setTimeLeft] = useState(seconds)
useEffect(() => {
if (!timeLeft) return
const intervalId = setInterval(() => {
setTimeLeft(t => t - 1)
}, 1000)
return () => clearInterval(intervalId)
}, [timeLeft])
return timeLeft
}function useCountdown(seconds: number) {
const [timeLeft, setTimeLeft] = useState(seconds)
useEffect(() => {
if (!timeLeft) return
const intervalId = setInterval(() => {
setTimeLeft(t => t - 1)
}, 1000)
return () => clearInterval(intervalId)
}, [timeLeft])
return timeLeft
}多个 useEffect
一个页面可以写多个 useEffect,按功能分离:
jsx
useEffect(() => {
// 数据请求
}, [])
useEffect(() => {
// 事件监听
}, [count])useEffect(() => {
// 数据请求
}, [])
useEffect(() => {
// 事件监听
}, [count])
Cao Note