【taro react】 ---- 实现 RuiPaging 滚动到底部加载更多数据

1. 前言

在移动应用开发里,列表数据的分页加载是一个高频需求。用户在浏览列表时,为了提升体验,通常希望滚动到底部就能自动加载更多数据。 RuiPaging 组件正是基于此需求开发,它封装了分页加载的通用逻辑,借助 Taro 框架,可适配多端应用,帮助开发者快速实现列表的分页加载功能。

2. 实现分析

  1. 组件定义与类型声明:借助泛型 T 提升组件的灵活性,使组件能处理不同类型的列表数据。利用 forwardRef 让组件可以接收 ref ,方便父组件调用组件内部的方法。同时定义了 RuiPagingProps 和 RuiPagingRef 接口,明确组件的属性和可调用方法的类型。
  2. 状态管理,使用 useState 管理四个状态: 2.1 page :记录当前页码,初始值为 1。 2.2 isEnd :标记是否已加载完所有数据,初始值为 false 。 2.3 isLoading :标记是否正在加载数据,避免重复请求,初始值为 false 。 2.4 list :存储列表数据,初始值为 [] 。
  3. 自动加载逻辑:在 useEffect 钩子中,若 auto 属性为 true 且当前没有在加载数据,就触发数据加载操作。
  4. 滚动加载逻辑:通过 ScrollView 组件的 onScrollToLower 事件监听滚动到底部的操作,若未加载完所有数据且当前没有在加载数据,就触发新数据的加载。
  5. 数据处理逻辑 5.1 complete 方法:处理数据加载完成后的逻辑,更新列表数据和页码,判断是否加载完所有数据。 5.2 reload 方法:重置状态并重新加载第一页数据。
  6. 方法暴露:使用 useImperativeHandle 暴露 complete 和 reload 方法,方便父组件调用。

3. 类型定义

  1. 定义一个空的ListItem接口,作为列表项的基本类型;
  2. 导出RuiPagingProps接口,用于定义分页组件的属性,使用泛型T继承ListItem;
  3. 可选的value属性,类型为T数组,用于传递初始数据;
  4. 可选的auto属性,类型为boolean,用于控制是否自动加载第一页数据;
  5. 必需的onChange回调函数,当列表数据变化时调用,接收T数组作为参数;
  6. 可选的size属性,类型为number,用于设置每页数据条数;
  7. 必需的onQuery回调函数,用于触发数据查询,接收页码和页面大小作为参数;
  8. 必需的children属性,类型为React.ReactNode,用于接收子组件。
  9. 导出RuiPagingRef接口,用于定义分页组件的引用方法,使用泛型T继承ListItem;
  10. complete方法定义,用于完成数据加载,接收T数组或布尔值作为参数;
  11. reload方法定义,用于重新加载数据,无参数。
typescript 复制代码
interface ListItem {}
export interface RuiPagingProps<T extends ListItem> {
  value?: T[];
  auto?: boolean;
  onChange: (e: T[]) => void;
  size?: number;
  onQuery: (page: number, size: number) => void;
  children: React.ReactNode;
}

export interface RuiPagingRef<T extends ListItem> {
  complete: (value: T[] | boolean) => void;
  reload: () => void;
}

4. 传入变量和状态

  1. 使用解构赋值语法从props中提取组件需要的属性;
  2. 设置auto属性默认值为true,表示组件挂载后自动加载第一页数据;
  3. 设置onChange回调函数默认为空函数,当数据发生变化时触发;
  4. 设置size属性默认值为10,表示每次查询的数据条数;
  5. 设置onQuery回调函数默认为空函数,用于执行实际的数据查询逻辑;
  6. 提取children属性,用于渲染子组件。
  7. 初始化page状态为1,表示当前页码;
  8. 初始化isEnd状态为false,表示是否已加载完所有数据;
  9. 初始化isLoading状态为false,表示是否正在加载数据;
  10. 初始化list状态为空数组,用于存储加载到的数据列表。
ini 复制代码
const {
  auto = true,
  onChange = () => {},
  size = 10,
  onQuery = () => {},
  children,
} = props;
const [page, setPage] = useState(1);
const [isEnd, setIsEnd] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [list, setList] = useState<T[]>([]);

