Hooks 支持在函数组件中使用(hook into)state、生命周期等 React 特性,Hooks 出现以前这些特性只有 class 组件才有。

使用 Hooks 的优点:

  1. 在不改变组件层级的情况下复用状态逻辑(stateful logic)。
    • Hooks 出现之前的解决方案有 render propshigher-order components
    • Hooks 复用的是状态逻辑,而不是状态本身,每次调用 Hooks 生成的状态是完全隔离的。
  2. 相关的逻辑合并到同一个方法中。
    • Hooks 出现之前相关的逻辑被迫分散在不同的生命周期方法中,一个生命周期方法里充斥了不相关的逻辑,不同生命周期方法里出现重复逻辑。
  3. 使用函数组件的写法,易上手,和 React 的发展方向一致。

useState

const [state, setState] = useState(initialState)

通常,函数中声明的变量在函数调用(调用函数组件重新渲染页面)结束后就会被销毁,但 useState 注入的状态在两次渲染之间会保留下来。

useState 只在第一次渲染组件时创建状态,后续渲染时则是返回状态的最新值。

setState 函数不会随着重新渲染发生变化(identity is stable)。

若新的状态需要基于前一次的状态计算,则可以传一个函数到 setState
If your update function returns the exact same value as the current state, the subsequent rerender will be skipped completely.
If you update a State Hook to the same value as the current state or return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree.
If you’re doing expensive calculations while rendering, you can optimize them with useMemo.

React 采用 <code>Object.is</code> 作为比较算法。

3 类比较操作的简要区别(对 primitives 的处理方式不同):

  1. ==:比较的类型不同时会做类型转换,对 NaN-0+0 做特殊处理;
    • NaN == NaN false
    • -0 == +0 true
  2. ===:比较的类型不同时不会做类型转换,其它行为和 == 一致;类型不同直接返回 false
    • NaN !== NaN false
    • -0 === +0 true
  3. Object.is:不对 NaN-0+0 做特殊处理,其它行为和 === 一致。
    • Object.is(NaN, NaN) true
    • Object.is(-0, +0) false

initialState 若是耗时计算的结果,则可以传递一个函数作为参数,这样一来函数只会在第一次渲染时执行:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
})

更新 state 时会整个替换而不是合并,这一点和 class 组件的 this.setState 不同。

通常将倾向于一起发生变化的状态划分到一个 state 中。

useEffect

Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.

Unlike componentDidMount and componentDidUpdate, the function passed to useEffect fires after layout and paint, during a deferred event. This makes it suitable for the many common side effects, like setting up subscriptions and event handlers, because most types of work shouldn’t block the browser from updating the screen.

Although useEffect is deferred until after the browser has painted, it’s guaranteed to fire before any new renders. React will always flush a previous render’s effects before starting a new update.

默认情况下,useEffect 中指定的副作用在每次 DOM 渲染完成(“after the render is committed to the screen”,包括第一次渲染和组件更新后的渲染)后都会被执行。

React 会记住 useEffect 中的方法,在渲染完成后(函数型组件已返回)仍能执行该方法。

多次渲染同一个组件时,传给 useEffect 的方法不是同一个而是全新的一个方法,这是有意为之的设计,可以避免使用过期的数据和 class 组件中常出现的因漏写 componentDidUpdate 而引起的 bug。

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    )
}
// 没有 componentDidUpdate 之前,组件加载完毕后若 this.props.friend.id 变化时,
// 页面展示的仍然是 id 变化之前的数据,组件卸载时取消订阅的操作也会指向错误的 id,
// 可能造成内存泄漏或崩溃。
componentDidUpdate(prevProps) {
    // Unsubscribe from the previous friend.id
    ChatAPI.unsubscribeFromFriendStatus(
        prevProps.friend.id,
        this.handleStatusChange
    );
    // Subscribe to the next friend.id
    ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    );
}
componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    )
}

// hooks 的等价写法
function FriendStatus(props) {
    useEffect(() => {
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        }
    })
}
// 订阅和取消订阅的调用时序示例:
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange)      // Run first effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange)  // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange)      // Run next effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange)  // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange)      // Run next effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

useEffect 执行下一次副作用之前,会用上一次的清理方法(若有)将上一次的副作用清理掉。

React 执行清理函数的时间点:

  1. 组件卸载时;
  2. 组件再一次执行副作用函数之前(清理上一次的副作用)。

useEffect 可选的第 2 个参数是一个数组,数组不为空时,数组内的任一元素发生变化(包括组件第一次加载和后续数组元素更新)时才会执行副作用;数组为空时,仅在组件挂载时执行副作用,组件卸载时执行清理。

副作用中用到组件作用域内的所有参与到 React 数据流中的值(包括 propsstate 和从它们衍生出来的值)都应放入 useEffect 的第 2 个数组参数内,否则有可能引用到以往渲染中的过期值。
useEffect, useLayoutEffect, useMemo, useCallback, useImperativeHandle 这几个方法都需要这样。

