17、React Hooks:社区钩子使用、规则及自定义创建指南

React Hooks:社区钩子使用、规则及自定义创建指南

1. 社区钩子的使用与查找

在开发中,除了 React 提供的内置钩子,社区也贡献了丰富的钩子资源。当你将鼠标悬停在复制链接按钮上时,会看到相应的信息文本。

要查找更多社区钩子,可以访问 这个页面 ,这里有可搜索的各种钩子列表。以下是一些社区提供的有趣钩子:
- use-events https://github.com/sandiiarov/use-events ,它将各种 JavaScript 事件转化为钩子,例如鼠标位置、触摸事件、外部点击等。
- react-use https://github.com/streamich/react-use ,包含处理传感器(如 useBattery useIdle useGeolocation 等)、UI(如 useAudio useCss useFullscreen 等)、动画(如 useSpring useTween useRaf 等)和副作用(如 useAsync useDebounce useFavicon 等)的各种钩子。

当然,在 GitHub 和 npm 上还有更多的钩子等待发现。相关示例代码可在 Chapter10/Chapter10_3 文件夹中找到,查看该文件夹内的 README.md 文件,可获取设置和运行示例的说明。

2. 钩子使用回顾

在之前的学习中,我们掌握了多种钩子的使用方法:
- 使用本地存储钩子(Local Storage Hook)通过 LocalStorage API 在浏览器中持久存储数据。
- 在 CreatePost 组件中使用历史状态钩子(History State Hook)实现撤销/重做功能。
- 学习了防抖(debouncing)的概念,并使用防抖回调钩子(Debounced Callback Hook)实现防抖。
- 了解了一些用于复制到剪贴板和处理悬停状态的实用钩子。

为了巩固所学,我们可以思考以下问题:
1. 哪个钩子可用于在浏览器中持久存储信息?
2. 哪个钩子可用于实现撤销/重做功能?
3. 什么是防抖?为什么需要进行防抖?
4. 哪个钩子可用于防抖?
5. 防抖值与延迟值有何不同?
6. 在哪里可以找到更多钩子?

3. 钩子的使用规则

钩子的使用有一定的规则和限制,违反这些规则可能会导致错误或意外行为,因此需要严格遵守。

3.1 钩子的使用场景

钩子只能在以下场景中使用:
- React 函数组件
- 自定义钩子(后续会学习如何创建)

钩子不能在以下场景中使用:
- 条件语句或循环内部
- 条件返回语句之后
- 事件处理程序中
- 类组件中
- 传递给 Memo Reducer Effect 钩子的函数内部
- try/catch/finally 块内部

钩子本质上是普通的 JavaScript 函数,但 React 依赖于它们在函数组件内部被调用。虽然使用其他钩子的自定义钩子可以在 React 函数组件外部定义,但在使用这些自定义钩子时,必须确保在 React 组件内部调用。

3.2 钩子的调用顺序

钩子应在顶层使用(非嵌套),理想情况下在函数组件或自定义钩子的开头调用,例如:

function ExampleComponent() {
  const [name, setName] = useState('')
  // …
}
function useCustomHook() {
  const [name, setName] = useState('')
  return { name, setName }
}

不要在条件语句、循环或嵌套函数中使用钩子,因为这会改变钩子的调用顺序,从而导致状态混乱。例如,以下代码是错误的:

const [enableFirstName, setEnableFirstName] = useState(false)
const [name, setName] = enableFirstName
  ? useState('')
  : ['', () => {}]
const [lastName, setLastName] = useState('')

当点击复选框启用名字输入框时,钩子的调用顺序会发生改变,导致 firstName 字段获取到 lastName 字段的状态。为了避免这种问题,应始终定义钩子,然后有条件地渲染内容,例如:

const [enableFirstName, setEnableFirstName] = useState(false)
const [name, setName] = useState('')
const [lastName, setLastName] = useState('')
My name is: {enableFirstName ? name : ''} {lastName}
3.3 钩子的命名规范