5. 初始化是否自动加载

  1. 使用useEffect钩子,用于在组件挂载时执行副作用操作;
  2. 检查auto属性是否为true且当前未在加载数据;
  3. 如果条件满足,设置加载状态为true,表示开始加载数据;
  4. 调用onQuery函数,传入当前页码和每页大小来获取数据。
scss 复制代码
useEffect(() => {
    if (auto && !isLoading) {
      setIsLoading(true);
      onQuery(page, size);
    }
}, []);

6. 滚动加载实现

  1. 定义一个名为onScrollToLower的函数,当用户滚动到列表底部时触发;
  2. 检查是否未达到数据末尾且当前未在加载数据;
  3. 如果条件满足,设置加载状态为true,表示开始加载数据;
  4. 调用onQuery函数,传入当前页码和每页大小来获取更多数据。
scss 复制代码
const onScrollToLower = () => {
  if (!isEnd && !isLoading) {
    setIsLoading(true);
    onQuery(page, size);
  }
};

7. 更新数据和重新加载实现

  1. 定义一个名为complete的函数,接收一个泛型数组或布尔值作为参数,用于处理分页加载完成后的逻辑;
  2. 设置加载状态为false,表示数据加载已完成;
  3. 判断传入的参数是否为数组类型;
  4. 如果参数是数组,则执行以下操作;
  5. 使用concat方法将新数据连接到现有列表末尾,创建一个新的数组;
  6. 调用onChange回调函数,将更新后的完整列表传递给父组件;
  7. 更新组件内部的状态,保存新的列表数据;
  8. 判断新获取的数据长度是否小于请求的页面大小;
  9. 如果数据长度小于页面大小,说明已加载完所有数据,设置结束状态为true;
  10. 如果数据长度等于页面大小,说明可能还有更多数据,将页码增加1;
  11. 如果传入的参数不是数组(通常表示加载失败),直接设置结束状态为true。
  12. 定义reload函数,用于重新加载数据;
  13. 将页码重置为1,从第一页开始加载;
  14. 清空当前列表数据;
  15. 将结束状态设置为false,允许重新加载数据;
  16. 调用onQuery函数,使用初始页码和页面大小开始加载数据。
scss 复制代码
const complete = (value: T[] | boolean) => {
  setIsLoading(false);
  if (Array.isArray(value)) {
    const newList = list.concat(value);
    onChange(newList);
    setList(newList);
    if (value.length < size) {
      setIsEnd(true);
    } else {
      setPage(page + 1);
    }
  } else {
    setIsEnd(true);
  }
};
const reload = () => {
  setPage(1);
  setList([]);
  setIsEnd(false);
  onQuery(1, size);
};

8. DOM 结构

ini 复制代码
<ScrollView
  className="rui-paging-content"
  scrollY
  onScrollToLower={onScrollToLower}
>
  {children}
  {list.length === 0 && isEnd && (
    <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]">
      暂无数据
    </View>
  )}
  {isEnd && list.length > 0 && (
    <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]">
      没有更多数据了
    </View>
  )}
  {isLoading && (
    <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]">
      加载中...
    </View>
  )}
</ScrollView>

9. 总结

到这一步基本功能就实现完成,当然如果需要添加下拉刷新、数据量大的虚拟列表等功能,就需要单独开发补充,到这一步已经满足基本的开发需求。

10. 完整代码

ini 复制代码
import { useEffect, useState, forwardRef, useImperativeHandle } from "react";
import { ScrollView, View } from "@tarojs/components";
import "./paging.scss";

