JavaScript / 前端 / 译文 · 10月 7, 2020 0

React v16.8.x – Hooks API

本页面介绍了React Hooks 的所有API 。

如果是新了解Hooks,可以先看之前的页面。您还可以在FAQ部分找到有用的信息。

  • 基础 API
    • useState
    • useEffect
    • useContext
  • 额外的 API
    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

基础 API

useState

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

返回一个有状态的值和一个用以更新该值的函数。

在初次render期间,返回的状态值state与传递的第一个参数的值相同(initialState)。

setState函数用来更新该值。它接受一个新的状态值并将组件重新渲染。

setState(newState);

在后续重新render期间,useState返回的第一个值将始终是应用更新后的最新状态。

更新函数

如果新的state需要使用当前的state来计算,你可以给setState传递一个函数。这个函数将会接收当前的状态值,并返回更新的值。这是一个使用两种形式的setState的计数器组件的示例:

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

"+"和"-"按钮使用的是函数形式,因为它更新状态是基于当前的状态值。而重置按钮使用的是正常形式,因为他总是让计数器回归0

注意
和类组件中的setState不同,useState不会自动合并更新对象。您可以通传递函数的方式并用扩展语法来将新旧对象合并:

setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});

另一个方法是useReducer,它更适合管理包含多个子值的状态对象。

懒初始值

initialState参数是初始渲染期间使用的状态。在随后的渲染中,它被会忽略。如果初始状态是昂贵计算的结果,则可以改为传递函数,该函数仅在初始渲染时执行:

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

阻止状态更新

如果你在更新hooks状态时,传递了相同的值,React不会重新渲染子节点或者触发effect。(React使用Object.is来比较)

useEffect

useEffect(didUpdate);

接收一个函数,用以在更新后立刻执行副作用。

函数组件的主体内部不允许使用变异,订阅,计时器,日志记录和其他副作用(称为React的渲染阶段)。这样做会导致UI中的错误和不一致性混乱。

你可以使用useEffect。传递给它的函数将会在每次渲染到屏幕上之后执行。将Effect想象成将从React的纯粹功能性世界进入命令式世界的大门。

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

需要清理的副作用

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

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

清除功能会在从UI中删除组件之前运行,以防止内存泄漏。此外,如果组件渲染多次(通常如此),则在执行下一个effect之前会清除先前的effect。 在我们的示例中,这意味着每次更新都会创建一个新订阅。要避免在每次更新时触发效果,请参阅下一节。

effect 的触发节点

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

不是所有的副作用都是这样。例如,用户可见的DOM突变必须在下一次绘制之前同步触发,这样用户就不会感觉到视觉不一致。对于这些类型的效果,React提供了一个名为useLayoutEffect的额外Hook。它与useEffect具有相同的签名,仅在触发时有所不同。

虽然useEffect延迟到浏览器绘制之后,但它一定会在任何新渲染之前触发。在开始新的更新之前,React将始终刷新先前渲染的效果。

条件触发 effect

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

但是,在某些情况下,这可能有点不合实际,例如上一节中的订阅示例。只有当props数据更改时,我们才会再次创建新订阅。

要实现此功能,可以传递第二个参数给useEffect,它是effect所依赖的值数组。我们更新的示例现在看起来像这样:

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

现在,订阅只会在props.source改变时才会重新创建。

传递空数组[]意味着告诉React你的effect不依赖任何值的更新,所有只会在组件挂载时运行,并在卸载时清理,更新期间不会再执行。

注意:
数组参数不会传递给effect函数。但从概念上讲,这就是它们所代表的内容:effect函数中引用的每个状态值也应出现在输入数组中。以后,先进的编译器可以自动创建这个数组。

useContext

const context = useContext(Context);

接受一个context对象(是React.createContext的返回值)并返回当前的context值,该值由最近的发布者提供。

发布者更新时,这个Hook会触发一个具有最新context值的rerender。

额外的Hooks

以下Hook可以是上一节中基本Hook的变体,也可以仅用于特定边缘情况。不必预先学习它们。

useReducer

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

useState 的替代方案。接收一个类型为(state, action) => newState的 reducer ,并返回当前的值和改变该值的dispatch方法。

当你有复杂的状态逻辑涉及多个子值或下一个状态依赖于前一个时,你就可以用useReducer替代useState。useReducer还允许您优化触发深度更新的组件的性能,因为你可以传递dispatch而不是回调函数。