钩子函数的命名应遵循一定的规范,即始终以 use 为前缀,后面紧跟首字母大写的钩子名称,例如 useState useEffect useQuery 。这样做的好处是可以清晰地区分普通函数和钩子函数,便于自动执行钩子规则。例如,为输入字段创建自定义钩子时,可命名为 useInputField ,这样在使用时能立即明确该钩子的用途。

3.4 自动执行钩子规则

如果遵循钩子函数以 use 为前缀的命名约定,就可以自动执行另外两条规则:
- 只能从 React 函数组件中调用钩子。
- 只能在顶层调用钩子(不在循环、条件语句或嵌套函数内部)。

React 提供了一个 ESLint 插件 eslint-plugin-react-hooks 来自动执行这些规则。ESLint 是一个代码分析工具,可检测源代码中的问题,如风格错误、潜在的 bug 和编程错误。Vite 已经为我们设置了 ESLint 及相关的 React 插件。如果违反钩子规则,ESLint 会显示错误信息。例如,移除以下禁用规则的代码:

// eslint-disable-next-line react-hooks/rules-of-hooks

就会触发 ESLint 错误。

4. 自定义钩子的创建

在掌握了内置钩子、社区钩子以及钩子的使用规则后,我们可以开始创建自己的自定义钩子。

4.1 技术要求

创建自定义钩子前,需要满足以下技术要求:
- 安装较新版本的 Node.js,可从 官方网站 获取安装信息。
- 安装 Node 包管理器(npm),它通常随 Node.js 一起安装。
- 建议使用 Visual Studio Code(VS Code)进行开发,可从 官方网站 获取安装信息。

本书使用的版本如下:
| 工具 | 版本 |
| ---- | ---- |
| Node.js | v22.14.0 |
| npm | v10.9.2 |
| Visual Studio Code | v1.97.2 |

虽然安装较新版本通常不会有问题,但某些步骤在新版本中可能会有所不同。如果遇到代码和步骤方面的问题,建议使用上述指定版本。本章代码可在 GitHub 上找到。

4.2 创建自定义主题钩子

在之前的开发中,我们引入了 ThemeContext 来为博客文章设置样式,并在多个组件中使用上下文钩子(Context Hook)访问 ThemeContext 。这种在多个组件中使用的功能通常适合封装成自定义钩子。

以下是创建自定义主题钩子 useTheme 的步骤:
1. 复制 Chapter10_3 文件夹到新的 Chapter12_1 文件夹:

$ cp -R Chapter10_3 Chapter12_1
  1. 在 VS Code 中打开新的 Chapter12_1 文件夹。
  2. 创建新的 src/hooks/ 文件夹。
  3. src/hooks/ 文件夹内创建新的 src/hooks/theme.js 文件。
  4. theme.js 文件中导入 useContext 函数和 ThemeContext
import { useContext } from 'react'
import { ThemeContext } from '@/contexts/ThemeContext.js'
  1. 定义并导出 useTheme 函数,该函数返回上下文钩子:
export function useTheme() {
  return useContext(ThemeContext)
}
4.3 使用自定义主题钩子

创建好自定义主题钩子后,我们可以在项目中使用它,步骤如下:
1. 编辑 src/components/post/Post.jsx 文件,移除以下导入:

import { useContext } from 'react'
import { ThemeContext } from '@/contexts/ThemeContext.js'

替换为 useTheme 函数的导入:

import { useTheme } from '@/hooks/theme.js'
  1. 用自定义主题钩子替换现有的上下文钩子:
export function Post({ id }) {
  const theme = useTheme()
}
  1. 编辑 src/components/post/PostListItem.jsx 文件,进行类似的操作:
import { useTheme } from '@/hooks/theme.js'
export function PostListItem({ id, title, author }) {
  const theme = useTheme()
}
  1. 运行开发服务器:
$ npm run dev

运行后会发现主题功能与之前相同,特色文章仍以不同颜色显示。使用自定义主题钩子可以简化代码(减少导入),并便于后续调整主题系统。例如,如果想从用户设置中获取默认主题,而不是从上下文中获取,可以在 useTheme 钩子中实现该功能,所有组件将自动使用新的主题系统。

4.4 创建自定义用户钩子

