一、API 列表

React 内置的 Hook 如下:

  • 基础 Hook:

    • useState
    • useEffect
    • useContext
  • 其他 Hook:

    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeMehtods
    • useLayoutEffect

二、基本 Hook

1、useState

const [state, setState] = useState(initialState);

useState 返回一个状态值变量,以及一个更新这个状态的方法。

在初始渲染期间,返回的 state 与作为第一个参数 initialState 传递的值相同。

setState 函数用于更新 state,它接受一个新的状态值,并将组件的 re-render 排入队列中。

setState(newState);

在后面的 re-render 期间,useState 返回的第一个值将始终是应用更新之后的最新 state。

1、通过 function 更新 state

如果使用先前 state 计算新的 state,则可以将一个函数传递个 setState。该函数将接收之前的值,并返回更新的值。下面是一个使用两种形式的 setState 的计数器组件的示例:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}

“+” 和 “-” 按钮使用 function 形式,因为更新的值基于先前的值,但 “重置” 按钮使用普通的形式,因为它总是将计数设置回0。

2、注意:

和类组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以通过将函数 updater 表单与对象扩展语法组合来复制此行为:

setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

另一个选项是使用 useReducer,它更适合管理包含多个子值的 state 对象。

3、延迟初始化

initialState 参数是初始渲染期间使用的 state,在之后的 render 中,它就被忽略了。如果初始 state 是昂贵计算的结果,则可以改为提供函数,这个函数仅在初始 render 的时候执行:

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

2、useEffect

useEffect(didUpdate);

接受一个 function 作为参数,这个 function 可能是副作用的方法。

函数组件的主体内部不允许使用 mutation、订阅、timer、日志记录以及其他的副作用(也就是在 React 的渲染阶段)。这样做会导致 UI 中的错误和不一致性混乱。

React function 组件中,应当使用 useEffect 来完成这些事情。传递给 useEffect 的函数将在渲染提交到屏幕后运行。将 effect 视为从 React 的纯粹 function 进入命令式的一种 escape hatch (逃生舱)。

默认情况下,effect 在每次完成渲染后都会执行,但是可以选择仅在某些值发生更改时触发它。

1、清理 effect

通常,effect 会创建在组件离开屏幕之前需要清理的资源,比如订阅或者计时器ID。为此,传递给 useEffect 可能会返回一个清理函数:

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清理订阅
    subscription.unsubscribe();
  };
});

clear function 在从 UI 中删除组件之后就执行,以防止内存泄露。此外,如果组件多次 rende(通常都会多次 render),则在执行下一个 effect 之前会清楚之前的 effect。在上面的示例中,每次 render 都会创建一个新的订阅。

2、effect 的时间

componentDidMountcomponentDidUpdate 不同,在延迟事件期间,传递给 useEffect 的函数在布局和绘制后触发。这使得它适用于许多常见的副作用,例如设置订阅和事件处理程序,因为大多数类型的工作不应该阻止浏览器更新屏幕。

但是,并不是所有的 effect 都可以延迟。例如,用户可见的 DOM 突变必须在下一次绘制之间同步触发,以便用户不会感觉到视觉上的不一致。(区别在概念上类似于被动事件侦听器和活动事件侦听器),对于这些类型的 effect,React 提供了一个名为 useLayoutEffect 的 附加 Hook。它与 useEffect 具有相同的签名,仅在触发时有所不同。

虽然 useEffect 延迟到浏览器绘制完成之后,但是它保证在任何新的 render 之前触发,在开始新的更新之前,React 将始终刷新先前 render 的 effect。

3、有条件的触发 effect

effect 的默认行为是在每次完成 render 后触发 effect。这样如果其中一个 输入发生变化,会始终创建 effect。

但是,在某些情况下,这可能是过度的,比如上面的订阅示例,应该仅在 prop 发生更改的时候,我们才会进行创建新的订阅。

要实现此功能,需要将第二个参数传递给 useEffect,它是 effect 所依赖的值数组:

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

上面代码中,只有在 props.source 更改的时候才会重新创建订阅。

传入一个空数组 [] 告诉 React 你的 effect 不依赖组件中的任何值,因此这个 effect 只能在 mount 的时候执行,而在 unmount 的时候清理,不会再更新的时候执行。

注意:

