React 19 的开发体验实在是太好了!
自从彻底掌握了 React 19 之后,我感觉自己更爱写 React 代码了。比如,像分页列表这种复杂交互,核心逻辑只需要简单几行代码就可以搞定。
分页列表是我们日常开发中,比较常见的需求。其中,通过点击或者滚动来触发加载更多是主流的交互方式之一。
这篇文章要带大家实现的效果如下图所示。
为了便于大家更容易理解和消化,我们先通过一个更简单的案例来理解代码思路,然后再实现最终目标。
首先,先定义请求数据的 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} />
})}
</>
)
}
旧的思路在实现上非常巧妙。但是简洁度依然弱于新的实现方案。除此之外,旧的实现思路还有许多问题需要处理,例如初始化时请求了两次,我们要考虑接口防重的问题。以及当我们多次连续点击按钮时,会出现竞态问题而导致渲染结果出现混乱。
我们基于 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} />
)
}
此时通过案例演示结果可以观察到,初始化时的接口重复问题被解决掉了,并且当我们多次连续点击新增时,也不会出现接口竞态混乱的问题。
希望大家能够通过这个案例,进一步感受到新的开发思维的强大之处。
我们可以在思维上将上一节的解决方案扩展到分页列表中,加载更多的场景。
这里唯一的一个小区别就是,上一章中,我们只在 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://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
/>
</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分页参数的维护、最后一页的判断,大家在实践中要自行维护,这里只做方案的演示,没有考虑所有边界情况
本文内容与案例来自于我倾力打造的付费小册 《React 19》。这本小册将会是市面上学习体验最好质量最高的小册,没有之一。
在这本小册的文章中,所有的案例,都不再是以截图的形式展示,而是以可操作,可交互的真实组件渲染而成。你可以轻松感受案例的最终形态。扫清学习过程中的认知差异。
除此之外,最终的完整代码,与最佳实践的案例演示,都会呈现在右侧区域。你还可以通过修改代码实时查看不同逻辑下的运行结果,学习效果直接翻倍。
并且每一个案例,我都精心设计了 UI 与 Loading 效果。确保案例也有最好的学习体验。而不是简单粗糙的案例。
小册内容会包含大量实战案例,确保每一位学完《React 19》的小伙伴都能所学即所得,并且在必要的案例中,我还会详细对比新旧方案的差异。目前该小册内容已经完成了一大半。预计最迟在未来两周以内会完结上线。
该小册的上线价格预计会在 30 元到 100 之间,如果你对该小册的内容质量和学习体验比较看好,可以在该小册上线之前提前投资,你只需要点击下方红色按钮,赞赏本文任意金额元以上,即可提前购买。
赞赏之后,请务必添加我的微信好友 icanmeetu 并告知来意。