React Hooks 学习笔记

引言

2019 年开始,在使用React的时候,已经逐步从 Class 组件,过渡到函数组件了。虽然函数组件十分便捷,可是在使用函数组件的时候,仍是有一些疑惑的地方,致使有时候会出现一些奇奇怪怪的问题。在这里,我想经过官网和博客文章以及本身的一些积累,整理下最佳实践,以备不时之需。html


Hook 不会影响你对 React 概念的理解。 偏偏相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。稍后咱们将看到,Hook 还提供了一种更强大的方式来组合他们。node


下面是一些官方列举的,提出并使用 hooks 的一些动机。react


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


你可使用 Hook 从组件中提取状态逻辑,使得这些逻辑能够单独测试并复用。Hook 使你在无需修改组件结构的状况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。git


复杂组件变得难以理解

useEffectgithub

咱们常常维护一些组件,组件起初很简单,可是逐渐会被状态逻辑和反作用充斥。每一个生命周期经常包含一些不相关的逻辑。例如,组件经常在 componentDidMountcomponentDidUpdate 中获取数据。可是,同一个 componentDidMount 中可能也包含不少其它的逻辑,如设置事件监听,而以后需在 componentWillUnmount 中清除。相互关联且须要对照修改的代码被进行了拆分,而彻底不相关的代码却在同一个方法中组合在一块儿。如此很容易产生 bug,而且致使逻辑不一致。npm


在多数状况下,不可能将组件拆分为更小的粒度,由于状态逻辑无处不在。这也给测试带来了必定挑战。同时,这也是不少人将 React 与状态管理库结合使用的缘由之一。可是,这每每会引入了不少抽象概念,须要你在不一样的文件之间来回切换,使得复用变得更加困难。编程


为了解决这个问题,Hook 将组件中相互关联的部分拆分红更小的函数(好比设置订阅或请求数据),而并不是强制按照生命周期划分。你还可使用 reducer 来管理组件的内部状态,使其更加可预测。json


难以理解的 class


除了代码复用和代码管理会遇到困难外,咱们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工做方式,这与其余语言存在巨大差别。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码很是冗余。你们能够很好地理解 props,state 和自顶向下的数据流,但对 class 却束手无策。即使在有经验的 React 开发者之间,对于函数组件与 class 组件的差别也存在分歧,甚至还要区分两种组件的使用场景。

为了解决这些问题,Hook 使你在非 class 的状况下可使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。api


官方Hooks


useState

useState 是咱们要学习的第一个 “Hook”,它的用途是管理状态。数组


const [state, setState] = useState(initialState);复制代码

返回一个 state,以及更新 state 的函数。

在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次从新渲染加入队列。

setState(newState);复制代码

在后续的从新渲染中,useState 返回的第一个值将始终是更新后最新的 state。

注意

React 会确保 setState 函数的标识是稳定的,而且不会在组件从新渲染时发生变化。这就是为何能够安全地从 useEffectuseCallback 的依赖列表中省略 setState

不会自动合并更新对象

与 class 组件中的 setState 方法不一样,useState 不会自动合并更新对象。你能够用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

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

useReducer 是另外一种可选方案,它更适合用于管理包含多个子值的 state 对象。

惰性初始 state

initialState 参数只会在组件的初始渲染中起做用,后续渲染时会被忽略。若是初始 state 须要经过复杂计算得到,则能够传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

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


useEffect

useEffect 就是一个 Effect Hook,给函数组件增长了操做反作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具备相同的用途,只不过被合并成了一个 API。


当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“反作用”函数。因为反作用函数是在组件内声明的,因此它们能够访问到组件的 props 和 state。默认状况下,React 会在每次渲染后调用反作用函数 —— 包括第一次渲染的时候。

反作用函数还能够经过返回一个函数来指定如何“清除”反作用。可是,这个清除做用,只有当组件被干掉了,才会触发。


无需清除的 effect

有时候,咱们只想在 React 更新 DOM 以后运行一些额外的代码。好比发送网络请求,手动变动 DOM,记录日志,这些都是常见的无需清除的操做。由于咱们在执行完这些操做以后,就能够忽略他们了。

image.png