function RuiPaging<T extends ListItem>(
  props: RuiPagingProps<T>,
  ref: React.ForwardedRef<RuiPagingRef<T>>
) {
  const {
    auto = true,
    onChange = () => {},
    size = 10,
    onQuery = () => {},
    children,
  } = props;
  const [page, setPage] = useState(1);
  const [isEnd, setIsEnd] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [list, setList] = useState<T[]>([]);
  useEffect(() => {
    if (auto && !isLoading) {
      setIsLoading(true);
      onQuery(page, size);
    }
  }, []);
  const onScrollToLower = () => {
    if (!isEnd && !isLoading) {
      setIsLoading(true);
      onQuery(page, size);
    }
  };
  const complete = (value: T[] | boolean) => {
    setIsLoading(false);
    if (Array.isArray(value)) {
      const newList = list.concat(value);
      onChange(newList);
      setList(newList);
      if (value.length < size) {
        setIsEnd(true);
      } else {
        setPage(page + 1);
      }
    } else {
      setIsEnd(true);
    }
  };
  const reload = () => {
    setPage(1);
    setList([]);
    setIsEnd(false);
    onQuery(1, size);
  };
  useImperativeHandle(ref, () => ({
    complete,
    reload,
  }));
  return (
    <ScrollView
      className="rui-paging-content"
      scrollY
      onScrollToLower={onScrollToLower}
    >
      {children}
      {list.length === 0 && isEnd && (
        <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]">
          暂无数据
        </View>
      )}
      {isEnd && list.length > 0 && (
        <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]">
          没有更多数据了
        </View>
      )}
      {isLoading && (
        <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]">
          加载中...
        </View>
      )}
    </ScrollView>
  );
}

export default forwardRef(RuiPaging);
interface ListItem {}
export interface RuiPagingProps<T extends ListItem> {
  value?: T[];
  auto?: boolean;
  onChange: (e: T[]) => void;
  size?: number;
  onQuery: (page: number, size: number) => void;
  children: React.ReactNode;
}

export interface RuiPagingRef<T extends ListItem> {
  complete: (value: T[] | boolean) => void;
  reload: () => void;
}

11. 组件使用

注意:可以配合【taro react】 ---- useModel 数据双向绑定 hook 实现进行开发,更加方便快捷,同时减少代码量。

javascript 复制代码
const paging = useRef<RuiPagingRef<Course>>(null);
const [keyword, keywordModel] = useModel("");
const [videos, model] = useModel<Course[]>([]);
// 教程列表
const getCourseList = (pageNum, pageSize) => {
  https
    .getCourseList({ keyword, classify_id: "", pageNum, pageSize })
    .then((res) => {
      const data = res?.data?.data;
      paging?.current?.complete(data?.records);
    })
    .catch((_) => {
      paging?.current?.complete(false);
    });
};
return (
  <View className="bg-[#eee]">
    <RuiPaging {...model()} ref={paging} onQuery={getCourseList}>
      <View>
        {/* 搜索框 */}
        <TutorialSearch {...keywordModel()} onSearch={init} />
        {/* 视频列表 */}
        <View className="mt-[30px] ml-[30px] mr-[30px] pb-[30px]">
          <TutorialList videos={videos} />
        </View>
      </View>
    </RuiPaging>
  </View>
);

12. 总结

  1. 代码复用性高 :将通用的分页逻辑封装在组件中,不同页面的列表只需引入该组件就能实现分页加载,减少了重复代码。
  2. 维护成本低 :分页逻辑集中管理,若需要修改或优化,只需在组件内部进行修改,无需在多个地方改动代码。
  3. 交互体验统一 :组件内部统一处理加载中和没有更多数据的提示,保证了不同列表的交互体验一致。
  4. 轻量级 :不依赖其他第三方库,减少了项目的依赖体积。
  5. 可定制性强 :可以根据项目的具体需求对组件进行修改和扩展。
相关推荐
艾小码3 小时前
React Hooks时代:抛弃Class,拥抱函数式组件与状态管理
前端·javascript·react.js
CF14年老兵4 小时前
构建闪电级i18n替代方案:我为何抛弃i18next选择原生JavaScript
前端·react.js·trae
轻语呢喃19 小时前
useRef :掌握 DOM 访问与持久化状态的利器
前端·javascript·react.js
wwy_frontend20 小时前
useState 的 9个常见坑与最佳实践
前端·react.js
egghead2631620 小时前
React组件通信
前端·react.js
小陀螺呀20 小时前
在React项目中实现Redux的完整指南
react.js
OLong21 小时前
React Update Queue 源码全链路解析:从 setState 到 DOM 更新
前端·react.js
wwy_frontend1 天前
不想装 Redux?useContext + useReducer 就够了!
前端·react.js
EndingCoder1 天前
Next.js 中间件:自定义请求处理
开发语言·前端·javascript·react.js·中间件·全栈·next.js