之前我们定义了 UserContext 来存储当前登录用户的用户名,后来用本地存储钩子替换了 UserContext ,但从上下文钩子到本地存储钩子的重构需要在多个组件中调整代码。为了避免未来出现类似问题,我们可以将所有与用户相关的信息和功能封装到一个用户钩子中。

以下是创建自定义用户钩子 useUser 的步骤:
1. 复制 Chapter12_1 文件夹到新的 Chapter12_2 文件夹:

$ cp -R Chapter12_1 Chapter12_2
  1. 在 VS Code 中打开新的 Chapter12_2 文件夹。
  2. 创建新的 src/hooks/user.js 文件。
  3. user.js 文件中导入 useLocalStorage 函数:
import { useLocalStorage } from '@uidotdev/usehooks'
  1. 定义 useUser 函数,使用本地存储钩子:
export function useUser() {
  const [username, setUsername] = useLocalStorage('username', null)
  // 可根据需要添加更多用户相关逻辑
}

相关示例代码可在 Chapter12/Chapter12_1 文件夹中找到,查看该文件夹内的 README.md 文件,可获取设置和运行示例的说明。

通过以上步骤,我们不仅学会了如何查找和使用社区钩子,还掌握了钩子的使用规则,并成功创建了自定义主题钩子和用户钩子。这将帮助我们更好地组织代码,提高代码的可维护性和复用性。在后续的开发中,我们可以根据实际需求创建更多的自定义钩子,进一步优化我们的项目。

5. 创建自定义 API 钩子

在实际开发中,与 API 进行交互是常见的需求。我们可以创建自定义 API 钩子来封装 API 请求逻辑,提高代码的复用性。

5.1 需求分析

假设我们有一个博客应用,需要从 API 获取文章列表。我们可以创建一个自定义 API 钩子来处理这个请求。

5.2 创建自定义 API 钩子步骤
  1. 复制 Chapter12_2 文件夹到新的 Chapter12_3 文件夹:
$ cp -R Chapter12_2 Chapter12_3
  1. 在 VS Code 中打开新的 Chapter12_3 文件夹。
  2. 创建新的 src/hooks/api.js 文件。
  3. api.js 文件中编写以下代码:
import { useState, useEffect } from 'react';

export function useFetchPosts() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/posts');
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setPosts(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  return { posts, loading, error };
}

上述代码中, useFetchPosts 钩子使用 useState 来管理文章列表、加载状态和错误信息。使用 useEffect 来发送 API 请求,并根据请求结果更新状态。

5.3 使用自定义 API 钩子

在组件中使用 useFetchPosts 钩子的步骤如下:
1. 编辑 src/components/post/PostList.jsx 文件,导入 useFetchPosts 钩子:

import { useFetchPosts } from '@/hooks/api.js';
  1. 在组件中使用钩子:
