React组件封装(二): 懒加载下拉框 LazyLoadSelect
需求
根据项目需求,需要封装一个可以懒加载的 Select 组件,他具有以下的功能:
- 下拉框滚动到底部时,加载下一页的数据,可以自定义 pageSize。当接口返回的条数小于 pageSize 时,取消加载
- 支持自定义渲染 options
- 父组件支持修改子组件的状态
(ref + useImperativeHandle)
- 继承 Select 全部属性
封装
首先定义默认状态,如默认的请求参数 { current: 1, pageSize: 20 }
、extraParamsFromOutSide(父组件支持修改内部组件的状态)
、dataList(数据列表)
、是否还有更多数据的判断
整体架子
import { useBoolean, useDebounceFn, useRequest, useSetState } from 'ahooks';
import { Select, Spin } from 'antd';
import React, { useImperativeHandle, useState } from 'react';
const { Option } = Select;
interface Res {
data: any;
success: boolean;
code: number;
}
interface PagetionProps {
currentPage: number;
pageSize: number;
}
interface ExtraParams {
[key: string]: any;
}
interface IRequestParams extends PagetionProps, ExtraParams {}
export interface IComponentProps {
requestCallback: (params: IRequestParams) => Promise<any>; // 下拉框请求回调
pageSize?: number; // 一次请求多少条数据
emptyText?: string; // 数据请求到底后的展示文案
distanceBottom?: number; // 下拉框滚动条距离底部多少距离就触发懒加载
manual?: boolean; // 初始化下拉框的时候发请求
customizeOptions?: any; // 自定义渲染下拉框选项的函数
extraParams?: ExtraParams; // 额外的查询参数
}
export interface ILazySelectRef {
setParams: (params: ExtraParams) => void;
}
const LazyLoadSelect = React.forwardRef<ILazySelectRef, IComponentProps>(
(props, ref) => {
const {
requestCallback,
pageSize = 20,
emptyText = '没有更多了',
distanceBottom = 0,
manual = false,
customizeOptions,
extraParams,
} = props;
// 默认分页参数
const [requestParams, setRequestParams] = useSetState<PagetionProps>({
currentPage: 1,
pageSize,
});
//外部组件可能额外改变的参数
const [extraParamsFromOutSide, setExtraParamsFromOutSide] =
useState<ExtraParams>({});
// 是否还有更多数据
const [
isEmptyData,
{ setTrue: setEmptyDataTrue, setFalse: setEmptyDataFalse },
] = useBoolean(false);
// 请求的数据列表
const [dataList, setDataList] = useState<any>([]);
// 用 useImperativeHandle 暴露一些外部ref能访问的属性
useImperativeHandle(ref, () => ({
setParams: (params: ExtraParams) => setExtraParamsFromOutSide(params),
}));
return (
<Select
showSearch
loading={requestLoading}
filterOption={false}
onPopupScroll={(e) => onPopupScroll(e)}
placeholder="请选择"
onClear={handlerClear}
{...props}
>
</Select>
);
},
);
架子搭好了,然后现在去实现数据的请求
数据请求
// 请求
const { loading: requestLoading, runAsync: runAsyncRequestList } =
useRequest(
async (params: PagetionProps) =>
await requestCallback({
...extraParams,
...extraParamsFromOutSide,
...params,
}),
{
manual,
defaultParams: [{ ...requestParams, ...extraParamsFromOutSide }],
onSuccess: (res: Res) => {
if (res.code === 200) {
// 获取列表数据
const result = res?.data || [];
setDataList([...dataList, ...result]);
// 当返回条数小于分页条数,或者没有返回数据就代表没有更多数据
if (result?.length < requestParams.pageSize) {
setEmptyDataTrue();
}
}
},
},
);
判断下拉框的滚动调是否滚动到 distanceBottom
处
// 判断是否滚动到底部
const { run: onPopupScroll } = useDebounceFn(
async (e) => {
e.persist();
const { scrollTop, offsetHeight, scrollHeight } = e.target;
// 距离底部多少距离开始加载下一页
if (
scrollTop + offsetHeight + distanceBottom >= scrollHeight &&
!isEmptyData
) {
setRequestParams({ currentPage: requestParams.currentPage + 1 });
await runAsyncRequestList({
...requestParams,
currentPage: requestParams.currentPage + 1,
});
}
},
{ wait: 350 },
);
清空选择框的回调、重新请求的回调
// 重新请求
const initRequest = async () => {
// 搜索重置分页
setRequestParams({ currentPage: 1 });
setEmptyDataFalse();
// 重置数据列表
setDataList([]);
await runAsyncRequestList({
...requestParams,
...extraParams,
currentPage: 1,
});
};
// 清除内容时的回调
const handlerClear = async () => {
// 当数据不足一页时,重新加载
if (dataList.length < requestParams.pageSize) {
initRequest();
}
};
自定义渲染 Options
<Select>
{/* 渲染自定义 options */}
{customizeOptions
? customizeOptions(dataList)
: dataList?.map((item: any) => {
return (
<Option {...item} value={item.value} key={item.id}>
{item.label}
</Option>
);
})}
</Select>
整体代码
/*
* @Description: Select 下拉懒加载组件
* @Version: 2.0
* @Author: Cyan
* @Date: 2023-03-10 10:04:17
* @LastEditors: liuxiqin
* @LastEditTime: 2023-08-22 16:07:10
*/
import { useBoolean, useDebounceFn, useRequest, useSetState } from 'ahooks';
import { Select, Spin } from 'antd';
import React, { useImperativeHandle, useState } from 'react';
const { Option } = Select;
interface Res {
data: any;
success: boolean;
code: number;
}
interface PagetionProps {
currentPage: number;
pageSize: number;
}
interface ExtraParams {
[key: string]: any;
}
interface IRequestParams extends PagetionProps, ExtraParams {}
export interface IComponentProps {
requestCallback: (params: IRequestParams) => Promise<any>; // 下拉框请求回调
pageSize?: number; // 一次请求多少条数据
emptyText?: string; // 数据请求到底后的展示文案
distanceBottom?: number; // 下拉框滚动条距离底部多少距离就触发懒加载
manual?: boolean; // 初始化下拉框的时候发请求
customizeOptions?: any; // 自定义渲染下拉框选项的函数
extraParams?: ExtraParams; // 额外的查询参数
}
export interface ILazySelectRef {
setParams: (params: ExtraParams) => void;
}
const LazyLoadSelect = React.forwardRef<ILazySelectRef, IComponentProps>(
(props, ref) => {
const {
requestCallback,
pageSize = 20,
emptyText = '没有更多了',
distanceBottom = 0,
manual = false,
customizeOptions,
extraParams,
} = props;
// 默认分页参数
const [requestParams, setRequestParams] = useSetState<PagetionProps>({
currentPage: 1,
pageSize,
});
//外部组件可能额外改变的参数
const [extraParamsFromOutSide, setExtraParamsFromOutSide] =
useState<ExtraParams>({});
// 是否还有更多数据
const [
isEmptyData,
{ setTrue: setEmptyDataTrue, setFalse: setEmptyDataFalse },
] = useBoolean(false);
// 请求的数据列表
const [dataList, setDataList] = useState<any>([]);
// 请求
const { loading: requestLoading, runAsync: runAsyncRequestList } =
useRequest(
async (params: PagetionProps) =>
await requestCallback({
...extraParams,
...extraParamsFromOutSide,
...params,
}),
{
manual,
defaultParams: [{ ...requestParams, ...extraParamsFromOutSide }],
onSuccess: (res: Res) => {
if (res.code === 200) {
// 获取列表数据
const result = res?.data || [];
setDataList([...dataList, ...result]);
// 当返回条数小于分页条数,或者没有返回数据就代表没有更多数据
if (result?.length < requestParams.pageSize) {
setEmptyDataTrue();
}
}
},
},
);
// 判断是否滚动到底部
const { run: onPopupScroll } = useDebounceFn(
async (e) => {
e.persist();
const { scrollTop, offsetHeight, scrollHeight } = e.target;
// 距离底部多少距离开始加载下一页
if (
scrollTop + offsetHeight + distanceBottom >= scrollHeight &&
!isEmptyData
) {
setRequestParams({ currentPage: requestParams.currentPage + 1 });
await runAsyncRequestList({
...requestParams,
currentPage: requestParams.currentPage + 1,
});
}
},
{ wait: 350 },
);
// 重新请求
const initRequest = async () => {
// 搜索重置分页
setRequestParams({ currentPage: 1 });
setEmptyDataFalse();
// 重置数据列表
setDataList([]);
await runAsyncRequestList({
...requestParams,
...extraParams,
currentPage: 1,
});
};
// 清除内容时的回调
const handlerClear = async () => {
// 当数据不足一页时,重新加载
if (dataList.length < requestParams.pageSize) {
initRequest();
}
};
// 用 useImperativeHandle 暴露一些外部ref能访问的属性
useImperativeHandle(ref, () => ({
setParams: (params: ExtraParams) => setExtraParamsFromOutSide(params),
}));
return (
<Select
showSearch
loading={requestLoading}
filterOption={false}
onPopupScroll={(e) => onPopupScroll(e)}
placeholder="请选择"
onClear={handlerClear}
{...props}
>
{/* 渲染自定义 options */}
{customizeOptions
? customizeOptions(dataList)
: dataList?.map((item: any) => {
return (
<Option {...item} value={item.value} key={item.id}>
{item.label}
</Option>
);
})}
{/* 没有更多数据 */}
{isEmptyData && (
<Option disabled>
<div
style={{
color: '#bfbfbf',
textAlign: 'center',
fontSize: 12,
pointerEvents: 'none',
}}
>
{emptyText}
</div>
</Option>
)}
{/* 下拉加载按钮 */}
{requestLoading && (
<Option disabled>
<div style={{ textAlign: 'center', pointerEvents: 'none' }}>
<Spin size="small" />
</div>
</Option>
)}
</Select>
);
},
);
export default LazyLoadSelect;
转载自:https://juejin.cn/post/7269966434125070373