React 进阶(八)Hook

Hook 是 React 16.8 新增的特性,为了解决函数组件不能保存状态和没有生命周期等问题。 Hook 是 JavaScript 函数,只能在函数最外层调用,不能在循环、条件判断和子函数中调用。 只能在函数组件中调用(自定义 Hook 除外)。

useState

添加状态。参数为初始化值,返回一个数组:[当前值, 设置值的函数]

import { memo, useState } from 'react'

const App = memo(() => {
  const [message, setMessage] = useState('Hello World')

  return (
    <div>
      <h2>{message}</h2>
      <button onClick={() => setMessage('Hello React')}>修改文本</button>
    </div>
  )
})

export default App

useEffect

处理副作用(网络请求、手动更新 DOM、事件监听)。

参数:一个函数(setup,可返回一个 cleanup 函数)和一个依赖数组。

import { memo, useEffect, useState } from 'react'

const App = memo(() => {
  const [counter, setCounter] = useState(0)

  useEffect(() => {
    document.title = counter
    return () => {
      // 清除副作用
    }
  }, [counter])

  useEffect(() => {
    console.log('订阅事件')
    return () => {
      console.log('取消订阅')
    }
  }, [])

  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={() => setCounter(counter - 1)}>-</button>
      <button onClick={() => setCounter(counter + 1)}>+</button>
    </div>
  )
})

export default App

useContext

简化多个 Context 的消费:

import { memo, useContext } from 'react'
import { UserContext, ThemeContext } from './context'

const App = memo(() => {
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)

  return (
    <div>
      <h2 style={{ color: theme.color }}>{user.name}</h2>
    </div>
  )
})

export default App

对比使用 Consumer 的嵌套写法:

<UserContext.Consumer>
  {(user) => (
    <ThemeContext.Consumer>
      {(theme) => <h2 style={{ color: theme.color }}>{user.name}</h2>}
    </ThemeContext.Consumer>
  )}
</UserContext.Consumer>

useReducer

import { memo, useReducer } from 'react'

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, counter: state.counter + 1 }
    case 'decrement':
      return { ...state, counter: state.counter - 1 }
    case 'add_number':
      return { ...state, counter: state.counter + action.num }
    case 'sub_number':
      return { ...state, counter: state.counter - action.num }
    default:
      return state
  }
}

const App = memo(() => {
  const [state, dispatch] = useReducer(reducer, { counter: 0 })

  return (
    <div>
      <h2>当前计数:{state.counter}</h2>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'sub_number', num: 5 })}>-5</button>
      <button onClick={() => dispatch({ type: 'add_number', num: 5 })}>+5</button>
    </div>
  )
})

export default App

useCallback

优化子组件渲染性能,返回的是一个缓存的函数

import { memo, useState, useCallback } from 'react'

const Increment = memo((props) => {
  console.log('Increment render')
  return <button onClick={props.increment}>+1</button>
})

const App = memo(() => {
  const [counter, setCounter] = useState(0)

  // 使用函数式更新,避免闭包陷阱
  const increment = useCallback(() => {
    setCounter((counter) => counter + 1)
  }, [])

  return (
    <div>
      <h2>当前计数:{counter}</h2>
      <button onClick={increment}>+1</button>
      <Increment increment={increment} />
    </div>
  )
})

export default App

useMemo

在每次重新渲染的时候缓存计算的结果

import { memo, useState, useMemo } from 'react'

function getMessage() {
  console.log('getMessage')
  return 'React Hook'
}

const App = memo(() => {
  const [counter, setCounter] = useState(0)
  const message = useMemo(() => getMessage(), [])

  return (
    <div>
      <h2>{message}</h2>
      <h2>当前计数:{counter}</h2>
      <button onClick={() => setCounter(counter + 1)}>+1</button>
    </div>
  )
})

export default App

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

useRef

返回一个 ref 对象,在组件的整个生命周期保持不变。

import { memo, useRef } from 'react'

const App = memo(() => {
  const titleRef = useRef()
  const getDOM = () => {
    console.log(titleRef.current)
  }

  return (
    <div>
      <h2 ref={titleRef}>Hello React</h2>
      <button onClick={getDOM}>获取DOM</button>
    </div>
  )
})

