如何写自定义 React Hook?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何写自定义 React Hook?相关的知识,希望对你有一定的参考价值。

参考技术A

大家好,我是在学习 React 的前端西瓜哥。

我们在写 React 函数组件时,如果想要复用组件的部分逻辑,可以考虑写自定义 Hook。本文会教大家如何写自定义 React Hook。

在此之前,我们先了解一下 Hook 的使用规则。

首先 Hook 只能在函数组件的顶层使用,不能在循环、条件、嵌套函数中执行。

这和 React Hook 的实现原理有关。当第一次执行函数组件时,React 会分配一个对象,然后一个个调用 Hook 时,将传入的参数或得到的结果依次放入到有序的表中缓存起来,然后保存在该对象中。

之后再次执行函数组件时,必须要保证这些 Hook 的执行顺序相同,才能做依赖项的对比,以及和前一次渲染的状态的对比等对比逻辑。

如果能 hook 可以出现循环、条件、嵌套函数中,就不能保证 Hook 执行顺序不变。

此外Hook 只能在函数组件内工作,不要在非组件函数中使用 Hook

如果你在普通函数内使用了 Hook,React 会报错。

当然如果是自定义 Hook(一个使用了 React 内置 Hook 的普通函数),然后放到函数组件内,那也是合法的。

为防止开发者不小心写岔,React 官方还写了一个名为 eslint-plugin-react-hooks 的 ESLint 插件,用于检测不合法的 Hook 调用位置,实在是太贴心了。强烈建议使用。

自定义 Hook,就是使用了内置 Hook 的普通函数。它是对函数组件可复用的部分逻辑的做了一层封装,然后再被函数组件使用。

自定义的 Hook 必须使用 use 开头 。因为 React 的官方 ESLint 插件认为 use 开头是自定义 Hook。

如果你不用 use 开头,ESLint 插件就会认为这是一个普通函数,调用时不符合 Hook 规则时不会报错,你就可能写出有问题的代码。

另外,自定义 Hook 下使用的 Hook,也必须位于顶层,这也是为了保证 hook 的顺序多次执行能保持一致。

下面我们来写几个常用的自定义 Hook。

我们希望实现类似类函数 componentDidMount 的效果。

使用方式:

相比原来的 useEffect 的实现,做了封装后的 useMount 更语义化一些。此外也不需要写多余的依赖项,并将组件销毁的回调函数也隔离出去了。

类似类函数 componentWillUnmount 的 useUnmount Hook 实现如下:

下面我们再实现一个 useEffect 的剔除掉挂载那一次的版本,对标类函数的 componentDidUpdate。

思路其实很简单,就是多使用一个默认值为 true 的布尔值变量。我们用它来记录当前是否为第一次执行,如果是,就不执行传入的函数,然后将布尔值设置为 false。

false 代表之后都是第二次第三次的执行,每次都执行传入的函数。

这里我们没有用 useState,因为通过 setState 方法会重新渲染组件。所以我们使用了 useRef,修改它的值不会触发组件的更新。

自定义 Hook,是一种比组件更小粒度的可复用逻辑组织方式,这也是 React 函数组件带给我们最大的惊喜。为此,我们有必要学习好自定义 Hook。

如何测试使用自定义 TypeScript React Hook 的组件?

【中文标题】如何测试使用自定义 TypeScript React Hook 的组件?【英文标题】:How to test a component which is using a custom TypeScript React Hook? 【发布时间】:2020-01-15 11:58:42 【问题描述】:

我目前正在 Typescript 中编写一个 React 组件,它使用一个名为 useAxios 的 axios-hooks 钩子。这个钩子的一个使用例子是here:

 export const App = () => 
  const [ data, loading, error , refetch] = useAxios(
    "https://api.myjson.com/bins/820fc"
  );

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error!</p>;

  return (
    <div>
      <button onClick=e => refetch()>refetch</button>
      <pre>JSON.stringify(data, null, 2)</pre>
    </div>
  );
;

const rootElement = document.getElementById("root");
render(<App />, rootElement);

我试图弄清楚如何编写一个可以模拟 useAxios 钩子的测试。我尝试创建底层 axios 组件的模拟,但无法正常工作:

import React from "react"
import  render  from "@testing-library/react"
import  Test  from "../test"
import useAxios from "axios-hooks"

jest.mock("axios-hooks")
const mockedAxios = useAxios as jest.Mocked<typeof useAxios>

