引言

React Hooks 已经成为 React 开发中不可或缺的一部分。 它们让函数组件也能拥有 state 和生命周期方法,极大地提高了代码的可复用性和可维护性。 本文将分享 5 个我经常使用的 React Hooks 技巧,可以帮助你提高开发效率,写出更简洁、更易于维护的代码。 为了让学习过程更有趣,我们还会用 React Hooks 实现一个简单的 “猜数字” 游戏!

1. 使用 useCallback 避免不必要的渲染

问题

当你将一个函数作为 prop 传递给子组件时,每次父组件重新渲染,该函数都会被重新创建,导致子组件不必要的渲染。 这会降低应用的性能,尤其是在子组件比较复杂的情况下。

解决方案

使用 useCallback Hook 缓存函数,只有当依赖项发生变化时才重新创建函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useCallback } from 'react';

function ParentComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // 依赖项为空,handleClick 函数只会被创建一次

return <ChildComponent onClick={handleClick} />;
}

function ChildComponent({ onClick }) {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
}

useCallback 示意图

  • 代码解释:
    • useCallback 接收两个参数:
      • 第一个参数是要缓存的函数。
      • 第二个参数是依赖项数组。 只有当依赖项数组中的值发生变化时,useCallback 才会重新创建函数。
    • 在本例中,依赖项数组为空,因此 handleClick 函数只会被创建一次。

原理

useCallback 返回一个 memoized 回调函数。 Memoization 是一种优化技术,它将函数的计算结果缓存起来,当下次使用相同的参数调用该函数时,直接返回缓存的结果,而不需要重新计算。

优点

  • 避免子组件不必要的渲染,提高性能。

缺点

  • 需要手动管理依赖项。 如果依赖项数组中的值没有正确设置,可能会导致缓存失效,反而降低性能。

最佳实践

  • 只在需要避免不必要渲染的情况下使用 useCallback
  • 仔细检查依赖项数组,确保包含所有需要监听的值。
  • 可以使用 ESLint 插件 (例如 eslint-plugin-react-hooks) 来检查 Hooks 的使用是否正确。

2. 使用 useMemo 缓存计算结果

问题

当你需要进行复杂的计算时,每次组件重新渲染,都会重新计算,影响性能。 例如,对一个大型数组进行排序或过滤。

解决方案

使用 useMemo Hook 缓存计算结果,只有当依赖项发生变化时才重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { useMemo, useState } from 'react';

