React Hook 使用总结

597 阅读7分钟

Hook简介

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

设计Hooks主要是解决组件的几个问题:
  • 在组件之间复用状态逻辑很难(只能用HOC或者render props),会导致组件树层级很深
  • 会产生巨大的组件(例如会在生命周期内处理逻辑)
  • 难以理解的类组件,比如方法需要bind,this指向不明确
针对以上问题,我们可以总结出使用Hook的优势
  1. Hook 使你在无需修改组件结构的情况下复用状态逻辑
  2. Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
  3. Hook 使你在非 class 的情况下可以使用更多的 React 特性
Hook的使用规则:

Hook 就是 JavaScript函数,但是使用它们会有两个额外的规则。

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。react 是根据 hooks 出现顺序来记录对应状态的
  • 只能在 React 的函数组件和自定义 hooks 中调用 Hook。
  • 命名规范(可被eslint-plugin-react-hooks强制执行这两条规则)
    • useState 返回数组的第二项以 set 开头(仅作为约定)。
    • 自定义 hooks 以 use 开头(可被 lint校验)。

React中提供的hooks:

  • useState: setState
  • useEffect: 类似componentDidMount/Update, componentWillUnmount,当效果为 componentDidMount/Update 时,总是在整个更新周期的最后(页面渲染完成后)才执行
  • useContext: context,需配合 createContext 使用
  • useRef: ref
  • useCallback: useMemo 的变形,对函数进行优化

  • useReducer:setState,同时 useState 也是该方法的封装
  • useLayoutEffect: 用法与 useEffect 相同,区别在于该方法的回调会在数据更新完成后,页面渲染之前进行,该方法会阻碍页面的渲染
  • useDebugValue:用于在 React 开发者工具中显示自定义 hook 的标签
  • useMemo: 可以对 setState 的优化
  • useImperativeHandle: 给 ref 分配特定的属性

Hook API 索引

1.useState

const [state, setState] = useState(initialState)
  • useState 有一个参数,该参数可传如任意类型的值或者返回任意类型值的函数。
  • useState 返回值为一个数组,数组的第一个参数为我们需要使用的 state,第二个参数为一个setter函数,可传任意类型的变量,或者一个接收 state 旧值的函数,其返回值作为 state 新值。
import React, { useState } from 'react';
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

export default Counter;

注意: 与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象,所以建议:

  • 如果数据结构简单,可以将变量根据数据结构需要放在不同的 useState 中。
setState(prevState => {
  // 也可以使用 Object.assign 
  return {...prevState, ...updatedValues};
});
- 
  • 如果数据结构复杂,建议使用 useReducer 管理组件的 state。

2.useEffect

useEffect(effect, array);

该 Hook 接收一个包含命令式、且可能有副作用代码的函数。

默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候才执行。

useEffect 接收两个参数,没有返回值。

  • 第一个参数为 effect 函数,该函数将在 componentDidMmount 时触发和 componentDidUpdate 时有条件触发。 注意: 与 componentDidMount 和 componentDidUpdate 不同之处是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。 如果需要在渲染之前触发,需要使用 useLayoutEffect。
    • 钩子的执行顺序:useLayoutEffect > requestAnimationFrame > useEffect
  • 第二个参数 array 作为有条件触发情况时的条件限制:
    • 如果不传,则每次 componentDidUpdate 时都会先触发 returnFunction(如果存在),再触发 effect。
    • 如果为空数组[],componentDidUpdate 时不会触发 returnFunction 和 effect。
    • 如果只需要在指定变量变更时触发 returnFunction 和 effect,将该变量放入数组。
不需要清除的Effect
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这个hook,可以告诉React组件需要在渲染后执行某些操作。React会保存所传递的函数,并且在执行DOM更新之后调用它。 将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)——它已经保存在函数作用域中。

需要清除的Effect
useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除订阅
    subscription.unsubscribe();
  };
});

为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除

effect 的条件执行

默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。可以给useEffect传递第二个参数,这样就不需要在每次组件更新时都创建新的effect。

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以componentDidUpdate时都不需要重复执行。

3.useContext

要理解 Context Hooks 中的 api,首先需要了解 context 和其使用场景。

设计目的: context 设计目的是为共享那些被认为对于一个组件树而言是“全局”的数据。

使用场景: context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。

3.1 createContext
const {Provider, Consumer} = React.createContext(defaultValue, calculateChangedBits)
  • createContext创建一对{ Provider, Consumer }。当 React 渲染 context 组件 Consumer 时,它将从组件树的上层中最接近的匹配的 Provider 读取当前的 context 值。Consumer 是 Provider 提供数据的使用者。
  • 如果上层的组件树没有一个匹配的 Provider,而此时你需要渲染一个 Consumer 组件,那么你可以用到 defaultValue 。这有助于在不封装它们的情况下对组件进行测试。