useEffect 作了什么? 经过使用这个 Hook,你能够告诉 React 组件须要在渲染后执行某些操做。React 会保存你传递的函数(咱们将它称之为 “effect”),而且在执行 DOM 更新以后调用它。


为何在组件内部调用 useEffectuseEffect 放在组件内部让咱们能够在 effect 中直接访问 count state 变量(或其余 props)。咱们不须要特殊的 API 来读取它 —— 它已经保存在函数做用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的状况下,还引入特定的 React API。


useEffect 会在每次渲染后都执行吗? 是的,默认状况下,它在第一次渲染以后

每次更新以后都会执行。(咱们稍后会谈到 如何控制它。)你可能会更容易接受 effect 发生在“渲染以后”这种概念,不用再去考虑“挂载”仍是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

提示

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


须要清除的 effect

以前,咱们研究了如何使用不须要清除的反作用,还有一些反作用是须要清除的。例如订阅外部数据源。这种状况下,清除工做是很是重要的,能够防止引发内存泄露!

image.png


为何要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每一个 effect 均可以返回一个清除函数。如此能够将添加和移除订阅的逻辑放在一块儿。它们都属于 effect 的一部分。


React 什么时候清除 effect? React 会在组件卸载的时候执行清除操做。正如以前学到的,effect 在每次渲染的时候都会执行。这就是为何 React

在执行当前 effect 以前对上一个 effect 进行清除。咱们稍后将讨论 为何这将助于避免 bug以及 如何在遇到性能问题时跳过此行为


每次更新的时候都要运行 Effect

在下面这个 Effect 中,咱们会在每次 props.friend.id 更新的时候,解除以前订阅的函数,并用新的状态 props.friend.id 订阅函数。

image.png


并不须要特定的代码来处理更新逻辑,由于 useEffect

默认
就会处理。它会在调用一个新的 effect 以前对前一个 effect 进行清理。为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操做调用序列:

image.png


经过跳过 Effect 进行性能优化


在某些状况下,每次渲染后都执行清理或者执行 effect 可能会致使性能问题。在 class 组件中,咱们能够经过在 componentDidUpdate 中添加对 prevPropsprevState 的比较逻辑解决:


image.png


这是很常见的需求,因此它被内置到了 useEffect 的 Hook API 中。若是某些特定值在两次重渲染之间没有发生变化,你能够通知 React 跳过对 effect 的调用,只要传递数组做为 useEffect 的第二个可选参数便可:

image.png

注意:

若是你要使用此优化方式,请确保数组中包含了全部外部做用域中会随时间变化而且在 effect 中使用的变量,不然你的代码会引用到先前渲染中的旧变量。参阅文档,了解更多关于如何处理函数以及数组频繁变化时的措施内容。


若是想执行只运行一次的 effect(仅在组件挂载和卸载时执行),能够传递一个空数组([])做为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,因此它永远都不须要重复执行。这并不属于特殊状况 —— 它依然遵循依赖数组的工做方式。


若是你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 [] 做为第二个参数更接近你们更熟悉的 componentDidMountcomponentWillUnmount 思惟模式,但咱们有更好的方式来避免过于频繁的重复调用 effect。除此以外,请记得 React 会等待浏览器完成画面渲染以后才会延迟调用 useEffect,所以会使得额外操做很方便。


咱们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

useContext

const value = useContext(MyContext);复制代码

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

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

别忘记 useContext 的参数必须是

context 对象自己

  • 正确: useContext(MyContext)
  • 错误: useContext(MyContext.Consumer)
  • 错误: useContext(MyContext.Provider)

调用了 useContext 的组件总会在 context 值变化时从新渲染。若是重渲染组件的开销较大,你能够 经过使用 memoization 来优化

提示

若是你在接触 Hook 前已经对 context API 比较熟悉,那应该能够理解,useContext(MyContext) 至关于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>

useContext(MyContext) 只是让你可以

读取
context 的值以及订阅 context 的变化。你仍然须要在上层组件树中使用 <MyContext.Provider> 来为下层组件
提供
context。

把以下代码与 Context.Provider 放在一块儿

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};
const ThemeContext = React.createContext(themes.light);
function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
function ThemedButton() {
  const theme = useContext(ThemeContext);  return (    <button style={{ background: theme.background, color: theme.foreground }}>      I am styled by theme context!    </button>  );
}复制代码