function Counter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const id = setInterval(() => {
      // 这里通过闭包访问 count,组件挂载后 count 值是 0,useEffect 只执行一次,
      // 它能访问到的 count 永远是 1,不会更新,每一次都是 setCount(0 + 1),
      // 更新在下一次渲染才会发生,但下一次渲染对应的是新的一个 useEffect、新的 count 变量。
      // 解决办法可以是把 count 加入依赖数组,或者是 setCount(c => c+1) 避免使用闭包访问过期的 count
      setCount(count + 1) 
    }, 1000)
    return () => clearInterval(id)
  }, []) // Bug: `count` is not specified as a dependency
  return <h1>{count}</h1>
}

The array of dependencies is not passed as arguments to the effect function. Conceptually, though, that’s what they represent: every value referenced inside the effect function should also appear in the dependencies array. In the future, a sufficiently advanced compiler could create this array automatically.

使用 hooks 的规则

只在 React 函数组件的顶层调用 hooks,不要在循环语句、条件语句和嵌套函数中调用 hooks。React 函数组件中可以使用多个 hooks,React 通过 hooks 的调用顺序来确定多个状态和 useState 之间的归属关系,所以在多次渲染之间 hooks 的调用顺序需要保持一致,否则会造成归属关系的错乱引入 bug。

// ------------
// First render
// ------------
useState('Mary')           // 1. Initialize the name state variable with 'Mary'
useEffect(persistForm)     // 2. Add an effect for persisting the form
useState('Poppins')        // 3. Initialize the surname state variable with 'Poppins'
useEffect(updateTitle)     // 4. Add an effect for updating the title
// -------------
// Second render
// -------------
useState('Mary')           // 1. Read the name state variable (argument is ignored)
useEffect(persistForm)     // 2. Replace the effect for persisting the form
useState('Poppins')        // 3. Read the surname state variable (argument is ignored)
useEffect(updateTitle)     // 4. Replace the effect for updating the title

不要在普通 JS 函数中调用 hooks,只在 React 函数组件和自定义 hooks 中调用,这样可以保证组件的状态逻辑对于 hooks 都是清晰可见的。

自定义 hooks

自定义 hooks 是名称以 use 为前缀且紧跟一个大写字母的 JS 函数,在它内部可以调用其它 hooks。自定义 hooks 的函数签名可以是任意形式,use 作为前缀可以快速判断使用时是否满足 hooks 的使用规则。

当需要在两个普通 JS 函数之间共享逻辑时,可以把逻辑抽象第三个函数,自定义 hooks 的思想也是如此。

很显然,可以在 hooks 之间传递数据。

useContext

useContext 接收一个 React.createContext 创建的对象作为参数,返回该 context 的当前值,该值由最近的 <SomeContext.Provider> 祖先决定。当最近的 <SomeContext.Provider> 祖先更新时,使用 useContext 的组件一定会用该 context 的最新值重新渲染。useContext 相当于 context 的消费者。

context API

const MyContext = React.createContext(defaultValue)

defaultValue 只在组件的祖宗节点没有对应 context 的提供者时会被读取到。

每个 context 对象都有一个 Provider 组件,Provider 接收一个 value 作为 props,供其它组件来消费并订阅 context 的变化。

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update.

export const themes = {
  light: {},
  dark: {},
}
const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
})

function App() {
    const [theme, toggleTheme] = useState(themes.light)
    return (
        <ThemeContext.Provider value={{theme, toggleTheme}}>
            <Comp />
        <ThemeContext.Provider />
    )
}

function Comp() {
    const {theme, toggleTheme} = useContext(ThemeContext)
}

useRef

const refContainer = useRef(initialValue)

useRef 返回一个对象,该对象的 current 字段被初始化为 initialValue,该对象在组件的整个生命周期都存在(都是同一个)。

useRef 的常用场景是命令式地访问 DOM 元素或 React 元素和维护一个对象。
Refs and the DOM

useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.

Conceptually, you can think of refs as similar to instance variables in a class. Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

useMemo 只会在数组中依赖的元素发生变化时才会重新计算 memoizedValue,避免每次渲染都执行复杂的运算。若依赖的数组为空,则每次渲染都会执行。

useMemo 中的方法在渲染的过程中执行

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
)

useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init)

React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.

情境

  1. Run an effect only on updates: use a mutable ref to manually store a boolean value corresponding to whether you are on the first or a subsequent render, then check that flag in your effect. (If you find yourself doing this often, you could create a custom Hook for it.)

  2. Get the previous props or state:

function Counter() {
  const [count, setCount] = useState(0)
  const prevCountRef = useRef()
  useEffect(() => {
    prevCountRef.current = count
  })
  const prevCount = prevCountRef.current // 渲染完成后 current 的值才被更新,current 更新不会触发重新渲染
  return <h1>Now: {count}, before: {prevCount}</h1>
}
  1. Implement getDerivedStateFromProps:
function ScrollView({row}) {
  const [isScrollingDown, setIsScrollingDown] = useState(false);
  const [prevRow, setPrevRow] = useState(null);
  if (row !== prevRow) { // props.row 更新
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }
  return `Scrolling down: ${isScrollingDown}`;
}

You Probably Don&rsquo;t Need Derived State


References