React 19 的开发体验实在是太好了!
自从彻底掌握了 React 19 之后,我感觉自己更爱写 React 代码了。比如,像分页列表这种复杂交互,核心逻辑只需要简单几行代码就可以搞定。
分页列表是我们日常开发中,比较常见的需求。其中,通过点击或者滚动来触发加载更多是主流的交互方式之一。
这篇文章要带大家实现的效果如下图所示。
为了便于大家更容易理解和消化,我们先通过一个更简单的案例来理解代码思路,然后再实现最终目标。
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》的小伙伴都能所学即所得,并且在必要的案例中,我还会详细对比新旧方案的差异。目前该小册内容已经完成了一大半。