对先前 Context 高级指南中的示例使用 hook 进行了修改,你能够在连接中找到有关如何 Context 的更多信息。


useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);复制代码

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(若是你熟悉 Redux 的话,就已经知道它如何工做了。)

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于以前的 state 等。而且,使用 useReducer 还能给那些会触发深更新的组件作性能优化,由于你能够向子组件传递 dispatch 而不是回调函数

如下是用 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>
    </>
  );
}复制代码

注意

React 会确保 dispatch 函数的标识是稳定的,而且不会在组件从新渲染时改变。这就是为何能够安全地从 useEffectuseCallback 的依赖列表中省略 dispatch

指定初始 state

有两种不一样初始化 useReducer state 的方式,你能够根据使用场景选择其中的一种。将初始 state 做为第二个参数传入 useReducer 是最简单的方法:

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

注意

React 不使用 state = initialState 这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,所以须要在调用 Hook 时指定。若是你特别喜欢上述的参数约定,能够经过调用 useReducer(reducer, undefined, reducer) 来模拟 Redux 的行为,但咱们不鼓励你这么作。

惰性初始化

你能够选择惰性地建立初始 state。为此,须要将 init 函数做为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

这么作能够将用于计算 state 的逻辑提取到 reducer 外部,这也为未来对重置 state 的 action 作处理提供了便利:

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

useCallback

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

返回一个 memoized 回调函数。

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

useCallback(fn, deps) 至关于 useMemo(() => fn, deps)

注意

依赖项数组不会做为参数传给回调函数。虽然从概念上来讲它表现为:全部回调函数中引用的值都应该出如今依赖项数组中。将来编译器会更加智能,届时自动建立数组将成为可能。

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);复制代码

返回一个 memoized 值。

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

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操做,诸如反作用这类的操做属于 useEffect 的适用范畴,而不是 useMemo

若是没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

你能够把 useMemo 做为性能优化的手段,但不要把它当成语义上的保证。未来,React 可能会选择“遗忘”之前的一些 memoized 值,并在下次渲染时从新计算它们,好比为离屏组件释放内存。先编写在没有 useMemo 的状况下也能够执行的代码 —— 以后再在你的代码中添加 useMemo,以达到优化性能的目的。

注意

依赖项数组不会做为参数传给“建立”函数。虽然从概念上来讲它表现为:全部“建立”函数中引用的值都应该出如今依赖项数组中。将来编译器会更加智能,届时自动建立数组将成为可能。

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>
    </>
  );
}复制代码

本质上,useRef 就像是能够在其 .current 属性中保存一个可变值的“盒子”。

你应该熟悉 ref 这一种访问 DOM 的主要方式。若是你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则不管该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

然而,useRef()ref 属性更有用。它能够很方便地保存任何可变值,其相似于在 class 中使用实例字段的方式。

这是由于它建立的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的惟一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef

不会
通知你。变动 .current 属性不会引起组件从新渲染。若是想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则须要使用 回调 ref 来实现。

回调Ref(测量DOM)

获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另外一个节点,React 就会调用 callback。这里有一个 小 demo:

function MeasureExample() {
  const [height, setHeight] = useState(0);
  const measuredRef = useCallback(node => {    
        if (node !== null) {      
        setHeight(node.getBoundingClientRect().height);    
        }  
    }, 
    []);
  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}复制代码

在这个案例中,咱们没有选择使用 useRef,由于当 ref 是一个对象时它并不会把当前 ref 的值的

变化
通知到咱们。使用 callback ref 能够确保 即使子组件延迟显示被测量的节点 (好比为了响应一次点击),咱们依然可以在父组件接收到相关的信息,以便更新测量结果。

注意到咱们传递了 [] 做为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,所以 React 不会在非必要的时候调用它。

本例中,仅在组件挂载和卸载时调用回调ref,由于 <h1>组件在每次从新渲染时,高度都不会变化,因此不须要触发回调ref。若是你但愿在组件调整大小时获得通知,可使用ResizeObserver或使用其余第三方的Hooks。
若是你愿意,你能够 把这个逻辑抽取出来做为 一个可复用的 Hook:

function MeasureExample() {
  const [rect, ref] = useClientRect();  return (
    <>
      <h1 ref={ref}>Hello, world</h1>
      {rect !== null &&
        <h2>The above header is {Math.round(rect.height)}px tall</h2>
      }
    </>
  );
}
function useClientRect() {
  const [rect, setRect] = useState(null);
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect());
    }
  }, []);
  return [rect, 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 内部的更新计划将被同步刷新。

尽量使用标准的 useEffect 以免阻塞视觉更新。


提示

若是你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则须要注意 useLayoutEffectcomponentDidMountcomponentDidUpdate 的调用阶段是同样的。可是,咱们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect

若是你使用服务端渲染,请记住,

不管
useLayoutEffect
仍是
useEffect 都没法在 Javascript 代码加载完成以前执行。这就是为何在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,须要将代码逻辑移至 useEffect 中(若是首次渲染不须要这段逻辑的状况下),或是将该组件延迟到客户端渲染完成后再显示(若是直到 useLayoutEffect 执行以前 HTML 都显示错乱的状况下)。

若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,能够经过使用 showChild && <Child /> 进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展现组件。这样,在客户端渲染完成以前,UI 就不会像以前那样显示错乱了。

useDebugValue


useDebugValue(value)复制代码


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

例如,“自定义 Hook” 章节中描述的名为 useFriendStatus 的自定义 Hook:


function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  // ...
  // 在开发者工具中的这个 Hook 旁边显示标签  
  // e.g. "FriendStatus: Online"  
  useDebugValue(isOnline ? 'Online' : 'Offline');
  return isOnline;
}复制代码

最佳实践

其实不只是对象,函数在每次渲染时也是独立的。这就是 Capture Value 特性。

在依赖列表中省略函数是否安全?

通常来讲,不安全。

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);  }
  useEffect(() => {
    doSomething();
  }, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)}复制代码

要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为何 一般你会想要在 effect

内部
去声明它所须要的函数。 这样就能容易的看出那个 effect 依赖了组件做用域中的哪些值:

function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);    }
    doSomething();
  }, [someProp]); // ✅ 安全(咱们的 effect 仅用到了 `someProp`)}复制代码

若是这样以后咱们依然没用到组件做用域中的任何值,就能够安全地把它指定为 []

useEffect(() => {
  function doSomething() {
    console.log('hello');
  }
  doSomething();
}, []); // ✅ 在这个例子中是安全的,由于咱们没有用到组件做用域中的 *任何* 值复制代码

根据你的用例,下面列举了一些其余的办法。

注意

咱们提供了一个 exhaustive-deps ESLint 规则做为 eslint-plugin-react-hooks 包的一部分。它会帮助你找出没法一致地处理更新的组件。

让咱们来看看这有什么关系。

若是你指定了一个 依赖列表 做为 useEffectuseMemouseCallbackuseImperativeHandle 的最后一个参数,它必须包含回调中的全部值,并参与 React 数据流。这就包括 props、state,以及任何由它们衍生而来的东西。

只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。下面这个案例有一个 Bug:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  async function fetchProduct() {
    const response = await fetch('http://myapi/product/' + productId); // 使用了 productId prop    const json = await response.json();
    setProduct(json);
  }
  useEffect(() => {
    fetchProduct();
  }, []); // 🔴 这样是无效的,由于 `fetchProduct` 使用了 `productId`  // ...
}复制代码

推荐的修复方案是把那个函数移动到你的 effect

内部
。这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  useEffect(() => {
    // 把这个函数移动到 effect 内部后,咱们能够清楚地看到它用到的值。    async function fetchProduct() {      const response = await fetch('http://myapi/product/' + productId);      const json = await response.json();      setProduct(json);    }
    fetchProduct();
  }, [productId]); // ✅ 有效,由于咱们的 effect 只用到了 productId  // ...
}复制代码

这同时也容许你经过 effect 内部的局部变量来处理无序的响应:

useEffect(() => {
    let ignore = false;    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId);
      const json = await response.json();
      if (!ignore) setProduct(json);    }
    fetchProduct();
    return () => { ignore = true };  }, [productId]);复制代码

咱们把这个函数移动到 effect 内部,这样它就不用出如今它的依赖列表中了。

提示

看看 这个小 demo这篇文章 来了解更多关于如何用 Hook 进行数据获取。

若是处于某些缘由你

