pfan123/Articles

React Hooks

pfan123 opened this issue · 0 comments

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

React Hooks出现,组件可尽量写成纯函数,如需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。

使用 React Hooks 好处:

  • Hook 将组件中相互关联的部分拆分成更小的函数方便复用
  • Class 组件通过 Hook 拆分复用状态逻辑方便理解

React 默认提供了一些常用钩子,也可以封装自己的钩子,以下常用的几个钩子:

  • useState()
  • useEffect()
  • useCallback()
  • useMemo()
  • useContext()
  • useReducer()

useState():状态钩子

useState()用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

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

注意

  • useState() 我们建议使用多个 state 变量
  • 在有关联的情况下,State 变量可使用对象和数组,但注意更新 state,变更需给定新对象或数组,若改变原始对象或数组则不会触发钩子。

useEffect():副作用钩子

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

可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合

在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。React 的 class 组件中,会把副作用操作放到 componentDidMountcomponentDidUpdate 函数中,并在 componentWillUnmount 中清除它。

无需清除的 effect

class 组件中,把副作用操作放到 componentDidMountcomponentDidUpdate 函数中:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

useEffect 改写:

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

需清除的 effect

在 React class 中,把副作用操作放到 componentDidMountcomponentDidUpdate 函数中,并在 componentWillUnmount 中清除它。

class Example extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    window.addEventListener('scroll', callback)
  }

  componentWillUnmount() {
    window.removeEventListener('scroll', callback)
  }

  render() {
    return (
      <div/>
    );
  }
}

useEffect 改写:

import React, { useEffect } from 'react';

function Example() {

  useEffect(() => {
    window.addEventListener('scroll', callback)`
    return () => window.removeEventListener('scroll', callback)
  })

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

Window Event Listener Hook 类似场景

export const useWindowEvent = (event, callback, dependencies) => {
  useEffect(() => {
    window.addEventListener(event, callback);
    return () => window.removeEventListener(event, callback);
  }, [event, callback, ...dependencies]);
};

useCallback()

useCallback 返回一个 memoized 回调函数。

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

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

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

useCallback(fn, deps) 请求数据场景缓存

useMemo()

useMemo 返回一个 memoized 值。

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

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

useCallbackuseMemo 都可缓存函数的引用或值,但是从更细的使用角度来说 useCallback 缓存函数的引用,useMemo 缓存计算数据的值。
useMemo 更多用于复杂的运算。

useContext()

const AppContext = useContext(AppContextProps)

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

当组件上层最近的 <AppContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 AppContext provider 的 context value 值。

useContext 的参数必须是 context 对象本身

  • 正确: useContext(AppContext)
export const AppContext = React.createContext<Partial<AppContextProps>>({})
<AppContext.Provider
  value={{
  	authenticated: state.authenticated,
  	userInfo: state.userInfo,
  }}
>
	{props.children}
</AppContext.Provider>


import { AppContext } from '../AppContext'

function ThemedButton() {
  const Context = useContext(AppContext);

  return (
    <div>
      {Context.userInfo}
    </div>
  );
}

Hook 规则

Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则, linter 插件 可强制执行这些规则:

  • 只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook,确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。

  • 只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook。你可以:

  • 在 React 的函数组件中调用 Hook
  • ✅ 在自定义 Hook 中调用其他 Hook

遵循此规则,确保组件的状态逻辑在代码中清晰可见。

npm install eslint-plugin-react-hooks --save-dev

// ESLint 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}

Other Resources

React Hook Intro

Creating a Reusable Window Event Listener Hook with useEffect and useCallback

React Hooks 原理

react-use

useApolloClient

React Hooks 你真的用对了吗?

useCallback、useMemo 分析 & 差别