输入数组不作为参数传递给 effect 函数,但从概念上讲,这就是它们所代表的内容:effect 函数中引用的每个值也应该出现在输入数组中。在未来,一个足够先进的表一起可以自动创建这个数组。

3、useContext

const context = useContext(Context);

接受上下文对象(从 React.createContext 返回的值)并且返回当前的上下文值,由给定上下文的最近上下文提供程序给出。

当提供程序更新时,这个 Hook 将使用最新的上下文值触发 re-render。

二、附加 Hook

附件 Hook 实际上是上面的基础 Hook 的一种变体,也可以用于特定的边缘情况。(React 建议不作为先学习的内容)

1、useReducer

const [state, dispatch] = useReducer(reducer, initialState);

这是 useState 的替代方案,接收类型为 (state, action) => newState 的 reducer,并返回与 dispatch 方法配对的当前 state。(如果熟悉 Redux,你已经知道这是如果工作的)

下面是之前的计数器示例使用 reducer 重写的:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'reset':
      return initialState;
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      // A reducer must always return a valid state.
      // Alternatively you can throw an error if an invalid action is dispatched.
      return state;
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, {count: initialCount});
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'reset'})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

1、延迟初始化

useReducer 接收可选的第三个参数是 initialAction。如果提供了, 则在初始化渲染期间应用初始操作。这对于计算包含通过 props 传递的值的初始 state 非常有用:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'reset':
      return {count: action.payload};
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      // A reducer must always return a valid state.
      // Alternatively you can throw an error if an invalid action is dispatched.
      return state;
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(
    reducer,
    initialState,
    {type: 'reset', payload: initialCount},
  );

  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

如果涉及多个子值的复杂状态逻辑时,useReducer 通常优于 useState,它还允许优化触发深度更新的组件的性能,因为可以传递调度而不是回调。更多内容可以看 react 的博客:https://reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down

2、useCallback

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

返回一个 memoized 回调

传递内联回调和一组输入。 useCallback 将返回一个有记忆的版本,这个版本仅在其中一个输入发生更改的时候才会发生变动。当将回调传递给依赖于引用相等性的优化子组件以防止不必要的 render ,比如(shouldComponentUpdate)的时候,这非常有用。

useCallback(fn, inputs) 等效于 useMemo(() => fn, inputs).

注意:

输入数组不作为参数传递给回调。从概念上讲:回调内引用的每个值也应该出现在 inputs 数组中,将来,一个足够先进的编译器可以自动创建这个数组。

3、useMemo

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

返回一个有记忆的值。

传递 “创建” 功能和输入数组。 useMemo 只会在其中一个输入发生更改的时候重新计算 memoized 值,这个优化有助于避免在每个 render 时进行昂贵的计算。

如果没有提供数组,则只要将函数实例作为第一个参数传递,就会计算新值。(使用内联函数,在每个 render 上)

注意:

输入数组不作为参数传递给回调。从概念上讲:回调内引用的每个值也应该出现在 inputs 数组中,将来,一个足够先进的编译器可以自动创建这个数组。

4、useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传递的参数 initialValue。返回的对象将持续整个组件的生命周期。

常见的使用场景是强制访问子组件:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

请注意 useRef() 比直接使用 ref 属性有用。保持任何可变值的方式类似于在类中使用实例字段的方法。(https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables

5、useImperativeMethods

useImperativeMethods(ref, createInstance, [inputs])

useImperativeMethods 自定义使用 ref 时公开给父组件的示例值。与往常一样,在大多数情况下应避免使用 refs 的命令式代码。

useImperativeMethodsforwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeMethods(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

在上面的示例中,渲染 <FancyInput ref={fancyInputRef} /> 的父组件能够调用 fancyInputRef.current.focus()

6、useLayoutEffect

签名和 useEffect 相同,但所有的 DOM 突变后同步触发。使用它从 DOM 读取布局并同步重新渲染。在浏览器有机会绘制之前,将在 useLayoutEffect 内部计划的更新将同步刷新。

应当尽可能的选择标准 useEffect 来避免阻塞 UI 的更新。

TIP

如果你正在从 class 组件迁移代码,则 useLayoutEffect 会在与 componentDidMountcomponentDidUpdate 相同的阶段触发,因此如果不确定 Hook 需要哪种 Hook,使用 useLayoutEffect 风险是最小的。