Skip to content

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 优化子组件时,必须同时满足:

  1. 子组件已通过 React.memo 缓存
  2. 子组件所有 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 规则 ​

  1. 只在最顶层调用 Hooks — 不要在循环、条件或嵌套函数中调用
  2. 只在 React 函数组件或自定义 Hook 中调用 Hooks
  3. 组件名必须首字母大写

依赖项设置 ​

  • 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-hooks
npm 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])