React 的自定义 Hooks 是一个让代码更加简洁、逻辑更易复用的强大工具。但对于刚接触它的新手来说,自定义 Hooks 的实际作用和使用场景可能还不够清晰。本篇文章将从零开始,结合实例,带你一步步理解自定义 Hooks 的核心目标,并展示它在状态逻辑和行为逻辑中的应用。
一、自定义 Hooks 是什么?
自定义 Hooks 是基于 React 原生 Hooks 的扩展,允许我们将常用的逻辑封装起来,并在多个组件中复用。它的名字通常以 use 开头,比如 useCounter 或 useFetch。
自定义 Hooks 主要有两大核心功能:
- 状态逻辑的复用:封装状态及其操作逻辑,减少重复代码。
- 行为逻辑的复用:封装事件监听、API 请求等副作用逻辑,统一管理复杂的组件行为。
二、为什么要用自定义 Hooks?
在开发中,如果我们发现某些逻辑在多个组件中重复出现,就可以将它抽取到一个自定义 Hook 中。这样不仅可以提高代码的可维护性,还能让组件更加专注于 UI 渲染。
自定义 Hooks 的优势:
- 逻辑抽离:将状态和行为逻辑从组件中抽离,保持组件的单一职责。
- 提高复用性:避免重复编写相似代码。
- 易于测试:自定义 Hooks 可以独立测试,保证逻辑的正确性。
三、状态逻辑复用的实践
状态逻辑复用是自定义 Hooks 的基础场景,它帮助我们管理状态及其相关操作。
示例 1:封装计数器逻辑
以下是一个简单的计数器自定义 Hook:
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return { count, increment, decrement };
}
// 使用方式
function CounterA() {
const { count, increment } = useCounter();
return <button onClick={increment}>Counter A: {count}</button>;
}
function CounterB() {
const { count, decrement } = useCounter();
return <button onClick={decrement}>Counter B: {count}</button>;
}
示例 2:封装表单输入逻辑
表单输入是常见的状态逻辑,通过自定义 Hook,我们可以快速管理输入值及其事件处理器。
function useInput(initialValue = '') {
const [value, setValue] = useState(initialValue);
const handleChange = (e) => setValue(e.target.value);
return { value, onChange: handleChange, setValue };
}
// 使用方式
function InputField() {
const name = useInput('');
const email = useInput('');
return (
<div>
<input {...name} placeholder="Name" />
<input {...email} placeholder="Email" />
<p>Name: {name.value}</p>
<p>Email: {email.value}</p>
</div>
);
}
四、行为逻辑复用的实践
行为逻辑复用更多地涉及事件处理和副作用逻辑,比如监听窗口大小、滚动位置,或封装防抖逻辑等。
示例 1:封装窗口大小监听逻辑
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
const handleResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// 使用方式
function Component() {
const { width, height } = useWindowSize();
return <p>Window size: {width} x {height}</p>;
}
示例 2:封装防抖逻辑
function useDebouncedCallback(callback, delay) {
const timeoutRef = useRef();
const debouncedCallback = (...args) => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => callback(...args), delay);
};
return debouncedCallback;
}
// 使用方式
function SearchInput() {
const [query, setQuery] = useState('');
const handleSearch = useDebouncedCallback((value) => {
console.log('Search:', value);
}, 500);
const handleChange = (e) => {
setQuery(e.target.value);
handleSearch(e.target.value);
};
return <input value={query} onChange={handleChange} placeholder="Search..." />;
}
五、状态与行为逻辑的结合
在实际开发中,状态和行为往往是紧密结合的。自定义 Hooks 允许我们将它们一并封装起来。
示例:数据请求的封装
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then((response) => response.json())
.then((data) => setData(data))
.catch((error) => setError(error))
.finally(() => 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 <pre>{JSON.stringify(data, null, 2)}</pre>;
}
六、自定义 Hooks 的测试
测试自定义 Hooks 是确保其逻辑正确性的重要一步。以下是常见的测试工具和示例。
使用 @testing-library/react-hooks
@testing-library/react-hooks 是专门用于测试 React Hooks 的工具。
示例:测试 useCounter
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter'; // 引入你的 Hook
test('should increment and decrement count', () => {
const { result } = renderHook(() => useCounter(0));
// 初始值
expect(result.current.count).toBe(0);
// 调用 increment
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
// 调用 decrement
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(0);
});
示例:测试 useFetch
import { renderHook } from '@testing-library/react-hooks';
import useFetch from './useFetch';
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 'test' }),
})
);
test('should fetch data successfully', async () => {
const { result, waitForNextUpdate } = renderHook(() => useFetch('https://api.example.com'));
// 初始状态
expect(result.current.loading).toBe(true);
// 等待数据更新
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
expect(result.current.data).toEqual({ data: 'test' });
expect(result.current.error).toBe(null);
});
小提示:在测试自定义 Hooks 时,要特别注意副作用的处理,可以通过 jest.fn() 模拟外部依赖。
七、总结
本文从自定义 Hooks 的基础概念出发,逐步展示了状态逻辑、行为逻辑及其结合的实践,并补充了自定义 Hooks 的测试方法。无论是刚接触还是希望深入了解自定义 Hooks,希望这篇文章能为你带来启发。尝试从你的项目中封装一个自定义 Hook,充分体验它带来的便利与提升吧!🚀