export function PostList() {
  const { posts, loading, error } = useFetchPosts();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

通过这种方式,我们将 API 请求逻辑封装在自定义钩子中,使得组件代码更加简洁,易于维护。

6. 创建防抖历史状态钩子

在某些场景下,我们可能需要对历史状态进行防抖处理,以避免频繁更新状态。

6.1 防抖历史状态钩子的实现思路

我们可以结合防抖钩子和历史状态钩子来创建一个防抖历史状态钩子。

6.2 创建防抖历史状态钩子步骤
  1. 复制 Chapter12_3 文件夹到新的 Chapter12_4 文件夹:
$ cp -R Chapter12_3 Chapter12_4
  1. 在 VS Code 中打开新的 Chapter12_4 文件夹。
  2. 创建新的 src/hooks/debouncedHistoryState.js 文件。
  3. debouncedHistoryState.js 文件中编写以下代码:
import { useHistoryState } from '@/hooks/historyState.js'; // 假设已有历史状态钩子
import { useDebounce } from '@/hooks/debounce.js'; // 假设已有防抖钩子

export function useDebouncedHistoryState(initialValue, delay) {
  const [state, setState] = useHistoryState(initialValue);
  const debouncedState = useDebounce(state, delay);

  return [debouncedState, setState];
}

上述代码中, useDebouncedHistoryState 钩子结合了 useHistoryState useDebounce 钩子,实现了对历史状态的防抖处理。

6.3 使用防抖历史状态钩子

在组件中使用 useDebouncedHistoryState 钩子的步骤如下:
1. 编辑 src/components/editor/Editor.jsx 文件,导入 useDebouncedHistoryState 钩子:

import { useDebouncedHistoryState } from '@/hooks/debouncedHistoryState.js';
  1. 在组件中使用钩子:
export function Editor() {
  const [content, setContent] = useDebouncedHistoryState('', 500);

  const handleChange = (e) => {
    setContent(e.target.value);
  };

  return (
    <textarea
      value={content}
      onChange={handleChange}
    />
  );
}

通过使用防抖历史状态钩子,我们可以避免在用户输入时频繁更新历史状态,提高性能。

7. 测试自定义钩子

为了确保自定义钩子的正确性和稳定性,我们需要对其进行测试。

7.1 测试工具选择

我们可以使用 React Testing Library 和 Jest 来测试自定义钩子。

7.2 测试自定义主题钩子示例

以下是测试 useTheme 钩子的示例代码:

import { renderHook } from '@testing-library/react-hooks';
import { useTheme } from '@/hooks/theme.js';
import { ThemeContext } from '@/contexts/ThemeContext.js';

describe('useTheme', () => {
  it('should return the theme from context', () => {
    const mockTheme = { primaryColor: 'blue' };
    const { result } = renderHook(() => useTheme(), {
      wrapper: ({ children }) => (
        <ThemeContext.Provider value={mockTheme}>
          {children}
        </ThemeContext.Provider>
      ),
    });

    expect(result.current).toEqual(mockTheme);
  });
});

上述代码中,使用 renderHook 函数来渲染钩子,并通过 ThemeContext.Provider 提供模拟的主题值。然后断言钩子返回的主题值与模拟值相等。

7.3 测试自定义 API 钩子示例

以下是测试 useFetchPosts 钩子的示例代码:

import { renderHook, act } from '@testing-library/react-hooks';
import { useFetchPosts } from '@/hooks/api.js';
import fetchMock from 'jest-fetch-mock';

fetchMock.enableMocks();

describe('useFetchPosts', () => {
  it('should fetch posts successfully', async () => {
    const mockPosts = [{ id: 1, title: 'Post 1' }];
    fetchMock.mockResponseOnce(JSON.stringify(mockPosts));

    const { result, waitForNextUpdate } = renderHook(() => useFetchPosts());

    await act(async () => {
      await waitForNextUpdate();
    });

    expect(result.current.posts).toEqual(mockPosts);
    expect(result.current.loading).toBe(false);
    expect(result.current.error).toBe(null);
  });

  it('should handle error', async () => {
    const mockError = new Error('Network error');
    fetchMock.mockRejectOnce(mockError);

    const { result, waitForNextUpdate } = renderHook(() => useFetchPosts());

    await act(async () => {
      await waitForNextUpdate();
    });

    expect(result.current.posts).toEqual([]);
    expect(result.current.loading).toBe(false);
    expect(result.current.error).toEqual(mockError);
  });
});

上述代码中,使用 fetchMock 来模拟 API 请求,分别测试了请求成功和请求失败的情况。

通过以上步骤,我们学会了创建自定义 API 钩子、防抖历史状态钩子,并掌握了测试自定义钩子的方法。这些技能将帮助我们更好地开发 React 应用,提高代码的质量和可维护性。整个开发流程可以用以下 mermaid 流程图表示:

graph LR
    A[社区钩子查找] --> B[掌握钩子使用规则]
    B --> C[创建自定义主题钩子]
    C --> D[创建自定义用户钩子]
    D --> E[创建自定义 API 钩子]
    E --> F[创建防抖历史状态钩子]
    F --> G[测试自定义钩子]

综上所述,从社区钩子的使用到自定义钩子的创建和测试,我们逐步深入了解了 React Hooks 的强大功能。通过合理运用这些知识,我们可以构建出更加高效、可维护的 React 应用。在实际开发中,我们可以根据具体需求灵活创建和使用各种自定义钩子,不断优化我们的项目。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值