前言
在React
应用开发中,数据请求
是一个极其常见的需求。几乎每个页面都需要处理加载状态
、错误处理
和重新加载
逻辑。
接下来,本文将详细介绍如何通过自定义Hook
将这些重复的逻辑封装起来
,实现代码的复用和整洁
。
正文
一、为什么需要自定义Hook?
在React
开发中,我们经常会遇到这样的场景 :多个组件需要相同的功能逻辑,如数据请求、状态管理
等。如果每个组件都重复编写
这些代码,会导致
:
- 代码冗余,维护困难
- 容易产生不一致的实现
- 业务逻辑分散,难以追踪
自定义Hook正是为了解决这些问题而生的。它让我们可以将组件逻辑提取到可重用的函数中,保持组件的简洁性。
二、基础实现:未封装的数据请求
让我们先看看未封装前的组件实现:
jsx
export default function App() {
const [courses, setCourses] = useState([]);
const [keyword, setKeyword] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetchData = async () => {
try {
const { data } = await get('/search', { q: keyword });
setCourses(data.courses);
} catch (err) {
setError(true);
} finally {
setLoading(false);
}
};
const onReload = async () => {
setLoading(true);
setError(false);
await fetchData();
}
useEffect(() => {
fetchData();
}, [keyword]);
if (loading) {
return <Loading />;
}
if (error) {
return <NetworkError onReload={onReload} />;
}
return (
<View>
{/* 页面内容 */}
</View>
);
}
这种实现方式有几个明显的问题:
- 状态管理代码占据了组件的大部分内容
- 数据请求逻辑与UI渲染逻辑混杂
- 在其他组件中需要重复相同的模式
三、创建自定义Hook:useFetchData
为了解决上述问题,我们可以创建一个自定义Hook来封装数据请求逻辑。以下是完整的实现:
jsx
import { useState, useEffect } from 'react';
import { get } from '../utils/request';
/**
* 自定义Hook用于获取数据
* @param {string} url - API请求路径(如'/search')
* @param {Object} params - 查询参数(如{ q: keyword })
* @returns {{
* data: Object,
* loading: boolean,
* error: boolean,
* onReload: Function,
* setData: Function
* }}
*/
const useFetchData = (url, params = {}) => {
const [data, setData] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
/**
* 请求接口数据
* @returns {Promise<void>}
*/
const fetchData = async () => {
try {
setLoading(true);
setError(false);
const { data: responseData } = await get(url, params);
setData(responseData);
} catch (err) {
setError(true);
} finally {
setLoading(false);
}
};
/**
* 重新加载数据
* @returns {Promise<void>}
*/
const onReload = async () => {
await fetchData();
};
useEffect(() => {
// 将params对象转为字符串,避免因引用变化导致的重复请求
const paramsStr = JSON.stringify(params);
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, paramsStr]);
return {
data,
loading,
error,
onReload,
setData // 允许外部修改data
};
};
export default useFetchData;
关键点解析:
-
状态管理 :
Hook
内部管理了data
、loading
和error
三种状态,外部组件无需关心这些状态的维护。 -
参数处理 :接受
url
和params
两个参数,params
参数会被用于API请求
。 -
依赖处理 :
useEffect
的依赖项包括url
和params
。由于params
是对象,直接作为依赖项会导致每次渲染都触发effect
(因为对象引用不同),所以将其转为字符串后再作为依赖项。 -
返回对象 :返回了
data
、loading
、error
、onReload
和setData
,提供了完整的数据请求功能集。
四、优化后的组件实现
使用自定义Hook后,组件的代码变得非常简洁:
jsx
import { TextInput, StyleSheet, Text, View } from 'react-native';
import { useState } from 'react';
import Loading from './components/shared/Loading';
import NetworkError from './components/shared/NetworkError';
import useFetchData from './hooks/useFetchData';
export default function App() {
const [keyword, setKeyword] = useState('');
const { data, loading, error, onReload } = useFetchData('/search', { q: keyword });
const { courses = [] } = data;
if (loading) {
return <Loading />;
}
if (error) {
return <NetworkError title="网络错误,请重试" onReload={onReload} />;
}
return (
<View style={styles.container}>
<Text>您搜索的关键词是:{keyword}</Text>
<TextInput
style={styles.input}
placeholder="请输入要搜索的课程"
value={keyword}
onChangeText={setKeyword}
/>
{/* 渲染课程列表 */}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 16,
paddingHorizontal: 8,
},
});
优势分析:
-
代码简洁:组件只关注UI渲染,业务逻辑被完全抽离。
-
复用性强:任何需要数据请求的组件都可以使用这个Hook。
-
一致性:所有组件都遵循相同的数据请求模式,便于维护。
-
可测试性:Hook可以单独测试,组件测试更加简单。
五、用法
1. 依赖项处理
在useEffect中,我们使用了JSON.stringify(params)
来避免因对象引用变化导致的重复请求。这是因为:
jsx
// 这样会导致问题
useEffect(() => {
fetchData();
}, [url, params]); // params对象每次都是新的引用
// 正确的做法
useEffect(() => {
const paramsStr = JSON.stringify(params);
fetchData();
}, [url, paramsStr]); // 依赖字符串而非对象
2. 取消请求
在实际应用中,我们还需要考虑组件卸载时取消未完成的请求:
jsx
const useFetchData = (url, params = {}) => {
// ...其他代码...
useEffect(() => {
let isMounted = true;
const paramsStr = JSON.stringify(params);
const fetchData = async () => {
try {
setLoading(true);
setError(false);
const { data: responseData } = await get(url, params);
if (isMounted) {
setData(responseData);
}
} catch (err) {
if (isMounted) {
setError(true);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false;
// 这里也可以使用axios的CancelToken或其他取消机制
};
}, [url, paramsStr]);
// ...其他代码...
};
3. 类型安全(使用TypeScript)
如果使用TypeScript,我们可以为Hook添加类型定义:
typescript
interface UseFetchDataResult<T> {
data: T;
loading: boolean;
error: boolean;
onReload: () => Promise<void>;
setData: React.Dispatch<React.SetStateAction<T>>;
}
function useFetchData<T = any>(url: string, params?: object): UseFetchDataResult<T>;
总结
自定义Hook
是React
中强大的代码复用机制。通过将数据请求逻辑封装到useFetchData Hook
中,我们获得了以下好处:
- 关注点分离 :
UI
组件只负责渲染,业务逻辑由Hook
处理。 - 代码复用:多个组件可以共享相同的数据请求逻辑。
- 一致性:所有组件遵循相同的数据处理模式。
- 可维护性:业务逻辑集中管理,修改时只需改动一处。
理解并掌握自定义Hook的使用,当然,扩展这个Hook,比如添加缓存、请求节流、错误重试等功能。自己去封装一些hook,提升我们的React开发效率和代码质量。