React 使用高阶组件封装 API 请求
一、高阶组件(HOC)概述
在 React 开发中,高阶组件(Higher - Order Component,简称 HOC)是一种非常强大的模式。它本质上是一个函数,这个函数接收一个组件作为参数,并返回一个新的组件。这种模式遵循了“组合优于继承”的原则,为 React 组件提供了一种复用逻辑的有效方式。
高阶组件不会修改传入的组件,也不会使用继承来复制其行为。相反,它通过将组件包装在容器组件中来增强其功能。这种方式使得我们可以在不改变现有组件内部代码的情况下,为其添加新的特性。
1.1 HOC 的作用
- 代码复用:假设有多个组件都需要进行相同的操作,比如日志记录、权限验证等。通过 HOC,我们可以将这些通用逻辑提取到 HOC 中,然后将需要这些功能的组件传入 HOC,从而避免在每个组件中重复编写相同的代码。
- 逻辑抽象:将一些与业务逻辑无关的功能,如数据获取、错误处理等,从具体的业务组件中分离出来。这样可以使业务组件更加专注于自身的 UI 展示和交互逻辑,提高代码的可维护性和可读性。
- 增强组件功能:为现有的组件添加新的属性和方法。例如,为一个普通的展示组件添加数据更新的能力,使其能够根据新的数据重新渲染。
1.2 HOC 的基本实现
下面是一个简单的 HOC 示例,它为传入的组件添加了一个名为 message
的属性:
import React from'react';
// 定义一个高阶组件
const withMessage = (WrappedComponent) => {
return (props) => {
return <WrappedComponent message="这是来自高阶组件的消息" {...props} />;
};
};
// 定义一个普通组件
const MyComponent = (props) => {
return <div>{props.message}</div>;
};
// 使用高阶组件增强普通组件
const EnhancedComponent = withMessage(MyComponent);
export default EnhancedComponent;
在上述代码中,withMessage
是一个高阶组件,它接收一个组件 WrappedComponent
作为参数,并返回一个新的函数组件。这个新组件在渲染 WrappedComponent
时,额外传递了一个 message
属性。
二、API 请求在 React 中的常见处理方式
在 React 应用中,与后端服务器进行数据交互是非常常见的需求。常见的处理 API 请求的方式有以下几种:
2.1 在组件内部直接处理
在组件的生命周期方法(如 componentDidMount
)或者函数组件的 useEffect
钩子中直接发起 API 请求。例如:
import React, { useEffect, useState } from'react';
const UserComponent = () => {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('https://api.example.com/user')
.then(response => response.json())
.then(data => setUser(data))
.catch(error => console.error('Error fetching user:', error));
}, []);
return (
<div>
{user? <p>{user.name}</p> : <p>加载中...</p>}
</div>
);
};
export default UserComponent;
这种方式虽然简单直接,但如果多个组件都需要进行类似的 API 请求,就会导致代码重复。而且,组件的逻辑变得复杂,既负责 UI 渲染,又负责数据获取和错误处理。
2.2 使用 Redux 等状态管理库
Redux 提供了一种集中式管理应用状态的方式。我们可以将 API 请求相关的逻辑放在 Redux 的 action 和 reducer 中。例如:
// actions/userActions.js
import axios from 'axios';
const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS';
const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE';
export const fetchUserSuccess = (user) => ({
type: FETCH_USER_SUCCESS,
payload: user
});
export const fetchUserFailure = (error) => ({
type: FETCH_USER_FAILURE,
payload: error
});
export const fetchUser = () => {
return async (dispatch) => {
try {
const response = await axios.get('https://api.example.com/user');
dispatch(fetchUserSuccess(response.data));
} catch (error) {
dispatch(fetchUserFailure(error));
}
};
};
// reducers/userReducer.js
const initialState = {
user: null,
error: null
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USER_SUCCESS:
return {
...state,
user: action.payload,
error: null
};
case FETCH_USER_FAILURE:
return {
...state,
user: null,
error: action.payload
};
default:
return state;
}
};
export default userReducer;
// components/UserComponent.js
import React from'react';
import { useSelector, useDispatch } from'react-redux';
import { fetchUser } from '../actions/userActions';
const UserComponent = () => {
const user = useSelector(state => state.user.user);
const error = useSelector(state => state.user.error);
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(fetchUser());
}, [dispatch]);
return (
<div>
{error && <p>{error.message}</p>}
{user? <p>{user.name}</p> : <p>加载中...</p>}
</div>
);
};
export default UserComponent;
这种方式将数据获取和状态管理分离,使代码结构更清晰。但对于一些小型应用或者简单的 API 请求场景,引入 Redux 可能会增加项目的复杂性。
三、使用高阶组件封装 API 请求
通过高阶组件封装 API 请求,可以有效地解决代码复用和逻辑分离的问题。下面我们逐步实现一个用于封装 API 请求的高阶组件。
3.1 创建基本的 API 请求 HOC
首先,我们创建一个高阶组件,它接收 API 的 URL 和请求方法作为参数,并为传入的组件提供请求数据和加载状态等属性。
import React, { useEffect, useState } from'react';
const withAPIFetch = (url, method = 'GET') => {
return (WrappedComponent) => {
return (props) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
let response;
if (method === 'GET') {
response = await fetch(url);
} else if (method === 'POST') {
response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(props.requestBody)
});
}
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, method, props.requestBody]);
return (
<WrappedComponent
data={data}
loading={loading}
error={error}
{...props}
/>
);
};
};
};
export default withAPIFetch;
在上述代码中:
withAPIFetch
是一个高阶组件,它接收url
和method
作为参数。- 内部返回的函数接收一个
WrappedComponent
,然后返回一个新的函数组件。 - 在新的函数组件中,使用
useState
钩子来管理数据(data
)、加载状态(loading
)和错误(error
)。 - 使用
useEffect
钩子在组件挂载时发起 API 请求。根据method
的值,使用不同的fetch
方式。如果请求成功,将数据设置到data
中;如果请求失败,将错误设置到error
中。最后,无论请求结果如何,都将loading
设置为false
。 - 最后,将
data
、loading
、error
以及其他传入的props
传递给WrappedComponent
。
3.2 使用 API 请求 HOC
接下来,我们展示如何使用这个高阶组件。假设我们有一个展示用户列表的组件:
import React from'react';
import withAPIFetch from './withAPIFetch';
const UserListComponent = ({ data, loading, error }) => {
if (loading) {
return <p>加载中...</p>;
}
if (error) {
return <p>{error.message}</p>;
}
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
const UserListWithAPI = withAPIFetch('https://api.example.com/users')(UserListComponent);
export default UserListWithAPI;
在上述代码中,我们通过 withAPIFetch('https://api.example.com/users')
为 UserListComponent
组件添加了从指定 URL 获取用户列表数据的功能。UserListComponent
只需要关心如何根据 data
、loading
和 error
进行 UI 渲染,而不需要关心数据是如何获取的。
3.3 处理不同请求方法和请求体
上述的 withAPIFetch
高阶组件已经支持了 GET 和 POST 请求。对于 POST 请求,我们通过 props.requestBody
来传递请求体数据。例如,我们有一个创建用户的组件:
import React from'react';
import withAPIFetch from './withAPIFetch';
const CreateUserComponent = ({ loading, error, createUser }) => {
const [newUser, setNewUser] = React.useState({ name: '', age: 0 });
const handleSubmit = (e) => {
e.preventDefault();
createUser({...newUser });
};
if (loading) {
return <p>创建中...</p>;
}
if (error) {
return <p>{error.message}</p>;
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="姓名"
value={newUser.name}
onChange={(e) => setNewUser({...newUser, name: e.target.value })}
/>
<input
type="number"
placeholder="年龄"
value={newUser.age}
onChange={(e) => setNewUser({...newUser, age: parseInt(e.target.value) })}
/>
<button type="submit">创建用户</button>
</form>
);
};
const CreateUserWithAPI = withAPIFetch('https://api.example.com/users', 'POST')((props) => {
const { data, loading, error } = props;
const createUser = (userData) => {
props.requestBody = userData;
// 触发重新渲染,从而发起新的 POST 请求
props.forceUpdate && props.forceUpdate();
};
return <CreateUserComponent createUser={createUser} {...props} />;
});
export default CreateUserWithAPI;
在上述代码中:
CreateUserComponent
是一个用于创建用户的组件,它通过createUser
函数来触发创建用户的 API 请求。CreateUserWithAPI
使用withAPIFetch('https://api.example.com/users', 'POST')
来封装 API 请求。在返回的新组件中,定义了createUser
函数,该函数设置props.requestBody
并触发forceUpdate
(如果存在)来发起新的 POST 请求。
四、优化 API 请求 HOC
虽然我们已经实现了一个基本的 API 请求 HOC,但还有一些地方可以优化。
4.1 缓存数据
对于一些不经常变化的数据,我们可以在 HOC 中添加缓存机制,避免重复请求。
import React, { useEffect, useState } from'react';
const apiCache = {};
const withAPIFetch = (url, method = 'GET') => {
return (WrappedComponent) => {
return (props) => {
const [data, setData] = useState(apiCache[url]? apiCache[url].data : null);
const [loading, setLoading] = useState(apiCache[url]? false : true);
const [error, setError] = useState(null);
useEffect(() => {
if (apiCache[url]) {
setData(apiCache[url].data);
setLoading(false);
return;
}
const fetchData = async () => {
setLoading(true);
try {
let response;
if (method === 'GET') {
response = await fetch(url);
} else if (method === 'POST') {
response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(props.requestBody)
});
}
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
apiCache[url] = { data: result };
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, method, props.requestBody]);
return (
<WrappedComponent
data={data}
loading={loading}
error={error}
{...props}
/>
);
};
};
};
export default withAPIFetch;
在上述代码中,我们使用一个全局对象 apiCache
来缓存 API 请求的结果。在 useEffect
中,如果缓存中已经有数据,则直接使用缓存数据,并将 loading
设置为 false
,不再发起新的请求。
4.2 支持自定义请求配置
有时候,我们可能需要更细粒度地控制 API 请求,比如设置请求头、超时时间等。我们可以扩展 withAPIFetch
高阶组件,使其支持传入自定义的请求配置。
import React, { useEffect, useState } from'react';
const apiCache = {};
const withAPIFetch = (url, method = 'GET', customConfig = {}) => {
return (WrappedComponent) => {
return (props) => {
const [data, setData] = useState(apiCache[url]? apiCache[url].data : null);
const [loading, setLoading] = useState(apiCache[url]? false : true);
const [error, setError] = useState(null);
useEffect(() => {
if (apiCache[url]) {
setData(apiCache[url].data);
setLoading(false);
return;
}
const fetchData = async () => {
setLoading(true);
try {
let response;
const requestConfig = {
...customConfig,
method
};
if (method === 'POST') {
requestConfig.body = JSON.stringify(props.requestBody);
}
response = await fetch(url, requestConfig);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
apiCache[url] = { data: result };
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, method, props.requestBody, customConfig]);
return (
<WrappedComponent
data={data}
loading={loading}
error={error}
{...props}
/>
);
};
};
};
export default withAPIFetch;
在上述代码中,withAPIFetch
增加了一个 customConfig
参数,用于接收自定义的请求配置。在发起请求时,将 customConfig
和 method
等合并成最终的请求配置。
五、处理复杂场景
在实际项目中,API 请求可能会涉及到更复杂的场景,比如分页、排序等。我们可以通过高阶组件来处理这些场景。
5.1 分页处理
假设我们有一个需要分页展示数据的组件,我们可以通过在高阶组件中传递分页参数来实现。
import React, { useEffect, useState } from'react';
const apiCache = {};
const withAPIFetch = (url, method = 'GET', customConfig = {}) => {
return (WrappedComponent) => {
return (props) => {
const { page = 1, pageSize = 10 } = props;
const cacheKey = `${url}-${page}-${pageSize}`;
const [data, setData] = useState(apiCache[cacheKey]? apiCache[cacheKey].data : null);
const [loading, setLoading] = useState(apiCache[cacheKey]? false : true);
const [error, setError] = useState(null);
useEffect(() => {
if (apiCache[cacheKey]) {
setData(apiCache[cacheKey].data);
setLoading(false);
return;
}
const fetchData = async () => {
setLoading(true);
try {
const queryParams = `?page=${page}&pageSize=${pageSize}`;
const requestUrl = url + queryParams;
let response;
const requestConfig = {
...customConfig,
method
};
if (method === 'POST') {
requestConfig.body = JSON.stringify(props.requestBody);
}
response = await fetch(requestUrl, requestConfig);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
apiCache[cacheKey] = { data: result };
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, method, props.requestBody, customConfig, page, pageSize]);
return (
<WrappedComponent
data={data}
loading={loading}
error={error}
{...props}
/>
);
};
};
};
export default withAPIFetch;
然后在使用该高阶组件的组件中传递分页参数:
import React from'react';
import withAPIFetch from './withAPIFetch';
const PagedUserListComponent = ({ data, loading, error, page, pageSize }) => {
if (loading) {
return <p>加载中...</p>;
}
if (error) {
return <p>{error.message}</p>;
}
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
const PagedUserListWithAPI = withAPIFetch('https://api.example.com/users')(PagedUserListComponent);
export default () => {
const [page, setPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(10);
return (
<div>
<PagedUserListWithAPI page={page} pageSize={pageSize} />
<button onClick={() => setPage(page - 1)} disabled={page === 1}>上一页</button>
<button onClick={() => setPage(page + 1)}>下一页</button>
</div>
);
};
5.2 排序处理
类似地,我们可以处理排序场景。假设 API 支持通过查询参数来进行排序,我们可以在高阶组件中添加排序逻辑。
import React, { useEffect, useState } from'react';
const apiCache = {};
const withAPIFetch = (url, method = 'GET', customConfig = {}) => {
return (WrappedComponent) => {
return (props) => {
const { sortBy = 'id', sortOrder = 'asc' } = props;
const cacheKey = `${url}-${sortBy}-${sortOrder}`;
const [data, setData] = useState(apiCache[cacheKey]? apiCache[cacheKey].data : null);
const [loading, setLoading] = useState(apiCache[cacheKey]? false : true);
const [error, setError] = useState(null);
useEffect(() => {
if (apiCache[cacheKey]) {
setData(apiCache[cacheKey].data);
setLoading(false);
return;
}
const fetchData = async () => {
setLoading(true);
try {
const queryParams = `?sortBy=${sortBy}&sortOrder=${sortOrder}`;
const requestUrl = url + queryParams;
let response;
const requestConfig = {
...customConfig,
method
};
if (method === 'POST') {
requestConfig.body = JSON.stringify(props.requestBody);
}
response = await fetch(requestUrl, requestConfig);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
apiCache[cacheKey] = { data: result };
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, method, props.requestBody, customConfig, sortBy, sortOrder]);
return (
<WrappedComponent
data={data}
loading={loading}
error={error}
{...props}
/>
);
};
};
};
export default withAPIFetch;
在使用该高阶组件的组件中传递排序参数:
import React from'react';
import withAPIFetch from './withAPIFetch';
const SortedUserListComponent = ({ data, loading, error, sortBy, sortOrder }) => {
if (loading) {
return <p>加载中...</p>;
}
if (error) {
return <p>{error.message}</p>;
}
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
const SortedUserListWithAPI = withAPIFetch('https://api.example.com/users')(SortedUserListComponent);
export default () => {
const [sortBy, setSortBy] = React.useState('id');
const [sortOrder, setSortOrder] = React.useState('asc');
return (
<div>
<SortedUserListWithAPI sortBy={sortBy} sortOrder={sortOrder} />
<select onChange={(e) => setSortBy(e.target.value)}>
<option value="id">按 ID 排序</option>
<option value="name">按姓名排序</option>
</select>
<select onChange={(e) => setSortOrder(e.target.value)}>
<option value="asc">升序</option>
<option value="desc">降序</option>
</select>
</div>
);
};
通过以上方式,我们可以在高阶组件中灵活处理各种复杂的 API 请求场景,使得业务组件更加简洁和可维护。