19

什么是 Hooks?

不通过编写类组件的情况下,可以在组件内部使用状态(state) 和其他 React 特性(生命周期,context)的技术

Hooks 为什么会出现

在之前的 React 版本中,组件分为两种:函数式组件(或无状态组件(StatelessFunctionComponent))和类组件,而函数式组件是一个比较的纯洁的 props => UI 的输入、输出关系,但是类组件由于有组件自己的内部状态,所以其输出就由 propsstate 决定,类组件的输入、输出关系就不再那么纯洁。同时也会带来下列问题:

  1. 状态逻辑难以复用。很多类组件都有一些类似的状态逻辑,但是为了重用这些状态逻辑,社区提出了 render props 或者 hoc 这些方案,但是这两种模式对组件的侵入性太强。另外,会产生组件嵌套地狱的问题。
  2. 大多数开发者在编写组件时,不管这个组件有木有内部状态,会不会执行生命周期函数,都会将组件编写成类组件,后续迭代可能增加了内部状态,又增加了副作用处理,又在组件中调用了一些生命周期函数,文件代码行数日益增多,最后导致组件中充斥着无法管理的混乱的状态逻辑代码和各种副作用,各种状态逻辑散落在实例方法和生命周期方法中,维护性变差,拆分更是难上加难。
  3. 在类组件中,需要开发者额外去关注 this 问题,事件监听器的添加和移除等等。

State Hook

state hook 提供了一种可以在 function component 中添加状态的方式。通过 state hook,可以抽取状态逻辑,使组件变得可测试,可重用。开发者可以在不改变组件层次结构的情况下,去重用状态逻辑。更好的实现关注点分离。

一个简单的使用 useState 栗子

import React, { useState } from "react";

const StateHook = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>you clicked {count} times</p>
      <button type="button" onClick={() => setCount(count + 1)}>
        click me
      </button>
    </div>
  );
};

几点说明:

  1. useState 推荐一种更加细粒度的控制状态的方式,即一个状态对应一个状态设置函数,其接受的参数将作为这个状态的初始值。其返回一个长度为2的元组,第一项为当前状态,第二项为更新函数。
  2. useState 的执行顺序在每一次更新渲染时必须保持一致,否则多个 useState 调用将不会得到各自独立的状态,也会造成状态对应混乱。比如在条件判断中使用 hook,在循环,嵌套函数中使用 hook,都会造成 hook 执行顺序不一致的问题。最后导致状态的混乱。另外,所有的状态声明都应该放在函数顶部,首先声明。
  3. useStatesetState 的区别

    useStatesetState 进行覆盖式更新,而 setState 则将状态进行合并式更新。

一个不正确的栗子

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

const UserForm = () => {
  const [state, setUser] = useState({ name: "", email: "" });

  const { name, email } = state;

  const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { target: { value: name } } = event;
    // 这里不可以单独的设置某一个字段 新的状态必须与初始的状态类型保持一致
    // 如果只设置了其中一个字段,编译器会报错,同时其余的字段也会丢失
    setUser({ name, email });
  };

  const handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { target: { value: email } } = event;
    // 这里不可以单独的设置某一个字段 新的状态必须与初始的状态类型保持一致
    setUser({ name, email });
  };

  return (
    <>
        <input value={name} onChange={handleNameChange} />
      <input value={email} onChange={handleEmailChange} />
    </>
  );
}

正确的做法

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

const UserForm = () => {
  // 一个状态对应一个状态更新函数
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { target: { value: name } } = event;
    // hear could do some validation
    setName(name);
  };

  const handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { target: { value: email } } = event;
    // hear could do some validation
    setEmail(email);
  };

  return (
    <>
        <input value={name} onChange={handleNameChange} />
      <input value={email} onChange={handleEmailChange} />
    </>
  );
}

Effect Hook

数据获取,设置订阅,手动的更改 DOM,都可以称为副作用,可以将副作用分为两种,一种是需要清理的,另外一种是不需要清理的。比如网络请求,DOM 更改,日志这些副作用都不要清理。而比如定时器,事件监听。

一个简单使用 effect hook 去修改文档标题的栗子。

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

const effectHook = () => {
  const [count, setCount] = useState(0);

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

  return (
    <div>
      <p>you clicked {count} times</p>
      <button type="button" onClick={() => setCount(count + 1)}>
        click me
      </button>
    </div>
  );
};

在调用 useEffect 后,相当于告诉 React 在每一次组件更新完成渲染后,都调用传入 useEffect 中的函数,包括初次渲染以及后续的每一次更新渲染。

几点说明:

  1. useEffect(effectCallback: () => void, deps: any[]) 接收两个参数,第二个参数依赖项是可选的,表示这个 effect 要依赖哪些值。
  2. 有时候我们并不想每次渲染 effect 都执行,只有某些值发生变化才去执行 effect,这个时候我们可以指定这个 effect 的依赖列表,可以是一个也可以几个,当其中列表中的某一个值发生变化,effect 才会执行。
  3. 第一个参数的返回值,会在组件卸载时执行,相当于 componentWillUnmount,可以清理定时器,移除事件监听,取消一些订阅。
  4. 当第二个参数为一个空数组时,相当于 componentDidMount 和 componentWillUnmount,表明这个 effect 没有任何依赖,只在首次渲染时执行。

Custom Hook

也可以使用 useEffectuseState 实现自定义 hook。

一个给 DOM 元素添加事件监听器的栗子。

import { useRef, useEffect } from "react";

type EventType = keyof HTMLElementEventMap;
type Handler = (event: Event) => void;

const useEventListener = (
  eventName: EventType,
  handler: Handler,
  element: EventTarget = document
) => {
  // 这里使用 `useRef` 来保存传入的监听器,
  // 在监听器变更后,去更新 `useRef` 返回的对象的 `current` 属性
  const saveHandler = useRef<Handler>();

  useEffect(() => {
    saveHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const supported = element && element.addEventListener;
    if (!supported) {
      return;
    }

    const listener: Handler = (event: Event) => (saveHandler.current as Handler)(event);

    element.addEventListener(eventName, listener);

    return () => {
      element.removeEventListener(eventName, listener);
    };
  }, [eventName, element]);
}

一个使用 useReducer 来实现加、减计数器的栗子。这里虽然使用 useReducer 创建了类似 redux 的 功能,但是如果有多个组件都引用了这个 hook,那么这个 hook 提供的状态是相互独立、互不影响的,即 useReducer 只提供了状态管理,但是并没有提供数据持久化的功能。redux 却提供了一种全局维护同一个数据源的机制。所以可以利用 useReducerContext 来实现数据持久化的功能。

import React, { useReducer } from "react";

const INCREMENT = "increment";
const DECREMENT = "decrement";

const initHandle = (initCount) => {
  return { count: initCount };
};

const reducer = (state, action) => {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    case "reset":
      return { count: action.payload };
    default:
      return state;
  }
};

const Counter = ({ initialCount }) => {
  const [state, dispatch] = useReducer(reducer, initialCount, initHandle);
  const { count } = state;

  return (
    <div>
      Counter: {count}
      <button type="button" onClick={() => dispatch({ type: "reset", payload: initialCount })}>
        Reset
      </button>
      <button type="button" onClick={() => dispatch({ type: INCREMENT })}>
        +
      </button>
      <button type="button" onClick={() => dispatch({ type: DECREMENT })}>
        -
      </button>j
    </div>
  );
};

一个对封装数据请求栗子。

import { useState, useEffect } from "react";
import axios, { AxiosRequestConfig } from "axios";

interface RequestError {
  error: null | boolean;
  message: string;
}

const requestError: RequestError = {
  error: null,
  message: "",
};

/**
 * @param url request url
 * @param initValue if initValue changed, the request will send again
 * @param options request config data
 *
 * @returns a object contains response's data, request loading and request error
 */
const useFetchData = (url: string, initValue: any, options: AxiosRequestConfig = {}) => {
  const [data, saveData] = useState();
  const [loading, updateLoading] = useState(false);
  const [error, updateError] = useState(requestError);

  let ignore = false;

  const fetchData = async () => {
    updateLoading(true);

    const response = await axios(url, options);

    if (!ignore) saveData(response.data);

    updateLoading(false);
  };

  useEffect(() => {
    try {
      fetchData();
    } catch (error) {
      updateError({ error: true, message: error.message });
    }

    return () => {
      ignore = true;
    };
  }, [initValue]);

  return { data, loading, error };
};

export { useFetchData };

Rules of Hook

随来 hooks 带来了新的组件编写范式,但是下面两条规则还是要开发者注意的。

  1. 在顶部使用 hook,不要使用 hook 在条件判断,循环,嵌套函数。
  2. 只在 function component 中使用 hook,或者自定义 hook 中使用 hook, 不要在常规的 JavaScript 函数中使用 hook

新的问题

hooks 的带来,虽然解决之前存在的一些问题,但是也带来了新的问题。

  1. 异常捕获。之前的版本中,我们可以用 componentDidCatch 来捕获组件作用域内的异常,做一些提示。但是在 hooks 中 ,我们只能使用 try {} catch(){} ` 去捕获,使用姿势也比较别扭。
  2. 一个组件若有状态,则状态一旦改变,所有的子组件需要重新渲染。所以一个有状态的组件,应该是没有子组件的。即 有状态的组件不做渲染,有渲染的组件没有状态
  3. 状态变更的函数不支持回调。this.setState() 中支持第二个参数,允许我们在状态变更后,传入回调函数做一些其他事情。但是 useState 不支持。详见
参考链接
making-sense-of-react-hooks
rehooks
awesome-react-hooks
如何使用useEffect来获取数据
hooks 是如何工作的
更多关于 hooks 的讨论

LeapFE
1.1k 声望2.3k 粉丝

字节内推,发送简历至 zhengqingxin.dancing@bytedance.com