export default App

useImperativeHandle

自定义由 ref 暴露出来的句柄,限制父组件对子组件的操作:

import { memo, useRef, forwardRef, useImperativeHandle } from 'react'

const Child = memo(
  forwardRef((props, ref) => {
    useImperativeHandle(ref, () => ({
      focus: () => {
        console.log('focus')
      }
    }))
    return <input ref={ref} type="text" />
  })
)

const App = memo(() => {
  const inputRef = useRef()
  const getChildDOM = () => {
    inputRef.current.focus()
  }

  return (
    <div>
      <Child ref={inputRef} />
      <button onClick={getChildDOM}>获取子组件DOM</button>
    </div>
  )
})

export default App

useLayoutEffect

类似 useEffect,但会在 DOM 更新后、浏览器绘制前同步执行,会阻塞浏览器渲染。不建议频繁使用。

import { memo, useState, useLayoutEffect } from 'react'

const App = memo(() => {
  const [counter, setCounter] = useState(0)

  useLayoutEffect(() => {
    if (counter === 1) {
      setCounter(0)
    }
  }, [counter])

  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={() => setCounter(counter + 1)}>+</button>
    </div>
  )
})

export default App

自定义 Hook

自定义 Hook 必须使用 use 开头。

生命周期 Hook

import { useEffect } from 'react'

function useLogLife(name) {
  useEffect(() => {
    console.log(`${name}组件被创建`)
  })
}

export default useLogLife

全局信息 Hook

import { useContext } from 'react'
import { TokenContext } from '../context'

function useToken() {
  const token = useContext(TokenContext)
  return [token]
}

export default useToken

监听窗口滚动 Hook

import { useState, useEffect } from 'react'

function useScrollPosition() {
  const [scrollX, setScrollX] = useState(0)
  const [scrollY, setScrollY] = useState(0)

  useEffect(() => {
    function handleScroll() {
      setScrollX(window.scrollX)
      setScrollY(window.scrollY)
    }
    window.addEventListener('scroll', handleScroll)
    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  })

  return [scrollX, scrollY]
}

export default useScrollPosition

localStorage Hook

import { useState, useEffect } from 'react'

function useLocalStorage(key) {
  const [data, setData] = useState(() => {
    const item = localStorage.getItem(key)
    if (!item) return
    return JSON.parse(item)
  })

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data))
  }, [key, data])

  return [data, setData]
}

export default useLocalStorage

useId

用于生成横跨服务端和客户端的稳定唯一 ID,避免 hydration 不匹配。

import { memo, useState, useId } from 'react'

const App = memo(() => {
  const [counter, setCounter] = useState(0)
  const id = useId()
  console.log(id)

  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={() => setCounter(counter + 1)}>+</button>
    </div>
  )
})

export default App

useTransition

在不阻塞 UI 的情况下更新状态:

import { memo, useState, useTransition } from 'react'

const App = memo(() => {
  const [names, setNames] = useState(namesArray)
  const [isPending, startTransition] = useTransition()

  const valueChangeHandle = (e) => {
    const value = e.target.value
    startTransition(() => {
      const newNames = namesArray.filter((name) => name.includes(value))
      setNames(newNames)
    })
  }

  return (
    <div>
      <input type="text" onInput={valueChangeHandle} />
      <ul>
        {isPending ? <span>loading...</span> : names.map((name, index) => <li key={index}>{name}</li>)}
      </ul>
    </div>
  )
})

export default App

useDeferredValue

import { memo, useState, useDeferredValue } from 'react'

const App = memo(() => {
  const [names, setNames] = useState(namesArray)
  const deferredValue = useDeferredValue(names)

  const valueChangeHandle = (e) => {
    const value = e.target.value
    const newNames = namesArray.filter((name) => name.includes(value))
    setNames(newNames)
  }

  return (
    <div>
      <input type="text" onInput={valueChangeHandle} />
      <ul>
        {deferredValue.map((name, index) => <li key={index}>{name}</li>)}
      </ul>
    </div>
  )
})

export default App