没法
把一个函数移动到 effect 内部,还有一些其余办法:

  • 你能够尝试把那个函数移动到你的组件以外。那样一来,这个函数就确定不会依赖任何 props 或 state,而且也不用出如今依赖列表中了。
  • 若是你所调用的方法是一个纯计算,而且能够在渲染时调用,你能够 转而在 effect 以外调用它, 并让 effect 依赖于它的返回值。
  • 万不得已的状况下,你能够 把函数加入 effect 的依赖但
    把它的定义包裹
    useCallback Hook。这就确保了它不随渲染而改变,除非
    它自身
    的依赖发生了改变:
function ProductPage({ productId }) {
  // ✅ 用 useCallback 包裹以免随渲染发生改变  const fetchProduct = useCallback(() => {    // ... Does something with productId ...  }, [productId]); // ✅ useCallback 的全部依赖都被指定了
  return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
  useEffect(() => {
    fetchProduct();
  }, [fetchProduct]); // ✅ useEffect 的全部依赖都被指定了
  // ...
}复制代码

注意在上面的案例中,咱们 须要 让函数出如今依赖列表中。这确保了 ProductPageproductId prop 的变化会自动触发 ProductDetails 的从新获取。

如何从 useCallback 读取一个常常变化的值?

注意

咱们推荐 在 context 中向下传递 dispatch 而非在 props 中使用独立的回调。下面的方法仅仅出于文档完整性考虑,以及做为一条出路在此说起。

同时也请注意这种模式在 并行模式 下可能会致使一些问题。咱们计划在将来提供一个更加合理的替代方案,但当下最安全的解决方案是,若是回调所依赖的值变化了,老是让回调失效。

在某些罕见场景中,你可能会须要用 useCallback 记住一个回调,但因为内部函数必须常常从新建立,记忆效果不是很好。若是你想要记住的函数是一个事件处理器而且在渲染期间没有被用到,你能够 把 ref 当作实例变量 来用,并手动把最后提交的值保存在它当中:

function Form() {
  const [text, updateText] = useState('');
  const textRef = useRef();
  useEffect(() => {
    textRef.current = text; // 把它写入 ref  });
  const handleSubmit = useCallback(() => {
    const currentText = textRef.current; // 从 ref 读取它    alert(currentText);
  }, [textRef]); // 不要像 [text] 那样从新建立 handleSubmit
  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}复制代码

这是一个比较麻烦的模式,但这表示若是你须要的话你能够用这条出路进行优化。若是你把它抽取成一个自定义 Hook 的话会更加好受些:

function Form() {
  const [text, updateText] = useState('');
  // 即使 `text` 变了也会被记住:
  const handleSubmit = useEventCallback(() => {    alert(text);
  }, [text]);
  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}
function useEventCallback(fn, dependencies) {  const ref = useRef(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });
  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);
  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}复制代码

不管如何,咱们都 不推荐使用这种模式 ,只是为了文档的完整性而把它展现在这里。相反的,咱们更倾向于 避免向下深刻传递回调

绕过 Capture Value

利用 useRef

利用 useRef 就能够绕过 Capture Value 的特性。能够认为 ref 在全部 Render 过程当中保持着惟一引用,所以全部对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每一个 Render 间存在隔离。

function Example() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);
  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });
  // ...
}复制代码

也能够简洁的认为,ref 是 Mutable 的,而 state 是 Immutable 的。

useState 的回调模式

上述例子使用了 count,然而这样的代码很别扭,由于你在一个只想执行一次的 Effect 里依赖了外部变量。

既然要诚实,那只好 想办法不依赖外部变量

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);复制代码

setCount 还有一种函数回调模式,你不须要关心当前值是什么,只要对 “旧的值” 进行修改便可。这样虽然代码永远运行在第一次 Render 中,但老是能够访问到最新的 state

利用 useReducer 函数

将更新与动做解耦就能够了:

const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;
useEffect(() => {
  const id = setInterval(() => {
    dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
  }, 1000);
  return () => clearInterval(id);
}, [dispatch]);复制代码

这就是一个局部 “Redux”,因为更新变成了 dispatch({ type: "tick" }) 因此无论更新时须要依赖多少变量,在调用更新的动做里都不须要依赖任何变量。 具体更新操做在 reducer 函数里写就能够了。在线 Demo