这是useState部分的计数器示例,重写为使用reducer

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({initialState}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

指定初始状态

这里有两种不同的方法来初始化useReducer状态。您可以根据用例选择其中一个。将初始状态作为第二个参数传递的最简单方法:

 const [state, dispatch] = useReducer(
    reducer,
    {count: initialCount}
  );

注意:
React不使用Redux推荐的state = initialState参数约定。初始值有时需要依赖于props,因此应在Hook调用中指定。如果您对此感到不满,可以调用useReducer(reducer,undefined,reducer)来模拟Redux行为,但不鼓励它。

延迟初始化

您还可以延迟创建初始状态。为此,您可以将init函数作为第三个参数传递。初始状态将设置为init(initialArg)

它允许您提取用于计算reducer外部的初始状态的逻辑。这对于稍后重置状态以响应操作也很方便:

function init(initialCount) {
  return {count: initialCount};
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  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>
    </>
  );
}

更新机制

如果从Reducer Hook返回与当前状态相同的值,则React将退出而不渲染子节点或触发effect。 (React使用Object.is比较算法。)

useCallback

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

返回一个记忆化(缓存返回值的技术)回调函数。

传递一个回调函数和控制数组。useCallback将返回一个记忆化版本,该版本仅在其中一个输入发生更改时才会更改。当使用回调函数优化组件渲染时这非常有用,返回结果一致将不会触发重绘。(例如:shouldComponentUpdate)。

useCallback(fn, inputs) 等价于 useMemo(() => fn, inputs)

注意:
输入数组不作为参数传递给回调函数。但是,这就是它们所代表的内容:回调中引用的每个值也应该出现在输入数组中。以后,先进的编译器可以自动创建这个数组。

useMemo

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

返回一个记忆化的值。

需要传递一个创建该值的函数和控制数组。useMemo只会在其中一个输入发生变化时重新计算记忆化值。此优化有助于避免在每个渲染上进行昂贵的计算。

请记住,传递给useMemo的函数在渲染期间运行。不要做那些在渲染时通常不会做的事情。例如:副作用。

如果未提供控制数组,则只要将新函数实例作为第一个参数传递,就会计算新值。 (在每次渲染时调用内联函数)。

您可以依赖useMemo进行性能优化,而不是语义保证。在将来,React可能会选择可以“忘记”一些以前记忆的值,并在下一次渲染时重新计算它们,例如为屏幕外组件释放内存。编写代码时要保证在没有useMemo的情况下仍可正常工作 – 然后添加它以优化性能。

注意:
输入数组不作为参数传递给回调函数。但是,这就是它们所代表的内容:回调中引用的每个值也应该出现在输入数组中。以后,先进的编译器可以自动创建这个数组。

useRef

const refContainer = useRef(initialValue);

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

常见的用例是强制访问一个子组件:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向了已挂载的input元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

注意,useRef()ref属性更有用。保存任何可变值的方法类似于在类中使用实例字段。

useImperativeHandle

useImperativeHandle(ref, createHandle, [inputs])}

useImperativeHandle是自定义使用ref时暴露给父组件的实例值。与往常一样,在大多数情况下应避免使用refs的命令式代码。 useImperativeHandle应与forwardRef一起使用:

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

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

useLayoutEffect

签名与useEffect相同,但在它会在所有DOM突变后同步触发。这样就可以从DOM读取布局并同步重新渲染。在浏览器有可能绘制之前,将会同步执行useLayoutEffect内部更新计划。

尽可能选用标准useEffect以避免视窗停顿。

注意:
如果您正在从类组件迁移代码,则useLayoutEffect会在与componentDidMountcomponentDidUpdate相同的阶段触发,因此如果您不确定使用哪个effect hook,则可能风险最小。

useDebugValue

useDebugValue(value)

useDebugValue可用于在React DevTools中显示自定义挂钩的标签。

例如,参考“构建自己的hooks”中描述的useFriendStatus自定义Hook:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // ...

  // 在 DevTools 中,该 hooks 会增加一个显示标签
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

注意:
我们不建议将调试值添加到每个自定义Hook。对于共享库一部分的自定义Hook,它相当有用。

推迟格式化调试值

在某些情况下,格式化显示值可能是一项昂贵的操作。除非实际检查Hook,否则也没有必要。

因此,useDebugValue接受格式化函数作为可选的第二个参数。只有在检查hooks时才会调用此功能。它接收调试值作为参数,并应返回格式化的显示值。

例如,自定义Hook返回Date值,可以通过传递以下格式化程序来避免不必要地调用toDateString函数:

useDebugValue(date, date => date.toDateString());
冀ICP备19028007号