use + Suspense 新思路极简代码实现数据加载更多

共 12121字,需浏览 25分钟

 ·

2024-06-18 09:00

React 19 的开发体验实在是太好了!

自从彻底掌握了 React 19 之后,我感觉自己更爱写 React 代码了。比如,像分页列表这种复杂交互,核心逻辑只需要简单几行代码就可以搞定。

分页列表是我们日常开发中,比较常见的需求。其中,通过点击或者滚动来触发加载更多是主流的交互方式之一。

这篇文章要带大家实现的效果如下图所示。

为了便于大家更容易理解和消化,我们先通过一个更简单的案例来理解代码思路,然后再实现最终目标。

  • 传统方式实现请求结果新增到列表中
  • react19 中实现新增列表内容
  • react19 中通过点击按钮实现分页列表加载更多

0

传统方案实现请求结果新增到列表

首先,先定义请求数据的 promise

// api.js
export const getMessage = async () => {
  const res = await fetch('https://api.chucknorris.io/jokes/random')
  return res.json()
}

然后需要定义一个状态用于存储列表。

const [list, updateList] = useState([])

由于每一项在请求时,都需要显示一个 Loading 状态,此时我们可以使用一个巧妙的方式来解决这个问题。那就是暂时往 list 中新增一条 type: loading 的数据。在遍历的时候判断出该数据渲染成 Skeleton 组件。

因此,我们单独声明一个列表组件 List,该组件接收 list 作为参数