import React from 'react';
/*  结果读取为123,因为没有找到Provider */
const { Provider, Consumer } = React.createContext(123);
function Bar() {
  return <Consumer>{color => <div>{color}</div>}</Consumer>;
}
function Context() {
  return <Bar />;
}

export default Context;
3.1.1 Provider

React 组件允许 Consumers 订阅 context 的改变。而 Provider 就是发布这种状态的组件,该组件接收一个 value 属性传递给 Provider 的后代 Consumers。一个 Provider 可以联系到多个 Consumers。Providers 可以被嵌套以覆盖组件树内更深层次的值。

export const ProviderComponent = props => {
  return (
    <Provider value={}>
      {props.children}
    </Provider>
  )
}
3.1.2 Consumer
<Consumer>
  {value => /* render something based on the context value */}
</Consumer>
  • 一个可以订阅 context 变化的 React 组件。当 context 值发生改变时,Consumer 值也会改变
  • 接收一个 函数作为子节点,该函数接收当前 context 的值并返回一个 React 节点。传递给函数的 value 将等于组件树中上层 context 的最近的 Provider 的 value 属性。如果 context 没有 Provider ,那么 value 参数将等于被传递给 createContext() 的 defaultValue 。
import React from 'react';
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
export default class Context extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
//   static contextType = ThemeContext;
  render() {
    // return <button>{this.context}</button>;
    return (
        <ThemeContext.Consumer>
            {theme => <button>{theme}</button>}
        </ThemeContext.Consumer>
    )
  }
}

每当 Provider 的值发生改变时, 作为 Provider 后代的所有 Consumers 都会重新渲染。 从 Provider 到其后代的Consumers 传播不受 shouldComponentUpdate 方法的约束,因此即使祖先组件退出更新时,后代Consumer也会被更新。

3.2 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 时重新渲染。

提示: useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。

import React, { useContext } from "react";

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee",
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222",
  },
};

const ThemeContext = React.createContext(themes.light);

function Context() {
  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>
  );
}

export default Context;

4. useRef

const refContainer = useRef(initialValue);
4.1 组件引用

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

如果是引用元素对象一般不传参数,返回一个可变的 ref 对象,该对象下面有一个 current 属性指向被引用对象的实例。

说到useRef,也会提到createRef,以及为什么要有这个 api 出现。createRef 使用方法和 useRef 一致,返回的是一个 ref 对象

两者当做ref正常使用时效果完全一样:

createRef
import React, { createRef } from "react";

const FocusInput = () => {
  const inputElement = createRef();
  const handleFocusInput = () => {
    inputElement.current.focus();
  };
  return (
    <div>
      <input type="text" ref={inputElement} />
      <button onClick={handleFocusInput}>Focus Input</button>
    </div>
  );
};

export default FocusInput;

useRef
import React, { useRef } from 'react'

const FocusInput = () => {
  const inputElement = useRef()
  const handleFocusInput = () => {
    inputElement.current.focus()
  }
  return (
    <>
      <input type='text' ref={inputElement} />
      <button onClick={handleFocusInput}>Focus Input</button>
    </>
  )
}

export default FocusInput;

但是,这两者对应 ref 的引用其实是有着本质区别的:createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。

import React, { useRef, createRef } from "react";

const CreateRef = () => {
  const [renderIndex, setRenderIndex] = React.useState(1);
  const refFromUseRef = React.useRef();
  const refFromCreateRef = createRef();

  if (!refFromUseRef.current) {
    refFromUseRef.current = renderIndex;
  }

  if (!refFromCreateRef.current) {
    refFromCreateRef.current = renderIndex;
  }

  return (
    <>
      <p>Current render index: {renderIndex}</p>
      <p>
        <b>refFromUseRef</b> value: {refFromUseRef.current}
      </p>
      <p>
        <b>refFromCreateRef</b> value:{refFromCreateRef.current}
      </p>

      <button onClick={() => setRenderIndex((prev) => prev + 1)}>
        Cause re-render
      </button>
    </>
  );
};

export default CreateRef;

4.2 替代this

一个经典案例:

import React, { useRef, useState } from 'react'