Dan 也将 useReducer 比做 Hooks 的的金手指模式,由于这充分绕过了 Diff 机制,

不过确实能解决痛点!

使用 useCallback 包装函数

function Parent() {
  const [query, setQuery] = useState("react");
  // ✅ Preserves identity until query changes
  const fetchData = useCallback(() => {
    const url = "https://hn.algolia.com/api/v1/search?query=" + query;
    // ... Fetch data and return it ...
  }, [query]); // ✅ Callback deps are OK
  return <Child fetchData={fetchData} />;
}
function Child({ fetchData }) {
  let [data, setData] = useState(null);
  useEffect(() => {
    fetchData().then(setData);
  }, [fetchData]); // ✅ Effect deps are OK
  // ...
}复制代码

因为函数也具备 Capture Value 特性,通过 useCallback 包装过的函数能够看成普通变量做为 useEffect 的依赖。useCallback 作的事情,就是在其依赖变化时,返回一个新的函数引用,触发 useEffect 的依赖变化,并激活其从新执行。


自定义Hooks

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

自定义 Hook 必须以 “use” 开头吗?必须如此。这个约定很是重要。不遵循的话,因为没法判断某个函数是否包含对其内部 Hook 的调用,React 将没法自动检查你的 Hook 是否违反了 Hook 的规则


在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用

状态逻辑
的机制(例如设置为订阅并存储当前值),因此每次使用自定义 Hook 时,其中的全部 state 和反作用都是彻底隔离的。


自定义 Hook 如何获取独立的 state?每次

调用
Hook,它都会获取独立的 state。因为咱们直接调用了 useFriendStatus,从 React 的角度来看,咱们的组件只是调用了 useStateuseEffect。 正如咱们在 以前章节了解到的同样,咱们能够在一个组件中屡次调用 useStateuseEffect,它们是彻底独立的。

在多个 Hook 之间传递信息

因为 Hook 自己就是函数,所以咱们能够在它们之间传递信息。

咱们将使用聊天程序中的另外一个组件来讲明这一点。这是一个聊天消息接收者的选择器,它会显示当前选定的好友是否在线:


const friendList = [
  { id: 1, name: 'Phoebe' },
  { id: 2, name: 'Rachel' },
  { id: 3, name: 'Ross' },
];
function ChatRecipientPicker() {
  const [recipientID, setRecipientID] = useState(1);  const isRecipientOnline = useFriendStatus(recipientID);
  return (
    <>
      <Circle color={isRecipientOnline ? 'green' : 'red'} />      <select
        value={recipientID}
        onChange={e => setRecipientID(Number(e.target.value))}
      >
        {friendList.map(friend => (
          <option key={friend.id} value={friend.id}>
            {friend.name}
          </option>
        ))}
      </select>
    </>
  );
}复制代码


咱们将当前选择的好友 ID 保存在 recipientID 状态变量中,并在用户从 <select> 中选择其余好友时更新这个 state。


因为 useState 为咱们提供了 recipientID 状态变量的最新值,所以咱们能够将它做为参数传递给自定义的 useFriendStatus Hook:


const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);复制代码


如此可让咱们知道

当前选中
的好友是否在线。当咱们选择不一样的好友并更新 recipientID 状态变量时, useFriendStatus Hook 将会取消订阅以前选中的好友,并订阅新选中的好友状态。


useLegacyState

若是你错过自动合并,你能够写一个自定义的 useLegacyState Hook 来合并对象 state 的更新。然而,咱们推荐把 state 切分红多个 state 变量,每一个变量包含的不一样值会在同时发生变化。


获取上一轮的 props 或 state?

能够 经过 ref 来手动实现:


function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);  return <h1>Now: {count}, before: {prevCount}</h1>;
}
function usePrevious(value) {  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}复制代码

注意看这是如何做用于 props, state,或任何其余计算出来的值的。

function Counter() {
  const [count, setCount] = useState(0);
  const calculation = count + 100;
  const prevCalculation = usePrevious(calculation);  // ...复制代码

考虑到这是一个相对常见的使用场景,极可能在将来 React 会自带一个 usePrevious Hook。

参见 derived state 推荐模式.


参考文章

相关文章
相关标签/搜索