React Hook

314 阅读8分钟

React Hook 在 react 16.8及以后的版本中才会有

React Hook 解决的问题

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

2. 减少组件的复杂程度

在传统的 class 中,会使用 componentDidMount 和 componentDidUpdate 获取数据。同时 componentDidMount 中也会处理一些其他的事务,例如事件监听,定时器等等。而后还需要在 componentWillUnmount 中取消。万一忘记其中某一个部分或者处理的时间过多,很可能导致一些可怕的bug。

3. 关于 class 类与函数组件 this 的问题

对于一部分人来说,理解 class 中的 this 会比理解函数组件中的 this 更加困难,而且增加了学习成本。但是,react 中并不会移除 class 这种方法

YouTube上面的视频

State Hook

传统的 React 组件的 state 都是这样的,创建一个 state 与更新(this.setState)

import React from 'react';
class Demo extends React.Component{
  constructor(props){
    super(props)

    this.state = {
      count: 0
    }
  }

  render(){
    return (<div onClick={_ => this.setState({count: this.state.count++})}>{this.state.count}</div>)
  }
}

使用 React Hook 后

import React, { useState } from 'react'
function Demo(){
  let [count, setCount] = useState(0)

  return (<div onClick={_ => setCount(count++)}>{count}</div>)
}

可以看到。使用 Hook 后的代码简洁了很多。但是,使用 useState 不会把新的 state 和旧的 state 进行合并。

上面,我们只是用了一个 count。但是通常一个组件都不会只有一个 state 的,这时候可以多次使用 useState

同时,定义 state 的时候定义在一个数组里面,可以猜到, useState 返回的不是一个不同的数字或者字符串,而是一个对象(数组)。这里这样定义,使用了 ES6 中的解构赋值

Effect Hook

useState 其实不难理解,唯一需要注意的就是 this.setState 是修改后的 state 与之前的 state 对比合并,而采用 useState 则是直接替换。

作为使用过一段时间的 React Hook 的程序员,个人认为 Effect Hook 才需要更多的理解。

React官方文档中这样定义的

你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。 useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

所以,我们使用 Hook 后,数据获取、订阅或者手动修改过 DOM等都需要在 useEffect 中进行了。

不要以为 useEffectcomponentDidMountcomponentDidUpdatecomponentWillUnmount 一样只能使用一次,他与 useState 一样,可以多次使用。

默认情况下,React 会在每次渲染后调用副作用函数(useEffect) —— 包括第一次渲染的时候。所以,在 useEffect 函数中可以直接使用 props 和 state

useEffect 接收两个参数。第一个参数是一个函数,第一个参数相当于 componentDidMountcomponentDidUpdate,第一个参数可以有一个返回值(一般就是一个函数,我们将之称为清除函数),相当于与 componentWillUnmount。这样一说,你可能就理解了。再来举个例子,更形象的说明一下

class Demo extends React.Component {
  componentDidMount(){
    this.timer = setInterval(() => doSomething(), 1000)
  }

  componentWillUnmount(){
    if(this.timer) clearInterval(this.timer)
  }

  ...
}

上面的是传统的方式,添加以及移除定时器的操作。因为需要在 componentWillUnmount 中进行判断,有时候(大部分时候)可能都会遗忘。

再来看看使用 useEffect 的代码

function Demo(){

  let timer = null
  useEffect(() => {
    timer = setInterval(() => doSomething(), 1000)
 
    // return 一个函数,将会在组件将要卸载的时候调用 相当于 componentWillUnmount
    return () => clearInterval(timer)
  })

  return ...
}

可以看出,使用 useEffect 不单单是代码更简洁,同时使我们的代码逻辑看起来更直观。设置定时器与清除定时器是放在一个API里面的,代码的耦合更高。更能体现这是一个整体,也避免了遗忘。

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

React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。

如果不涉及到异步,订阅等操作,可以不用返回清除函数

上面只是 useEffect 的一个简单的事例,它的功能不止于此。因为之前还说过,处理数据请求也是在里面处理的。那么怎么使用呢

function Demo(){
  useEffect(() => {
    // do ajax request
  })
}

如果只是上面那样写,会有一个严重的问题。之前说过, useEffect 是会在DOM初次加载完成以及DOM更新完成的时候调用,所以上面的请求会在每一次DOM更新的时候再次执行,而如果请求返回的结果会使DOM更新,那么,这就是一个无限循环的过程了。

那么怎么处理这个副作用呢?这时候就需要 useEffect 的第二个参数了。一般是一个数组

如果两次需要更新的数据没有变化,只需要在第二个参数(数组)中添加对应的变量,例如

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

但是如果是上面的处理 ajax request 的 effect 。只需要传递一个空数组即可。这样,这个 effect 只会执行一次。

React 会对数组中的数据进行更新前后数据的对比,如果没有变化,那么则不更新

这个方法对于有清除函数的 effect 同样适用。

React官网中说到:未来版本,可能会在构建时自动添加第二个参数。期待他的到来,这将大大减少可能出现的bug。

其他 Hook

除了 useStateuseEffect 两个常用的 Hook, 还有一些其他的 Hook, 这些可能用的不多。

useContext

const value = useContext(MyContext);

这个 Hook 用于连接 React 上下文。使用过 React.createContext 的老铁应该知道,这是创建一个 React 上下文

const Context = React.createContext;

// 上层组件
<Context.Provider></Context.Provider>

// 消费这个 Context 的组件
<Context.Consumer></Context.Consumer>

使用 useContext

const Context = React.createContext;

useContext(Context)

例子

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() {
  // 通过 useContext 使用 React.createContext(themes.light) 创建的 Context
  const theme = useContext(ThemeContext);

  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

useReducer

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

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

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>
    </>
  );
}

既然作用类似于 Redux, 那么可以用这个取代 Redux 么?答案是可以的,不过需要结合 useContext 来使用。掘金上面有码友给出了一个例子用 useContext + useReducer 替代 redux

你可以在新项目中或者涉及状态管理不多的项目中尝试使用,现有的大型项目不建议重构,使用 Redux 依然是不错的方案。

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 值。

useCallbackuseMemo 都可以用于 React 性能优化的手段。

useRef

const refContainer = useRef(initialValue);

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

所以,这个方法就相当于 class 中的 ref 属性,用于获取具体的DOM元素。

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>
    </>
  );
}

useImperativeHandle

useLayoutEffect

useDebugValue

上面未说明的 Hook 可以查看 React 官网

Hook 规则

Hook 永远是在最顶层调用,不能在条件判断语句或者其他语句中。

function Demo(){
  // 正确
  useEffect(() => {
    if(name === 'tal'){
      // do something
    }
  })

  // 错误
  if (name === 'tal') {
    useEffect(() => {
      // do something
    })
  }
}

如果你害怕你写错了,但是没有检查出来,可以使用 eslint-plugin-react-hooks 这个插件来检测。

自定义 Hook

Hook 我们也是可以自定义的。那么为什么需要自定义。答案是 逻辑共享

假如有一个 state 需要在多个组件中使用,我们不应该在多个组件中都单独的去创建这个 state, 而是应该逻辑共享。把这个 state 以及操作这个 state 的方法定义在我们自己的 Hook 中。那这个 Hook 就是我们自定义的 Hook,其实,他也是一个函数,接收参数,返回你需要的值。唯一需要注意的是:自定义 Hook 必须以 use 开头,不管怎么变,使用需要遵循 React Hook 以 use 开头的规则。

官网介绍