function Component({ list }) {
const [sortOrder, setSortOrder] = useState('asc');

const sortedList = useMemo(() => {
console.log('Sorting list');
const newList = [...list]; // Create a copy to avoid mutating the original array
return newList.sort((a, b) => {
if (sortOrder === 'asc') {
return a - b;
} else {
return b - a;
}
}); // 模拟复杂计算
}, [list, sortOrder]); // 只有当 list 或 sortOrder 发生变化时才重新计算

return (
<div>
<button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
Change Sort Order
</button>
<ul>
{sortedList.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}

useMemo 示意图

  • 代码解释:
    • useMemo 接收两个参数:
      • 第一个参数是要缓存的计算函数。
      • 第二个参数是依赖项数组。 只有当依赖项数组中的值发生变化时,useMemo 才会重新计算。
    • 在本例中,只有当 listsortOrder 发生变化时,才会重新对 list 进行排序。

原理

useMemo 返回一个 memoized 值。 与 useCallback 类似,useMemo 也使用了 memoization 技术。

优点

  • 避免重复计算,提高性能。

缺点

  • 需要手动管理依赖项。

最佳实践

  • 只在计算量比较大的情况下使用 useMemo
  • 避免在 useMemo 中执行副作用操作。
  • 仔细检查依赖项数组,确保包含所有需要监听的值。

3. 自定义 Hook 提取逻辑

问题

在多个组件中重复使用相同的逻辑,导致代码冗余,难以维护。 例如,从 API 获取数据、处理表单输入等。

解决方案

创建自定义 Hook 提取可复用的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { useState, useEffect } from 'react';

function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
setLoading(true);
fetch(url)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! Status: ${res.status}`);
}
return res.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);

return { data, loading, error };
}

function Component() {
const { data, loading, error } = useFetch('https://api.example.com/data');

if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;

return (
<ul>
{data && data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

![自定义 Hook 示意图](images/测/自定义 Hook.png)

  • 代码解释:
    • useFetch Hook 封装了从 API 获取数据的逻辑。
    • 它接收一个 url 参数,并返回 dataloadingerror 三个状态。
    • Component 组件使用 useFetch Hook 获取数据,并根据状态显示不同的内容。

原理

自定义 Hook 本质上是一个函数,它以 use 开头,并且可以调用其他的 Hook。

优点

  • 提高代码复用性,减少代码冗余。
  • 使组件更简洁,更易于理解。
  • 提高代码的可测试性。

缺点

  • 需要仔细设计 Hook 的 API。
  • 过度使用自定义 Hook 可能会导致代码难以理解。

最佳实践

  • 只提取可复用的逻辑到自定义 Hook 中。
  • 为自定义 Hook 提供清晰的 API。
  • 编写单元测试来测试自定义 Hook。

4. 使用 useRef 访问 DOM 元素

问题

需要直接访问 DOM 元素,例如获取输入框的值、控制滚动条的位置或实现焦点管理。

解决方案

使用 useRef Hook 创建一个 ref 对象,并将其绑定到 DOM 元素。

1
2
3
4
5
6
7
8
9
10
11
import React, { useRef, useEffect } from 'react';

function Component() {
const inputRef = useRef(null);

useEffect(() => {
inputRef.current.focus(); // 组件挂载后自动聚焦输入框
}, []);

return <input type="text" ref={inputRef} />;
}

useRef 示意图

  • 代码解释:
    • useRef(null) 创建一个 ref 对象,初始值为 null
    • ref={inputRef} 将 ref 对象绑定到 input 元素。
    • inputRef.current 指向 input 元素。
    • useEffect 在组件挂载后执行,inputRef.current.focus() 使输入框获得焦点。

原理

useRef 返回一个可变的 ref 对象,它的 .current 属性可以被修改,并且不会触发组件重新渲染。

优点

  • 可以直接访问 DOM 元素。
  • 可以在组件的整个生命周期内保存值。

缺点

  • 需要小心使用,避免直接操作 DOM 元素导致的问题。 例如,直接修改 DOM 元素的样式可能会与 React 的虚拟 DOM 机制冲突。

最佳实践

  • 只在必要的情况下使用 useRef 访问 DOM 元素。
  • 避免在 useEffect 中直接操作 DOM 元素,可以使用 requestAnimationFrame 来优化性能。

5. 使用 useContext 共享状态

问题

需要在多个组件之间共享状态,例如用户登录信息、主题颜色或语言设置。 使用 props 逐层传递状态比较繁琐,难以维护。

解决方案

使用 useContext Hook 和 Context API 共享状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}

function Component() {
const { theme, setTheme } = useContext(ThemeContext);

return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle theme
</button>
</div>
);
}

function App() {
return (
<ThemeProvider>
<Component />
</ThemeProvider>
);
}

useContext 示意图

  • 代码解释:
    • createContext('light') 创建一个 Context 对象,初始值为 'light'
    • ThemeProvider 组件使用 ThemeContext.Provider 提供状态,value 属性指定要共享的状态。
    • Component 组件使用 useContext(ThemeContext) 获取状态。

原理

Context API 提供了一种在组件树中共享状态的方式,而不需要手动地通过 props 逐层传递状态。

优点

  • 方便地在多个组件之间共享状态,避免了 props 逐层传递。
  • 使代码更简洁,更易于理解。

缺点

  • 可能会导致组件过度渲染。 当 Context 的值发生变化时,所有使用该 Context 的组件都会重新渲染。

最佳实践

  • 只在需要在多个组件之间共享状态的情况下使用 Context API。
  • 避免将频繁变化的状态放在 Context 中。
  • 可以使用 useReducer Hook 来管理 Context 中的复杂状态。

额外奖励:使用 React Hooks 实现 “猜数字” 游戏

游戏介绍

这是一个经典的猜数字游戏。 计算机随机生成一个 1 到 100 之间的整数,玩家需要猜测这个数字。 每次猜测后,计算机会给出提示:猜测的数字太高或太低。 玩家需要在限定的次数内猜中这个数字。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React, { useState, useRef, useEffect } from 'react';

function GuessTheNumber() {
const [secretNumber, setSecretNumber] = useState(generateSecretNumber());
const [guess, setGuess] = useState('');
const [message, setMessage] = useState('');
const [guessesRemaining, setGuessesRemaining] = useState(7); // 限制猜测次数
const inputRef = useRef(null);

// 生成 1 到 100 之间的随机整数
function generateSecretNumber() {
return Math.floor(Math.random() * 100) + 1;
}

// 处理猜测
const handleGuess = () => {
const parsedGuess = parseInt(guess, 10);

if (isNaN(parsedGuess) || parsedGuess < 1 || parsedGuess > 100) {
setMessage('请输入 1 到 100 之间的有效数字');
return;
}

if (parsedGuess === secretNumber) {
setMessage(`恭喜你猜对了! 答案是 ${secretNumber}`);
setGuessesRemaining(0); // 禁用输入
} else if (parsedGuess < secretNumber) {
setMessage('太低了!');
setGuessesRemaining(guessesRemaining - 1);
} else {
setMessage('太高了!');
setGuessesRemaining(guessesRemaining - 1);
}

setGuess(''); // 清空输入框
inputRef.current.focus(); // 聚焦输入框
};

// 重置游戏
const handleReset = () => {
setSecretNumber(generateSecretNumber());
setGuess('');
setMessage('');
setGuessesRemaining(7);
inputRef.current.focus();
};

useEffect(() => {
inputRef.current.focus(); // 初始聚焦输入框
}, []);

return (
<div>
<h2>猜数字游戏</h2>
<p>我心里有一个 1100 之间的数字。 你有 {guessesRemaining} 次机会猜中它!</p>
<input
type="number"
value={guess}
onChange={(e) => setGuess(e.target.value)}
ref={inputRef}
disabled={guessesRemaining === 0}
/>
<button onClick={handleGuess} disabled={guessesRemaining === 0}>
猜测
</button>
<p>{message}</p>
{guessesRemaining === 0 && (
<button onClick={handleReset}>重新开始</button>
)}
</div>
);
}

export default GuessTheNumber;

猜数字游戏截图

游戏原理

  1. 状态管理:

    • secretNumber: 使用 useState 存储计算机生成的随机数。
    • guess: 使用 useState 存储玩家输入的猜测。
    • message: 使用 useState 存储游戏提示信息。
    • guessesRemaining: 使用 useState 存储剩余的猜测次数。
  2. 生成随机数:

    • generateSecretNumber 函数生成 1 到 100 之间的随机整数。
  3. 处理猜测:

    • handleGuess 函数处理玩家的猜测:
      • 验证输入是否有效。
      • 比较猜测与随机数。
      • 更新 messageguessesRemaining 状态。
      • 清空输入框并聚焦。
  4. 重置游戏:

    • handleReset 函数重置游戏状态。
  5. DOM 访问:

    • useRef 用于访问输入框,并在游戏开始和每次猜测后聚焦。
  6. useEffect:

    • 确保在组件首次渲染时,输入框获得焦点。

如何在你的 Hexo 博客中使用这个游戏?

  1. 创建 React 组件: 将上面的代码保存为 GuessTheNumber.js 文件,并将其放在你的 React 项目的 src/components 目录下 (如果你的 Hexo 主题支持 React)。

  2. 在 Markdown 文件中引入组件: 使用 Hexo 插件 (例如 hexo-renderer-jsx) 在 Markdown 文件中引入 React 组件。 具体方法取决于你使用的主题和插件。 通常你需要先安装插件:

    1
    npm install hexo-renderer-jsx

    然后在 Markdown 文件中使用类似下面的语法引入组件:

    1
    <GuessTheNumber />
  3. 配置 Webpack (如果需要): 如果你的 Hexo 主题没有默认配置 Webpack,你可能需要手动配置 Webpack 来处理 React 组件。

注意: 在 Hexo 博客中集成 React 组件可能需要一些额外的配置,具体步骤取决于你使用的主题和插件。

总结

本文分享了 5 个提升 React Hooks 开发效率的实用技巧,并用 React Hooks 实现了一个简单的 “猜数字” 游戏。 希望这些技巧能够帮助你写出更简洁、更易于维护的React 代码,并让你在学习过程中充满乐趣!

参考资料