it("Displays loading", () => 
  // How to mock the return values in the following line?
  // mockedAxios.

  const  getByText  = render(<Test />)

  expect(getByText("Loading...")).toBeDefined()
)

我知道我不应该模拟作为底层依赖项的 axios,我应该能够模拟 useAxios,但无论如何我都会尝试。

我意识到这个问题在 SO 上已经被多次提及,但我可以找到解决这个特定用例的方法。

非常感谢任何帮助!

【问题讨论】:

如果你jest.mock("axios-hooks") 和后来的useAxious.mockResolvedValue(dataToRespond) 会发生什么?还是您需要基于 URL 的动态模拟? 当我使用打字稿时,我似乎无法做到这一点。我认为有办法使用 TS 做到这一点? 所以问题特定于 TS 中的类型检查,对吧?对我来说,它看起来像是测试运行,但模拟并没有在这样的事情上工作 @skyboyer 我用示例测试更新了我的问题。我试图弄清楚如何模拟 TS 中的钩子返回值。 【参考方案1】:

模拟模块并为每个测试设置useAxios 的预期结果,例如

jest.mock('axios-hooks');

import useAxios from 'axios-hooks';

test('App displays loading when request is fetching', () => 
  useAxios.mockReturnValue(Promise.resolve( loading: true ));
  // mount component
  // Verify "Loading" message is rendered
);

【讨论】:

我正在使用 TypeScript,但这个答案似乎不起作用 @BenSmith TS 不应该真正参与其中,究竟是什么不起作用? 看来确实如此。另请参阅我更新的问题,其中包含用 TS 编写的不完整测试。 @BenSmith TS 应该是非侵入式的(除非改变了),所以上面的代码应该运行。看看你的例子,你已经添加了const mockedAxios = useAxios as jest.Mocked&lt;typeof useAxios&gt;,它把我们带到了 TS 领域,但这真的有必要吗?您是否按原样尝试过没有任何 TS 的代码?【参考方案2】:

我自己想出了如何做到这一点。为了测试自定义钩子,我做了以下操作:

import * as useAxios from "axios-hooks"
jest.mock("axios-hooks")
const mockedAxios = useAxios as jest.Mocked<typeof useAxios>

it("Displays loading message", async () => 

  // Explicitly define what should be returned
  mockedAxios.default.mockImplementation(() => [
      
        data: [],
        loading: true,
        error: undefined
      ,
      () => undefined
    ])

  const  getByText  = render(<Test />)

  expect(getByText("Loading...")).toBeDefined()
)

【讨论】:

【参考方案3】:

为了让编译器对() =&gt; undefined 参数感到满意,我不得不跳过一些额外的环节。我不是双重as 的粉丝,但我不知道如何使它不那么冗长,因为我是 TS 新手。

import * as useAxios from 'axios-hooks';
import  AxiosPromise  from 'axios';
import React from 'react';
import Test from 'components/Test';
import  render  from '@testing-library/react';

jest.mock('axios-hooks');
const mockedUseAxios = useAxios as jest.Mocked<typeof useAxios>;

it('renders a loading message', async () => 
  mockedUseAxios.default.mockImplementation(() => [
    
      data: [],
      loading: true,
      error: undefined,
    ,
    (): AxiosPromise => (undefined as unknown) as AxiosPromise<unknown>,
  ]);

  const  getByText  = render(<Test />);

  expect(getByText('Loading...')).toBeDefined();
);

【讨论】:

您的答案与我的完全相同,因为返回函数的变化。你为什么不发表评论? 我考虑了这一点,但我的回答还包括额外的导入语句以及您的示例中缺少的相关导入。非常感谢您在这里提出问题并为故障排除提供良好的起点。有时,在单个标记实例中以整体合理的方法获得答案会很好。 好吧,说得很好,我很高兴能帮上忙。我经常参考我自己的答案来嘲笑这个库,因为它每次都让我着迷!

以上是关于如何写自定义 React Hook?的主要内容,如果未能解决你的问题,请参考以下文章

如何测试使用自定义TypeScript React Hook的组件?

返回组件数组的 React 自定义 Hook

react 实现自定义的hook

React-hook-form v7 - 自定义输入警告功能组件不能被赋予参考

实现一个自定义 React Hook:useLocalStorageState

实现一个自定义 React Hook:useLocalStorageState