MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 使用高阶组件封装 API 请求

2023-10-292.2k 阅读

一、高阶组件(HOC)概述

在 React 开发中,高阶组件(Higher - Order Component,简称 HOC)是一种非常强大的模式。它本质上是一个函数,这个函数接收一个组件作为参数,并返回一个新的组件。这种模式遵循了“组合优于继承”的原则,为 React 组件提供了一种复用逻辑的有效方式。

高阶组件不会修改传入的组件,也不会使用继承来复制其行为。相反,它通过将组件包装在容器组件中来增强其功能。这种方式使得我们可以在不改变现有组件内部代码的情况下,为其添加新的特性。

1.1 HOC 的作用

  1. 代码复用:假设有多个组件都需要进行相同的操作,比如日志记录、权限验证等。通过 HOC,我们可以将这些通用逻辑提取到 HOC 中,然后将需要这些功能的组件传入 HOC,从而避免在每个组件中重复编写相同的代码。
  2. 逻辑抽象:将一些与业务逻辑无关的功能,如数据获取、错误处理等,从具体的业务组件中分离出来。这样可以使业务组件更加专注于自身的 UI 展示和交互逻辑,提高代码的可维护性和可读性。
  3. 增强组件功能:为现有的组件添加新的属性和方法。例如,为一个普通的展示组件添加数据更新的能力,使其能够根据新的数据重新渲染。

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;

在上述代码中:

  1. withAPIFetch 是一个高阶组件,它接收 urlmethod 作为参数。
  2. 内部返回的函数接收一个 WrappedComponent,然后返回一个新的函数组件。
  3. 在新的函数组件中,使用 useState 钩子来管理数据(data)、加载状态(loading)和错误(error)。
  4. 使用 useEffect 钩子在组件挂载时发起 API 请求。根据 method 的值,使用不同的 fetch 方式。如果请求成功,将数据设置到 data 中;如果请求失败,将错误设置到 error 中。最后,无论请求结果如何,都将 loading 设置为 false
  5. 最后,将 dataloadingerror 以及其他传入的 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 只需要关心如何根据 dataloadingerror 进行 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;

在上述代码中:

  1. CreateUserComponent 是一个用于创建用户的组件,它通过 createUser 函数来触发创建用户的 API 请求。
  2. 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 参数,用于接收自定义的请求配置。在发起请求时,将 customConfigmethod 等合并成最终的请求配置。

五、处理复杂场景

在实际项目中,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 请求场景,使得业务组件更加简洁和可维护。