function App() {
  const [count, setCount] = useState()
  function handleAlertClick() {
    setTimeout(() => {
      alert(`Yout clicked on ${count}`)
    }, 3000)
  }
  return (
    <div>
      <p>You click {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  )
}

当我们更新状态的时候, React 会重新渲染组件, 每一次渲染都会拿到独立的 count 状态, 并重新渲染一个 handleAlertClick 函数. 每一个 handleAlertClick 里面都有它自己的 count。

但是你会发现,count的值并不能实时的显示更新的数据,这是因为js中一直存在的闭包机制导致的,当点击显示弹窗的按钮时,此时count的值已经确定,并且传入到了alert方法的回调中,形成闭包,后续值的改变不会影响到定时器的触发。

而如果在类组件中,如果我们使用的是this.state.count,得到的结果又会是实时的,因为它们都是指向的同一个引用对象。

在函数组件中,我们可以使用 useRef 来实现实时得到新的值,这就是 useRef 的另外一种用法,它还相当于 this , 可以存放任何变量。useRef 可以很好的解决闭包带来的不方便性。

import React, { useRef, useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const lastestCount = useRef()
  lastestCount.current = count
  function handleAlertClick() {
    setTimeout(() => {
      alert(`You clicked on ${lastestCount.current}`) // 实时的结果
    }, 3000)
  }
  return (
    <div>
      <p>Yout click {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  )
}

5. useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b); // ----->内联回调函数
  },
  [a, b],// ----->依赖项数组
);
// useCallback的实现原理
let memoizedState = null
function useCallback(callback, inputs) {
  const nextInputs =
    inputs !== undefined && inputs !== null ? inputs : [callback]
  const prevState = memoizedState;
  if (prevState !== null) {
    const prevInputs = prevState[1]
    if (areHookInputsEqual(nextInputs, prevInputs)) {
      return prevState[0]
    }
  }
  memoizedState = [callback, nextInputs]
  return callback
}

注意:第二个参数目前只用于指定需要判断是否变化的参数,并不会作为形参传入回调函数,建议回调函数中使用到的变量都应在数组中列出。

useCallback 返回值

返回一个memoized回调函数,在依赖参数不变的情况下,返回的回调函数是同一个引用地址。

提示:每当依赖参数发生改变useCallback就会自动重新返回一个新的 memoized 函数(地址发生改变)

useCallback 使用场景

可以避免非必要渲染例如 (shouldComponentUpdate)的子组件,优化子组件渲染次数。

import React, { useState, memo } from "react";
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const handleChildren = () => {
    console.log("clicked ChildrenComponent");
  };

  const handleParent = () => {
    console.log("clicked ParentComponent");
    setCount((preCount) => preCount + 1);
  };

  return (
    <div>
      <button onClick={handleParent}>ParentComponent --count =={count} </button>
      <ChildrenComponent handleChildren={handleChildren} />
    </div>
  );
};

const ChildrenComponent = memo(({ handleChildren }) => {
  console.log("ChildrenComponent rending");
  return <button onClick={handleChildren}>ChildrenComponent </button>;
});

export default ParentComponent;

可以发现,每次点击ParentComponent就会导致ChildrenComponent也渲染一次,虽然ChildrenComponent采用了memo 优化,如图:

useCallback.js:9 clicked ParentComponent
useCallback.js:22 ChildrenComponent rending
useCallback.js:5 clicked ChildrenComponent
useCallback.js:9 clicked ParentComponent
useCallback.js:22 ChildrenComponent rending

使用useCallback,可以来优化ChildrenComponent的渲染,见如下代码:

const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const handleChildren = () => {
    console.log('clicked ChildrenComponent');
  };
  const handleChildrenCallback = useCallback(() => {
    handleChildren();
  }, []);

  const handleParent = () => {
    console.log('clicked ParentComponent');
    setCount(preCount => preCount + 1);
  };

  return (
    <div>
      <div onClick={handleParent}>ParentComponent --count =={count} </div>
      <ChildrenComponent handleChildren={handleChildrenCallback} />
    </div>
  );
};

const ChildrenComponent = memo(({ handleChildren }) => {
  console.log('ChildrenComponent rending');
  return <div onClick={handleChildren}>ChildrenComponent </div>;
});

6.自定义Hook

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

import { useState, useCallback, useEffect } from "react";

const useResize = () => {
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight,
  });
  const onResize = useCallback(() => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight,
    });
  }, []);
  useEffect(() => {
    window.addEventListener("resize", onResize);
    return () => {
      window.removeEventListener("resize", onResize);
    }
  }, [onResize]);
  return size;
};
export default useResize;

通过自定义Hook,可以将组件逻辑提取到可重用的函数中。自定义的Hook通过共享同一个memoizedState,共享同一个顺序实现影响相应的函数组件。

底层原理

React 是如何把对 Hook 的调用和组件联系起来的?

React 保持对当前渲染中的组件的追踪,是因为遵循了Hook规范。每个组件内部都有一个[记忆单元格]列表。它们不过是我们用来存储一些数据的JavaScript 对象,当你用useState()调用一个Hook的时候,它会读取当前的单元格(或在首次渲染时将其格式化),然后把指针移动到下一个。

【参考文档】

React Hook官方文档

React Hooks 使用总结

彻底理解 React hook useCallback和useMemo的区别

作者:JulyCheng