react-infinite-scroll-component 实现滚动加载
效果
index.tsx
import {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite'
import styles from './index.module.scss'
import recordStore, {IData} from 'src/store/recordStore'
import {formatTimestamp} from 'src/utils'
import InfiniteScroll from 'react-infinite-scroll-component'
const TableContent: React.FC = props => {
// 查询月份下拉框
const [dropDown, setDropDown] = useState(false)
const handleOpen = (e: any) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
setDropDown(!dropDown)
}
useEffect(() => {
document.body.addEventListener('click', () => {
setDropDown(false)
})
return () => {
document.body.removeEventListener('click', () => {
setDropDown(false)
})
}
}, [])
const selectMonth = (e: any) => {
// console.log(e.currentTarget.getAttribute('data-name'))
setDropDown(!dropDown)
const month = e.currentTarget.getAttribute('data-name')
recordStore.setSelectedMonth(month)
}
// 获取最近月份记录
useEffect(() => {
if (recordStore.selectedMonth) {
recordStore.fetchMonthlyData()
}
}, [recordStore.selectedMonth])
return (
<div className={styles.container}>
<div className={styles.title}>
<div className={styles.sum}>合计:{recordStore.totalAmount}</div>
<div className={styles.search}>
查询
<div className={styles.select}>
<ul>
<li>
<div className={styles.head} onClick={handleOpen}>
<span>{recordStore.selectedMonth}</span>
<span className={styles.icon_down}></span>
</div>
<ul className={dropDown ? styles.option : styles.hide}>
{recordStore.monthList &&
recordStore.monthList.map((item: string, index: number) => (
<li key={`month_${index}`} data-name={item} className={styles.item} onClick={selectMonth}>
{item}
</li>
))}
</ul>
</li>
</ul>
</div>
</div>
</div>
<div className={styles.table}>
<div className={styles.thead}>
<p>时间</p>
<p>昵称</p>
<p>项目</p>
<p>数量</p>
<p>花费</p>
</div>
<InfiniteScroll
className={styles.scroll}
dataLength={recordStore.dataList.length}
next={() => {
recordStore.fetchDataList()
}}
hasMore={recordStore.hasMore}
height={320}
key={recordStore.scrollKey}
>
<div className={styles.tbody}>
{recordStore.dataList &&
recordStore.dataList.map((item: IData, index: number) => (
<div key={`data_tab1_${index}`} className={styles.row}>
<p>{formatTimestamp(item.createTime)}</p>
<p className={styles.nickname}>{item.nickname}</p>
<p>{item.itemName}</p>
<p>{item.amount}</p>
<p>{item.citrineAmount}</p>
</div>
))}
{!recordStore.dataList && (
<div style={{textAlign: 'center', lineHeight: '200px', color: 'rgba(0,0,0,0.3)'}}>暂无数据</div>
)}
</div>
</InfiniteScroll>
</div>
</div>
)
}
export default observer(TableContent)
recordStore.ts
import {makeAutoObservable, runInAction} from 'mobx'
import {formatMonth} from 'src/utils'
import {getDataList, listMonth} from 'src/services/api'
export interface IData {
nickname: string // 昵称
createTime: number // 时间
itemName: string // 项目名称
itemId: number // 项目id
amount: number // 数量
citrineAmount: number // 花费
}
class RecordStore {
scrollKey = 'init1' // InfiniteScroll key1
scrollKey2 = 'init2' // InfiniteScroll key2
selectedMonth = '' // 选中月份
monthList: Array<string> = [] // 月份列表
totalAmount: number // 总额度1
totalAmount2: number // 总额度2
dataList: Array<IData> = [] // 数据列表1
page = 0 // 当前页码1
hasMore = true // 有无下一页信息1
dataList2: Array<IData> = [] // 数据列表2
page2 = 0 // 当前页码2
hasMore2 = true // 有无下一页信息2
constructor() {
makeAutoObservable(this)
}
// 更改选中月份
async setSelectedMonth(month = '') {
runInAction(() => {
this.selectedMonth = month
})
}
// 获取月份列表
async getMonthList() {
const res = await listMonth()
if (res.status === 0) {
runInAction(() => {
this.monthList = res.data
this.selectedMonth = res.data.length > 0 && res.data[0]
})
}
}
// 获取月份记录
async fetchMonthlyData(tab = 1, size = 11) {
if (tab === 1) {
runInAction(() => {
this.scrollKey = Math.random().toString(36).substr(2)
this.page = 1
this.hasMore = true
})
} else {
runInAction(() => {
this.scrollKey2 = Math.random().toString(36).substr(2)
this.page2 = 1
this.hasMore = true
})
}
// ↓↓↓↓↓↓↓↓↓↓-测试数据-↓↓↓↓↓↓↓↓↓↓
const item = {
yyno: 1,
nickname: '昵称昵称',
createTime: 1655275863,
itemName: '金拱门',
itemId: 1,
amount: 100,
citrineAmount: 1000,
}
const d: any = []
for (let i = 0; i < 11; i++) {
d.push(item)
}
// ↑↑↑↑↑↑↑↑↑↑-测试数据-↑↑↑↑↑↑↑↑↑↑
const res = await getDataList({
tab: tab,
month: formatMonth(this.selectedMonth),
size: size,
page: tab === 1 ? this.page : this.page2,
})
if (res.status === 0) {
if (tab === 1) {
runInAction(() => {
// ↓↓↓↓↓↓↓↓↓↓-测试数据-↓↓↓↓↓↓↓↓↓↓
res.data.datas = d
// ↑↑↑↑↑↑↑↑↑↑-测试数据-↑↑↑↑↑↑↑↑↑↑
this.totalAmount = res.data.totalAmount
this.dataList = res.data.datas
if ((this.dataList && this.dataList.length < size) || !this.dataList) {
this.hasMore = false
}
// ↓↓↓↓↓↓↓↓↓↓-测试数据加载完毕-↓↓↓↓↓↓↓↓↓↓
if (!this.dataList || (this.dataList && this.dataList.length > 100)) {
this.hasMore = false
}
// ↑↑↑↑↑↑↑↑↑↑-测试数据加载完毕-↑↑↑↑↑↑↑↑↑↑
})
} else {
runInAction(() => {
this.totalAmount2 = res.data.totalAmount
this.dataList2 = res.data.datas && this.dataList2.concat(...res.data.datas)
if ((this.dataList2&& this.dataList2.length < size) || !this.dataList2) {
this.hasMore2 = false
}
})
}
}
}
// 分页获取帮扶/被帮扶记录
async fetchDataList(tab = 1, size = 11) {
if (tab === 1) {
runInAction(() => {
this.page++
})
} else {
runInAction(() => {
this.page2++
})
}
// ↓↓↓↓↓↓↓↓↓↓-测试数据-↓↓↓↓↓↓↓↓↓↓
const item = {
nickname: '我的昵称',
createTime: 1655275863,
itemName: '麦当当',
itemId: 1,
amount: 100,
citrineAmount: 1000,
}
const d: any = []
for (let i = 0; i < 11; i++) {
d.push(item)
}
// ↑↑↑↑↑↑↑↑↑↑-测试数据-↑↑↑↑↑↑↑↑↑↑
const res = await getDataList({
tab: tab,
month: formatMonth(this.selectedMonth),
size: size,
page: tab === 1 ? this.page : this.page2,
})
if (res.status === 0) {
if (tab === 1) {
runInAction(() => {
// ↓↓↓↓↓↓↓↓↓↓-测试数据-↓↓↓↓↓↓↓↓↓↓
res.data.datas = d
// ↑↑↑↑↑↑↑↑↑↑-测试数据-↑↑↑↑↑↑↑↑↑↑
this.totalAmount = res.data.totalAmount
this.dataList = res.data.datas && this.dataList.concat(...res.data.datas)
if ((this.dataList && this.dataList.length < size) || !this.dataList) {
this.hasMore = false
}
// ↓↓↓↓↓↓↓↓↓↓-测试数据加载完毕-↓↓↓↓↓↓↓↓↓↓
if (!this.dataList || (this.dataList && this.dataList.length > 100)) {
this.hasMore = false
}
// ↑↑↑↑↑↑↑↑↑↑-测试数据加载完毕-↑↑↑↑↑↑↑↑↑↑
})
} else {
runInAction(() => {
this.totalAmount2 = res.data.totalAmount
this.dataList2 = res.data.datas && this.dataList2.concat(...res.data.datas)
if ((this.dataList2 && this.dataList2.length < size) || !this.dataList2) {
this.hasMore2 = false
}
})
}
}
}
}
export default new RecordStore()
index.module.scss
li {
list-style: none;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
width: 416px;
margin: 18px 12px 0;
padding: 0 18px 0 22px;
font-size: 12px;
.sum {
color: #ff6600;
}
.search {
display: flex;
line-height: 24px;
ul {
margin-left: 8px;
width: 96px;
height: 24px;
border: 1px solid #dcdcdc;
border-radius: 5px;
background: #eeeeee;
font-size: 12px;
cursor: pointer;
li {
position: relative;
.head {
overflow: hidden;
width: 100%;
height: 24px;
box-sizing: border-box;
line-height: 20px;
padding-left: 15px;
.icon_down {
position: absolute;
top: 8px;
right: 16px;
width: 11px;
height: 7px;
background-image: url(../imgs/icon_down.png);
background-size: 100%;
}
}
.option {
width: 96px;
position: absolute;
top: 23px;
left: -9px;
border: none;
border-radius: 0;
box-sizing: border-box;
z-index: 1;
.item {
line-height: 20px;
padding-left: 18px;
background: #eeeeee;
height: 24px;
&:hover {
background: #cccccc;
}
}
}
.hide {
display: none;
}
}
}
}
}
.table {
width: 416px;
margin: 18px 0 0 12px;
font-size: 12px;
.thead {
display: flex;
align-items: center;
height: 30px;
margin-bottom: 4px;
background: #f7f3ff;
font-weight: bold;
}
.tbody{
height: 330px;
}
// react-infinite-scroll-component
.row {
display: flex;
align-items: center;
height: 30px;
width: 100%;
.nickname{
width: 90px;
@include text-overflow()
}
}
.thead p,
.row p {
display: inline-block;
text-align: center;
&:nth-child(1) {
width: 88px;
}
&:nth-child(2) {
width: 96px;
}
&:nth-child(3) {
width: 77px;
}
&:nth-child(4) {
width: 70px;
}
&:nth-child(5) {
width: 81px;
}
}
}
// react-infinite-scroll-component
// 滚动条样式
*::-webkit-scrollbar {
width: 12px;
height: 12px;
}
*::-webkit-scrollbar-button {
width: 0px;
height: 0px;
display: none;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
height: 6px;
border-radius: 25px;
background-clip: padding-box;
background-color: #eeeeee;
}
补充
react-infinite-scroll-component 是一款滚动加载插件,在页面滚动的时候加载数据。
在页面滚动的时候,infiniteScroll 页面数是自动+1。举个例子,页面加载到第三页,infiniteScroll 无法在重新开始计数,而在某些场景中需要重新开始计数,比如上文中提到的切换月份,page number 需要重新计数。
解决方案: 改变 infiniteScroll 的 key
值
具体细节实现见以下博客: