快速上手 React Hook

2022-10-24 18:45:53 浏览数 (1)

快速上手 React Hook

HookReact 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。本篇文章将介绍React Hook相关知识。

1. Hook 和函数组件

复习一下, React 的函数组件是这样的:

代码语言:javascript复制
const Example = (props) => {
  // 你可以在这使用 Hook
  return <div />;
}

或是这样:

代码语言:javascript复制
function Example(props) {
  // 你可以在这使用 Hook
  return <div />;
}

你之前可能把它们叫做“「无状态组件」”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。

Hookclass 内部是不起作用的。但你可以使用它们来取代 class

「什么是 Hook ?」

Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 stateHook。稍后我们将学习其他 Hook

2. useState

如果你之前在 React 中使用过 class,这段代码看起来应该很熟悉:

代码语言:javascript复制
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

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

state 初始值为 { count: 0 } ,当用户点击按钮后,我们通过调用 this.setState() 来增加 state.count

useState 用于在函数组件中调用给组件添加一些内部状态 state,正常情况下纯函数不能存在状态副作用,通过调用该 Hook 函数可以给函数组件注入状态 state

我们先将上述示例改成函数组件:

代码语言:javascript复制
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 变量”。我们的变量叫 count, 但是我们可以叫他任何名字,比如 banana。这是一种在函数调用时保存变量的方式 —— useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

「useState 需要哪些参数?」 useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0 作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 useState() 两次即可。)

「useState方法的返回值是什么?」 返回值为:当前 state 以及更新 state 的函数。这就是我们写 const [count, setCount] = useState() 的原因。这与 class 里面 this.state.countthis.setState 类似,唯一区别就是你需要成对的获取它们。

既然我们知道了 useState 的作用,我们的示例应该更容易理解了。

3. useEffect

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。

React 组件中有两种常见副作用操作:需要清除的和不需要清除的。我们来更仔细地看一下他们之间的区别。

3.1 无需清除的 effect

有时候,我们只想「在 React 更新 DOM 之后运行一些额外的代码。「比如」发送网络请求」,手动变更 DOM,「记录日志」,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 classHook 都是怎么实现这些副作用的。

在 React 的 class 组件中,render 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。

这就是为什么在 React class 中,我们把副作用操作放到 componentDidMountcomponentDidUpdate 函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性

代码语言:javascript复制
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>
    );
  }
}

注意,「在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。」

这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。

现在让我们来看看如何使用 useEffect 执行相同的操作。

代码语言:javascript复制
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>
  );
}

useEffect 做了什么?」 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

「为什么在组件内部调用 useEffect?」useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗?」 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(我们稍后会谈到如何控制它。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

3.2 需要清除的 effect

之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如「订阅外部数据源」。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。

在 React class 中,你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:

代码语言:javascript复制
class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

你会注意到 componentDidMountcomponentWillUnmount 之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。

那么如何使用 Hook 编写这个组件?

你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:

代码语言:javascript复制
import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

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

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

3.3 性能优化

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevPropsprevState 的比较逻辑解决:

代码语言:javascript复制
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

代码语言:javascript复制
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

上面这个示例中,我们传入 [count] 作为第二个参数。这个参数是什么作用呢?如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。

当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。

如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 [] 作为第二个参数更接近大家更熟悉的 componentDidMountcomponentWillUnmount 思维模式,但我们有更好的方式(后面会介绍)来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。

4. useContext

Context 提供了一个无需为每层组件手动添加 props ,就能在组件树间进行数据传递的方法,useContext 用于函数组件中订阅上层 context 的变更,可以获取上层 context 传递的 value prop 值

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

代码语言:javascript复制
const value = useContext(MyContext);

看完下面案例你将会知道如何使用

代码语言:javascript复制
import React, { useContext, useState } from 'react';

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

// 为当前 theme 创建一个 context
const ThemeContext = React.createContext();

export default function Toolbar(props) {
  const [theme, setTheme] = useState(themes.dark);

  const toggleTheme = () => {
    setTheme(currentTheme => (
      currentTheme === themes.dark
        ? themes.light
        : themes.dark
    ));
  };

  return (
    // 使用 Provider 将当前 props.value 传递给内部组件
    <ThemeContext.Provider value={{theme, toggleTheme}}>
      <ThemeButton />
    </ThemeContext.Provider>
  );
}

function ThemeButton() {
  // 通过 useContext 获取当前 context 值
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button style={{background: theme.background, color: theme.foreground }} onClick={toggleTheme}>
      Change the button's theme
    </button>
  );
}

下面我们看看与之等价 class 示例

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

useContext 并没有改变使用 context 的方式,它只为我们提供了一种额外的、更漂亮的方法来使用上层 context。在将其应用于使用多 context 的组件时将会非常有用

代码语言:javascript复制
import React from 'react';

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

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

function ThemeButton() {
  return (
    <ThemeContext.Consumer>
      {
        ({theme, toggleTheme}) => (
          <button style={{background: theme.background, color: theme.foreground }} onClick={toggleTheme}>
            Change the button's theme
          </button>
        )
      }
    </ThemeContext.Consumer>
  );
}

export default class Toolbar extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = {
      theme: themes.light
    };

    this.toggleTheme = this.toggleTheme.bind(this);
  }

  toggleTheme() {
    this.setState(state => ({
      theme:
        state.theme === themes.dark
          ? themes.light
          : themes.dark
    }));
  }

  render() {
    return (
      <ThemeContext.Provider value={{ theme: this.state.theme, toggleTheme: this.toggleTheme }}>
        <ThemeButton />
      </ThemeContext.Provider>
    )
  }

5. useMemo

代码语言:javascript复制
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

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

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

6. useCallback

代码语言:javascript复制
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染,在 props 属性相同情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

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

7. useRef

代码语言:javascript复制
const refContainer = useRef(initialValue);

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

一个常见的用例便是命令式地访问子组件:

代码语言:javascript复制
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>
    </>
  );
}

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

你应该熟悉 ref 这一种访问 DOM的主要方式。如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

然而,useRef()ref 属性更有用。它可以「很方便地保存任何可变值」,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

8. useImperativeHandle

代码语言:javascript复制
useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 React.forwardRef 一起使用:

代码语言:javascript复制
function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = React.forwardRef(FancyInput);

9. useLayoutEffect

useLayoutEffectuseEffect 类似,与 useEffect 在浏览器 layout 和 painting 完成后异步执行 effect 不同的是,它会在浏览器布局 layout 之后,painting 之前同步执行 effect。

useLayoutEffect 的执行时机对比如下:

代码语言:javascript复制
import React, { useState, useEffect, useLayoutEffect } from 'react';

export default function LayoutEffect() {
  const [width, setWidth] = useState('100px');

  // useEffect 会在所有 DOM 渲染完成后执行 effect 回调
  useEffect(() => {
    console.log('effect width: ', width);
  });
  // useLayoutEffect 会在所有的 DOM 变更之后同步执行 effect 回调
  useLayoutEffect(() => {
    console.log('layoutEffect width: ', width);
  });
  
  return (
    <>
      <div id='content' style={{ width, background: 'red' }}>内容</div>
      <button onClick={() => setWidth('100px')}>100px</button>
      <button onClick={() => setWidth('200px')}>200px</button>
      <button onClick={() => setWidth('300px')}>300px</button>
    </>
  );
}

// 使用 setTimeout 保证在组件第一次渲染完成后执行,获取到对应的 DOM
setTimeout(() => {
  const contentEl = document.getElementById('content');
  // 监视目标 DOM 结构变更,会在 useLayoutEffect 回调执行后,useEffect 回调执行前调用
  const observer = new MutationObserver(() => {
    console.log('content element layout updated');
  });
  observer.observe(contentEl, {
    attributes: true
  });
}, 1000);

10. useReducer

代码语言:javascript复制
const [state, dispatch] = useReducer(reducer, initialArg, init);

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

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。

以下是用 reducer 重写 useState 一节的计数器示例:

代码语言:javascript复制
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>
    </>
  );
}

useReducer 初始化 state 的方式有两种

代码语言:javascript复制
// 方式1
const [state, dispatch] = useReducer(
 reducer,
  {count: initialCount}
);

// 方式2
function init(initialClunt) {
  return {count: initialClunt};
}

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

11. Hook 规则

Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则。我们提供了一个 linter 插件来强制执行这些规则:

  1. 「只在最顶层使用 Hook」

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

  1. 「只在 React 函数中调用 Hook」

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

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

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

官方发布了一个名为 eslint-plugin-react-hooks 的 ESLint 插件来强制执行这两条规则。如果你想尝试一下,可以将此插件添加到你的项目中:

打算后续版本默认添加此插件到 Create React App 及其他类似的工具包中。

代码语言:javascript复制
npm install eslint-plugin-react-hooks --save-dev
代码语言:javascript复制
// 你的 ESLint 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}

12. 自定义 Hook

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

在我们学习useEffect 时,我们已经见过这个聊天程序中的组件,该组件用于显示好友的在线状态:

代码语言:javascript复制
import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

现在我们假设聊天应用中有一个联系人列表,当用户在线时需要把名字设置为绿色。我们可以把上面类似的逻辑复制并粘贴到 FriendListItem 组件中来,但这并不是理想的解决方案:

代码语言:javascript复制
import React, { useState, useEffect } from 'react';

function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

相反,我们希望在 FriendStatusFriendListItem 之间共享逻辑。

目前为止,在 React 中有两种流行的方式来共享组件之间的状态逻辑: render props 和高阶组件,现在让我们来看看 Hook 是如何在让你不增加组件的情况下解决相同问题的。

  1. 「提取自定义 Hook」

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

「自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。」 例如,下面的 useFriendStatus 是我们第一个自定义的 Hook:

代码语言:javascript复制
import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

此处并未包含任何新的内容——逻辑是从上述组件拷贝来的。与组件中一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。

与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头,这样可以一眼看出其符合 「Hook 的规则」

此处 useFriendStatus 的 Hook 目的是订阅某个好友的在线状态。这就是我们需要将 friendID 作为参数,并返回这位好友的在线状态的原因。

代码语言:javascript复制
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // ...

  return isOnline;
}

现在让我们看看应该如何使用自定义 Hook。

  1. 「使用自定义 Hook」

我们一开始的目标是在 FriendStatusFriendListItem 组件中去除重复的逻辑,即:这两个组件都想知道好友是否在线。

现在我们已经把这个逻辑提取到 useFriendStatus 的自定义 Hook 中,然后就可以使用它了:

代码语言:javascript复制
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

「这段代码等价于原来的示例代码吗?」 等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。「自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。」

「自定义 Hook 必须以 “use” 开头吗?」 必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 「Hook 的规则」

「在两个组件中使用相同的 Hook 会共享 state 吗?」 不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

「自定义 Hook 如何获取独立的 state?」 每次调用 Hook,它都会获取独立的 state。由于我们直接调用了 useFriendStatus,从 React 的角度来看,我们的组件只是调用了 useStateuseEffect

「参考」

  • Hook 简介 – React (reactjs.org)

0 人点赞