1

前端阅读室

Hook

简介

什么是Hook?

Hook是一些可以让你在函数组件里“钩入”React state及生命周期等特性的函数。

使用Hook的原因

  1. 在组件之间复用状态逻辑很难

React没有提供将可复用性行为“附加”到组件的途经,以前一般是通过render props和高阶组件解决的。这类方案需要重新组织你的组件结构,这可能会很麻烦。

  1. 复杂组件变得难以理解

组件被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。同一个 componentDidMount中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。
Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

  1. 难以理解的class

你必须去理解JavaScript中this的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。class也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。

使用State Hook

计数器的例子

import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

声明了一个叫count的state 变量,然后把它设为0。React会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用setCount来更新当前的count。

使用Effect Hook

Effect Hook可以让你在函数组件中执行副作用操作

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

你可以把useEffect Hook看做componentDidMount,componentDidUpdate和 componentWillUnmount这三个函数的组合。

你可能会注意到,传递给useEffect的函数在每次渲染中都可以在effect中获取最新的count的值。每次重新渲染,其实都会生成新的effect,替换掉之前的。某种意义上讲,effect更像是渲染结果的一部分——每个effect“属于”一次特定的渲染。

与componentDidMount或componentDidUpdate 不同,使用useEffect调度的effect不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect不需要同步地执行。在个别情况下(例如测量布局),有单独的useLayoutEffect Hook供你使用,其API与 useEffect相同。

需要清除的effect

如果你的effect返回一个函数,React会在组件卸载的时候调用它:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

注意,并不是必须为effect中返回的函数命名。其实也可以返回一个箭头函数或者给起一个别的名字。

使用多个Effect实现关注点分离

你也可以使用多个effect。这会将不相关逻辑分离到不同的effect中:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

React将按照effect声明的顺序依次调用组件中的每一个effect。

通过跳过Effect进行性能优化

如果某些特定值在两次重渲染之间没有发生变化,你可以通知React跳过对effect的调用,只要传递数组作为useEffect的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

如果数组中有多个元素,即使只有一个元素发生变化,React也会执行effect。
请确保数组中包含了所有外部作用域中会随时间变化并且在effect中使用的变量。
如果想执行只运行一次的effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])

Hook规则

  1. 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用Hook。遵守这条规则,你就能确保Hook在每一次渲染中都按照同样的顺序被调用。这让React能够在多次的useState和 useEffect调用之间保持hook状态的正确。
  2. 不要在普通的JavaScript函数中调用Hook

你可以:
1.在React的函数组件中调用Hook
2.在自定义Hook中调用其他Hook

自定义Hook

自定义Hook是一个函数,其名称以“use”开头,函数内部可以调用其他的Hook。

import { useState, useEffect } from 'react';

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 FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

自定义Hook是一种自然遵循Hook设计的约定,而并不是React的特性。

Hook API索引

useState补充

  1. 函数式更新
setCount(prevCount => prevCount - 1)
  1. 惰性初始state

如果初始state需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});
  1. 跳过state更新

调用State Hook的更新函数并传入当前的state时,React将跳过子组件的渲染及effect的执行。(React使用Object.is比较算法来比较state。)

useEffect补充

effect的执行时机
useEffect会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React将在组件更新前刷新上一轮渲染的effect。

useContext

const value = useContext(MyContext);

接收一个context对象(React.createContext的返回值)并返回该context的当前值。当前的context值由上层组件中距离当前组件最近的 <MyContext.Provider> 的value prop 决定。

当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重渲染,并使用最新传递给MyContext provider的context value值。即使祖先使用React.memo或shouldComponentUpdate,也会在组件本身使用useContext时重新渲染。

useReducer

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

以下是用reducer重写useState一节的计数器示例:

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: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

惰性初始化

你可以选择惰性地创建初始state。为此,需要将init函数作为useReducer的第三个参数传入,这样初始state将被设置为init(initialArg)。

跳过dispatch

如果Reducer Hook的返回值与当前state相同,React将跳过子组件的渲染及副作用的执行。(React使用Object.is比较算法来比较state。)

useCallback

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

把内联回调函数及依赖项数组作为参数传入useCallback,它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如shouldComponentUpdate)的子组件时,它将非常有用。

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

useMemo

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

把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算 memoized值。这种优化有助于避免在每次渲染时都进行高开销的计算。

useRef

const refContainer = useRef(initialValue);

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

一个常见的用例便是命令式地访问子组件:

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

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

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

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

在本例中,渲染<FancyInput ref={inputRef} />的父组件可以调用 inputRef.current.focus()。

useLayoutEffect

其函数签名与useEffect相同,但它会在所有的DOM变更之后同步调用effect。可以使用它来读取DOM布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect内部的更新计划将被同步刷新。

useDebugValue

useDebugValue(value)

useDebugValue可用于在React开发者工具中显示自定义hook的标签。

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

  // ...

  // 在开发者工具中的这个 Hook 旁边显示标签
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

推荐你向每个自定义Hook添加debug值。当它作为共享库的一部分时才最有价值。

延迟格式化debug值

useDebugValue接受一个格式化函数作为可选的第二个参数。该函数只有在Hook被检查时才会被调用。它接受debug值作为参数,并且会返回一个格式化的显示值。

useDebugValue(date, date => date.toDateString());

前端阅读室


小番茄
67 声望5 粉丝