React Hook 学习记录

172 阅读7分钟

Hook简介

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。先看两个官方文档中提供的展示Hook用法的例子。

  1. State Hook

    import React, { useState } from 'react';
    
    function Example() {
      // useState 是一个hook,声明一个叫 "count" 的 state 变量,初始值为 0。返回当前的值和更改值的函数。
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
    
  2. Effect Hook

    import React, { useState, useEffect } from 'react';
    
    function Example() {
      const [count, setCount] = useState(0);
    
      // Similar to componentDidMount and componentDidUpdate:
      useEffect(() => {
        // 组件初始化和更新后设置文档标题
        document.title = `You clicked ${count} times`;
      });
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
    

Hook 要解决的问题

  1. 组件间难以复用状态逻辑。
  2. 复杂组件内逻辑的状态和副作用代码分散难以理解。
  3. class组件理解不易、不利于工具优化 。

引入 Hook 的影响

  1. 可选:无需重写现有代码,不破坏现有结构,不取代现有React概念,渐进使用。
  2. 兼容:100%向后兼容,不包含破坏性改动。
  3. 自 v16.8.0 可用。

使用 Hook 的限制与检测工具

限制:

  1. 只在最顶层使用 Hook,不要在循环、条件或嵌套函数中调用 Hook
  2. 只在 React 函数组件和自定义Hook中调用 Hook,不要在普通的 JavaScript 函数中调用 Hook

React 提供了一个elint插件 eslint-plugin-react-hooks 可用于检测 Hook 被正确使用。

  1. react-hooks/rules-of-hooks: 检查 Hook 的规则
  2. react-hooks/exhaustive-deps:检查 effect 的依赖

内置Hook的使用

内置10 个 Hook 分别是:

  1. useState:用于申明和更新组件 state。
  2. userEffect:用于传入函数完成Dom变更等副作用操作,该函数会在组件每次渲染结束后执行。
  3. userContext:用于接受上层 Context 对象并返回当前值。
  4. useReducer:用于组件复杂状态的处理,可以理解为一个简易的redux。
  5. useCallback:用于返回一个 memoized 的回调函数,依赖不改变时返回的函数引用不变,常用于事件绑定或函数props传递。
  6. userMemo:返回一个 memoized 值,依赖不改变时不会重新执行函数直接使用上次计算的值。
  7. userRef:返回一个 ref 对象,可看作组件的实例变量,ref.current的值变化时不会通知触发重新渲染。
  8. userImperativeHandle:配合 ref 使用给父组件暴露实例方法和值。
  9. userLayoutEffect:基本等同与 userEffect,不过会在绘制前执行。
  10. userDebugValue:用于自定义Hook调试,会在React 开发者工具中显示自定义Hook的标签。

以下介绍各Hook详细使用方法。

0. 相关类型

以下为内置Hook函数定义依赖的类型定义。

type Dispatch<A> = A => void;
type BasicStateAction<S> = (S => S) | S;

1. useState

定义

useState<S>(
	initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>]

描述

申明一个state并设置初始值,返回一个 state值及更新 state 的函数。

示例

import React, { useState } from "react";

export default function App({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(count + 1)}>+</button>
    </>
  );
}

points

  1. useState返回一个包含当前状态和更新状态函数的数组,通过解构自行命名。
  2. useState 接受一个函数惰性计算初始值,非首次渲染时会跳过该函数执行。eg:useState(computedFunction)}
  3. setCount 更新时行为是覆盖当前值而不是合并到当前值。
  4. setCount 可接受一个函数,函数接受先前state返回更新后的值。eg:setCount(prevCount => prevCount + 1)}

2. userEffect

定义

useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void

描述

使用 userEffect 传入函数完成副作用操作,该函数会在组件每次渲染结束后执行。

示例

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

export default function App() {
  const [count, setCount] = useState(0);

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

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

备注

  1. 支持返回一个函数用于清除副作用,副作用和清除副作用函数在每次渲染后都会执行。若需要在渲染前执行,可参考 useLayoutEffect。
  2. 支持传入第二个参数指明副作用函数依赖的数组,依赖未变化时将跳过副作用处理。
  3. 若需要根据条件动态决定是否执行,需要将条件放在副作用函数内。

3. userContext

定义

useContext<T>(context: ReactContext<T>): T

用途

用于接受上层 Context 对象并返回当前值。

示例

import React, { useContext } from "react";

const Context = React.createContext();

function Counter() {
  const count = useContext(Context);
  return <div>Current: {count}</div>;
}

export default function App() {
  return (
    <Context.Provider value={1}>
      <Counter />
    </Context.Provider>
  );
};

备注

  1. 需要配合上层组件中 Context.Provider 一起使用。

4. useReducer

定义

useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>]

用途

用于组件复杂状态的处理,可以理解为一个简易的 redux。

示例

import React, { useReducer } from "react";

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

export default function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}

备注

  1. 接受第二个参数initialArg 为初始值,若初始值计算量大,可以通过传入第三个参数init函数来进行初始化。

5. useCallback

定义

useCallback<T>(callback: T, deps: Array<any> | void | null): T

用途

用于返回一个 memoized 的回调函数,依赖不改变时返回的函数引用不变,常用于事件绑定或函数props传递。

示例

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

class Button extends React.PureComponent {
  render() {
    console.log("rendered ", this.props.text);
    return <button onClick={this.props.onClick}>{this.props.text}</button>;
  }
}

export default function App() {
  const [count1, setCount1] = useState(0);
  const memoizedOnClick = useCallback(function onClick() {
    setCount1((count1) => count1 + 1);
  }, []);
  const [count2, setCount2] = useState(0);
  const onClick = function onClick() {
    setCount2((count2) => count2 + 1);
  };
  // 点击button1和button2时只有button2会重新渲染
  return (
    <>
      {count1}
      <Button text="button1" onClick={memoizedOnClick} />
      {count2}
      <Button text="button2" onClick={onClick} />
    </>
  );
}

备注

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

6. userMemo

定义

useMemo<T>(create: () => T, deps: Array<any> | void | null): T

用途

返回一个 memoized 值,依赖不改变时不会重新执行函数直接使用上次计算的值

示例

import React, { useState, useMemo } from "react";

export default function App() {
  const [count1, setCount1] = useState(2);
  // 点击 button2 触发重新渲染时不会重新计算result
  const result = useMemo(
    function () {
      console.log("computed pow");
      return Math.pow(count1, 2);
    },
    [count1]
  );

  const [count2, setCount2] = useState(2);
  return (
    <>
      pow: {result} <br />
      count2: {count2} <br />
      <button onClick={() => setCount1(count1 + 1)}>button1</button>
      <button onClick={() => setCount2(count2 + 1)}>button2</button>
    </>
  );
}

7. userRef

定义

useRef<T>(initialValue: T): {current: T}

用途

返回一个 ref 对象,可看作组件的实例变量,ref.current的值变化时不会通知触发重新渲染

示例

// 示例1: 获取dom节点
import React, { useRef } from "react";

export default function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // 获取dom节点
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
// 示例2: 组件实例变量
import React, { useEffect, useRef, useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  // intervalId 用于组件的实例变量
  let intervalId = useRef(null);
  useEffect(() => {
    intervalId.current = setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
    console.log("create ", intervalId.current);
    return () => {
      console.log("clear ", intervalId.current);
      clearInterval(intervalId.current);
    };
  }, []);
  return (
    <>
      count: {count}; intervalId.current: {intervalId.current}
      <button
        onClick={() => {
          // 示例3: 点击stop后更改 intervalId 的值界面不会重新渲染
          clearInterval(intervalId.current);
          intervalId.current = Math.random();
        }}
      >
        stop
      </button>
    </>
  );
}

export default function App() {
  const [visible, setVisible] = useState(true);

  return (
    <>
      {visible && <Counter />}
      <button onClick={() => setVisible((v) => !v)}>Toggle</button>
    </>
  );
}

备注

  1. 可用于获取dom节点
  2. 可用于组件实例变量
  3. ref.current更新不会引发组件重新渲染

8. userImperativeHandle

定义

useImperativeHandle<T>(
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<any> | void | null,
): void

用途

配合 ref 暴露实例方法和值给父组件使用

示例

import React, {
  forwardRef,
  useRef,
  useState,
  useImperativeHandle
} from "react";

const Counter = forwardRef(function Counter(props, ref) {
  const [count, setCount] = useState(0);
  useImperativeHandle(ref, () => ({
    increment: () => { // 暴露increment方法
      setCount((c) => c + 1);
    }
  }));
  return <>count: {count}</>;
});

export default function App() {
  const counterRef = useRef();
  return (
    <>
      <Counter ref={counterRef} />
      <button onClick={() => counterRef.current.increment()}>increment</button>
    </>
  );
}

备注

  1. 尽量避免使用
  2. 应当与 forwardRef 一起使用

9. userLayoutEffect

定义

useLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<any> | void | null,
): void

用途

基本等同与 userEffect,不过会在dom变更后绘制前执行

示例

// styles.css
.container {
  background: green;
  height: 200px;
  position: relative;
  margin-bottom: 10px;
}
.inner {
  display: block;
  width: 130px;
  height: 50px;
  background: red;
  position: absolute;
  left: 0;
  top: 0;
  transition: all 3s;
}
.active {
  left: 100px;
}
// App.js
import React, { useLayoutEffect, useRef, useEffect } from "react";
import "./styles.css";

export default function App() {
  const ref1 = useRef();
  const ref2 = useRef();
  useLayoutEffect(() => {
    // 1. 初次执行 render 渲染成 HTML
    // 2. 执行 useEffect 增加类 active
    // 3. 初次绘制 left:100,看不到动画
    ref1.current.className = "inner active";
  }, []);
  useEffect(() => {
    // 1. 初次执行 render 渲染成 HTML
    // 2. 初次绘制 left:0
    // 3. 执行 useEffect 增加类 active
    // 4. 再次绘制 left:100 触发 transition,可以看到动画
    ref2.current.className = "inner active";
  }, []);
  return (
    <div>
      <div className="container">
        <div ref={ref1} className="inner">
          useLayoutEffect
        </div>
      </div>
      <div className="container">
        <div ref={ref2} className="inner">
          useEffect
        </div>
      </div>
    </div>
  );
}

10. userDebugValue

定义

useDebugValue<T>(value: T, formatterFn: ?(value: T) => any): void

用途

用于自定义Hook调试,会在React 开发者工具中显示自定义Hook的标签。

示例

import React, { useDebugValue, useState } from "react";

function useFriendStatus() {
  const [isOnline] = useState("1");
  // 在开发者工具中的这个 Hook 旁边显示标签
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? "Online" : "Offline");

  return isOnline;
}

export default function App() {
  const isOnline = useFriendStatus();

  return <>isOnline: {isOnline}</>;
}

备注

  1. 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。eg: useDebugValue(date, date => date.toDateString());

自定义Hook

  1. 自定义 Hook 就是调用了 Hook 的普通函数,强烈推荐名称以“use” 开头命名。这是一个非强制约定,不准守不影响功能运行,但是影响阅读体验和工具识别和检测Hook 规则。
  2. 可以在函数中调用其他内置或自定义 Hook,同样需要准守Hook的限制如只能在顶层调用。
  3. 通过自定义 Hook可以将组件逻辑提取到单独的函数实现复用,若目的不是重用组件逻辑且未调用任何Hook不要将函数命名为“use”开头。

以下是一个自定义Hook的例子,用于重用聊天和评论消息送达时间的格式化和随时间更新逻辑。

import React, { useRef, useEffect, useState } from "react";

function formatTime(time) {
  const now = +new Date();
  const offsetSeconds = Math.round((now - time) / 1000);
  const Minute = 60;
  const Hour = Minute * 60;
  const Day = Hour * 24;
  if (offsetSeconds < Minute) {
    return `${offsetSeconds}秒前`;
  } else if (offsetSeconds < Hour) {
    return `${Math.round(offsetSeconds / Minute)}分前`;
  } else if (offsetSeconds < Day) {
    return `${Math.round(offsetSeconds / Hour)}小时前`;
  } else {
    return `${Math.round(offsetSeconds / Day)}天前`;
  }
}

function useFormatMessageTime(arrivedTime) {
  const [formattedTime, setFormattedTime] = useState(() =>
    formatTime(arrivedTime)
  );
  const ref = useRef();
  useEffect(() => {
    console.log("setInterval");
    ref.current = setInterval(() => {
      setFormattedTime(formatTime(arrivedTime));
    });
    return () => {
      console.log("clearInterval");
      clearInterval(ref.current);
      ref.current = null;
    };
  }, [arrivedTime]);
  return formattedTime;
}

export default function App() {
  const [time, setTime] = useState(new Date());
  const formattedTime = useFormatMessageTime(time);
  return (
    <>
      message arrived at: {formattedTime}
      <button onClick={() => setTime(new Date())}>set now</button>
    </>
  );
}

实现原理

推荐阅读:

第二个文章中的代码运行有一点错误,修正后如下:codesandbox.io/s/react-fib…

对比Vue

文章 对比 React Hooks 和 Vue Composition API 已对此有较为详细的对比。简单列举其中讲诉的异同点。

相同点:

  1. 解决状态复用和逻辑代码点分散的问题
  2. API有相似之处

不同点:

  1. React 基于链表实现有调用顺序的限制,Vue基于Proxy实现没有此限制。
  2. React 需要手动定义hook 依赖但提供了优化跳过执行的手段,Vue可以自动收集依赖。
  3. React 的hook需要在每次渲染时都会执行,vue会放在setup里执行一次。
  4. React 提供的useEffect处理副作用类似组件执行时相当于多个生命周期,Vue为不同的选项式生命周期提供不同的API。

总结图

ReactHook.png

参考资料