function List(props{
  const list = props.list
  return (
    <>
      {list.map((item, index) => {
        if (item.type === 'loading') {
          return <Skeleton />
        }
        return <Userinfo index={index} username={item.id} message={item.value} />
      })}
    </>

  )
}

当我们在发送请求时,先往 list 中新增一条 type: loading 的数据。此时我们利用 list 的特性与闭包的缓存特性,在接口请求成功之后再把请求过来的有效数据更新到 list 中即可。

代码如下

useEffect(() => {
  updateList([...list, {type'loading'}])
  getMessage().then(res => {
    updateList([...list, res])
  })
}, []);

完整代码如下:

import {use, useState, Suspense, useEffect} from 'react'
import Userinfo from './Userinfo'
import Skeleton from './Skeleton'
import Button from './Button'
import {getMessage} from './api'

export default function Demo01({
  const [list, updateList] = useState([])

  useEffect(() => {
    updateList([...list, {type'loading'}])
    getMessage().then(res => {
      updateList([...list, res])
    })
  }, []);

  function __handler({
    updateList([...list, {type'loading'}])
    getMessage().then(res => {
      updateList([...list, res])
    })
  }

  return (
    <>
      <div className='text-right mb-4'>
        <Button onClick={__handler}>新增数据</Button>
      </div>
      <List list={list} />
    </>

  )
}

function List(props{
  const list = props.list
  return (
    <>
      {list.map((item, index) => {
        if (item.type === 'loading') {
          return <Skeleton />
        }
        return <Userinfo index={index} username={item.id} message={item.value} />
      })}
    </>

  )
}

1

新的思路

旧的思路在实现上非常巧妙。但是简洁度依然弱于新的实现方案。除此之外,旧的实现思路还有许多问题需要处理,例如初始化时请求了两次,我们要考虑接口防重的问题。以及当我们多次连续点击按钮时,会出现竞态问题而导致渲染结果出现混乱。

我们基于 use + Suspense 的思路来考虑新的方案。

首先,我们应该将数据存储在 promise 中,因此很自然就能想到,多个数据,那么我们应该需要维护多个 promise,因此,我们需要定义一个由 promise 组成的数组。

const [promise, updatePromise] = useState(() => [getMessage()])

由于初始化时,我们需要自动请求一条数据,因此我们给该数组的初始值为 [getMessage()]

点击时,需要新增一个数据,那么其实就是新增一个 promise,所以代码也非常简单,就是如下所示

function __handler({
  updatePromise([...promise, getMessage()])
}

处理好之后,我们只需要使用 map 遍历该数组即可。在遍历逻辑中,每一项都返回 Suspense 包裹的子组件。我们将 promise 传递给该子组件,并在子组件中使用 use 读取 promise 中的值。

最终的代码实现如下。

export default function Demo01({
  const [promise, updatePromise] = useState(() => [getMessage()])

  function __handler({
    updatePromise([...promise, getMessage()])
  }

  return (
    <>
      <div className='text-right mb-4'>
        <Button onClick={__handler}>新增数据</Button>
      </div>
      {promise.map((item, index) => (
        <Suspense fallback={<Skeleton />} key={`hello ${index}`}>
          <User promise={item} index={index} />
        </Suspense>
      ))}
    </>

  )
}

function User(props{
  const result = use(props.promise)
  return (
    <Userinfo index={props.index} username={result.id} message={result.value} />
  )
}

此时通过案例演示结果可以观察到,初始化时的接口重复问题被解决掉了,并且当我们多次连续点击新增时,也不会出现接口竞态混乱的问题。

希望大家能够通过这个案例,进一步感受到新的开发思维的强大之处。

2

点击按钮实现分页列表加载更多

我们可以在思维上将上一节的解决方案扩展到分页列表中,加载更多的场景。

这里唯一的一个小区别就是,上一章中,我们只在 promise 中存储了一条数据。如果我们将一页数据也存在 promise 中呢?

加载更多的分页逻辑就会变得非常简单。为了方便演示,我们这里以一页数据只有三条为例。

首先简单约定接口,该接口返回一页数据。3条

// api.js
const count = 3;
const fakeDataUrl = `https://randomuser.me/api/?results=${count}&inc=name,gender,email,nat,picture&noinfo`;

export const fetchList = async () => {
  const res = await fetch(fakeDataUrl)
  return res.json()
}

然后定义一个可以遍历显示一页数据的组件。该组件接收一个 promise,并使用 use 读取请求结果。

// List.jsx
import { use } from 'react';
export default function CurrentList({promise}{
  const {results} = use(promise)
  return (
    <div>
      {results.map((item, i) => (
        <div key={item.name.last} className='flex border-b py-4 mx-4 items-center'>
          <div className='flex-1'>
            <div className='flex'>
              <img className='w-14 h-14 rounded-full' src={item.picture.large} alt='' />
              <div className='flex-1 ml-4'>
                <div className='font-bold'>{item.name.last}</div>
                <div className='text-gray-400 mt-3 text-sm line-clamp-1'>react 19 re, a design language for background applications</div>
              </div>
            </div>

            <div className='mt-4 line-clamp-2 text-sm'>We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.</div>
          </div>

          <img
            className='w-52 ml-2'
            alt="logo"
            src="https://qinglite-1253448069.cos.ap-shanghai.myqcloud.com/web/96b2b85f4c53744407dbed03982cf81c0e1dc322"
          />

        </div>
      ))}
    </div>

  )
}

此时我们稍微梳理一下逻辑,首先我们有多个 promise,然后每个 promise 中有一页数据,因此,我们可以遍历 promise,并在遍历中渲染能显示一页数据的 List 组件。

因此,我们首先要定义一个状态用于保存 promise 数组

const [promises, increasePromise] = useState(() => [fetchList()])

初始化时需要渲染一页数据,所以我们设置该数组的默认值为 [fetchList()]

loadmore 事件触发之后,我们只需要往该数组中新增一个 promise 即可

const onLoadMore = () => {
  increasePromise([...promises, fetchList()])
};

然后遍历 promises,在遍历中使用 Suspense 包裹内部有 use 逻辑的 List 组件

{promises.map((promise, i) => (
  <Suspense fallback={<Skeleton />} key={`hello ${i}`}>
    <List promise={promise} />
  </Suspense>

))}

注意看,完整的代码

const Index = () => {
  const [promises, increasePromise] = useState(() => [fetchList()])

  const onLoadMore = () => {
    increasePromise([...promises, fetchList()])
  };

  return (
    <>
      {promises.map((promise, i) => (
        <Suspense fallback={<Skeleton />} key={`hello ${i}`}>
          <List promise={promise} />
        </Suspense>
      ))}
      <div className='text-center my-4'>
        <Button onClick={onLoadMore}>loading more</Button>
      </div>
    </>

  );
};
export default Index;

非常 nice,我们用极简的代码实现了复杂的交互逻辑。

i

分页参数的维护、最后一页的判断,大家在实践中要自行维护,这里只做方案的演示,没有考虑所有边界情况

3

合集介绍

本文内容与案例来自于我倾力打造的付费小册 《React 19》。这本小册将会是市面上学习体验最好质量最高的小册,没有之一

在这本小册的文章中,所有的案例,都不再是以截图的形式展示,而是以可操作,可交互的真实组件渲染而成。你可以轻松感受案例的最终形态。扫清学习过程中的认知差异。

除此之外,最终的完整代码,与最佳实践的案例演示,都会呈现在右侧区域。你还可以通过修改代码实时查看不同逻辑下的运行结果,学习效果直接翻倍。

并且每一个案例,我都精心设计了 UI 与 Loading 效果。确保案例也有最好的学习体验。而不是简单粗糙的案例。

小册内容会包含大量实战案例,确保每一位学完《React 19》的小伙伴都能所学即所得,并且在必要的案例中,我还会详细对比新旧方案的差异。目前该小册内容已经完成了一大半。

浏览 54
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报