Skip to content

Commit

Permalink
feat: load transaction list in batches to enable infinite scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
malfynnction committed Jan 20, 2024
1 parent b741925 commit cc08fb6
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 14 deletions.
39 changes: 39 additions & 0 deletions src/components/InfiniteScroll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useState } from 'react'

type Props = {
children: React.ReactNode
batchSize: number
onLoadMore: (offset: number) => Promise<boolean>
}

const InfiniteScroll = ({ children, batchSize, onLoadMore }: Props) => {
const [batchOffset, setBatchOffset] = useState(0)
const [hasMore, setHasMore] = useState(true)

// load more items whenever batchOffset is changed
useEffect(() => {
// don't run on initial render or when there are no more items to load
if (batchOffset !== 0 && hasMore) {
onLoadMore(batchOffset).then(setHasMore)
}
}, [batchOffset]) // eslint-disable-line react-hooks/exhaustive-deps

const onScroll = () => {
const scrollTop = document.documentElement.scrollTop
const scrollHeight = document.documentElement.scrollHeight
const clientHeight = document.documentElement.clientHeight

if (scrollTop + clientHeight >= scrollHeight) {
setBatchOffset(batchOffset + batchSize)
}
}

useEffect(() => {
window.addEventListener('scroll', onScroll)
return () => window.removeEventListener('scroll', onScroll)
})

return children
}

export default InfiniteScroll
58 changes: 46 additions & 12 deletions src/components/TransactionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { FunnelIcon } from '@heroicons/react/24/solid'
import TransactionFilters from './TransactionFilters'
import { safeName } from '../lib/name-helper'
import { XMarkIcon } from '@heroicons/react/20/solid'
import InfiniteScroll from './InfiniteScroll'

const transactionApi = api('transactions')
const categoryApi = api('categories')
Expand All @@ -46,11 +47,14 @@ const egg = `
</span>
`

const batchSize = 50

const TransactionsList = ({ budget }: Props) => {
const { t }: Translation = useTranslation()
const [searchParams, setSearchParams] = useSearchParams()

const [isLoading, setIsLoading] = useState(true)
const [isLoadingMore, setIsLoadingMore] = useState(false)
const [showFilters, setShowFilters] = useState(false)
const [accounts, setAccounts] = useState<Account[]>([])
const [groupedEnvelopes, setGroupedEnvelopes] = useState<GroupedEnvelopes>([])
Expand All @@ -73,13 +77,7 @@ const TransactionsList = ({ budget }: Props) => {
}

Promise.all([
transactionApi
.getAll(budget, activeFilters)
.then(transactions =>
setGroupedTransactions(
groupBy(transactions, ({ date }: Transaction) => formatDate(date))
)
),
loadTransactionBatch(0),
categoryApi.getAll(budget).then((categories: Category[]) =>
setGroupedEnvelopes(
categories.map(category => ({
Expand All @@ -94,6 +92,28 @@ const TransactionsList = ({ budget }: Props) => {
})
}, [budget, searchParams]) // eslint-disable-line react-hooks/exhaustive-deps

const loadTransactionBatch = (
offset: number,
appendTransactions: boolean = false
) => {
return transactionApi
.getBatch(budget, offset, batchSize, activeFilters)
.then(transactions => {
const allTransactions = appendTransactions
? [
...transactions,
...Object.values(groupedTransactions).flat(),
].sort((a: Transaction, b: Transaction) => {
return Date.parse(b.date) - Date.parse(a.date)
})
: transactions
setGroupedTransactions(
groupBy(allTransactions, ({ date }: Transaction) => formatDate(date))
)
return transactions
})
}

const displayValue = (
filter: keyof FilterOptions,
value: string | boolean
Expand Down Expand Up @@ -251,11 +271,25 @@ const TransactionsList = ({ budget }: Props) => {
<LoadingSpinner />
) : Object.keys(groupedTransactions).length ? (
<div className="sm:card">
<GroupedTransactions
budget={budget}
accounts={accounts}
transactions={groupedTransactions}
/>
<InfiniteScroll
batchSize={batchSize}
onLoadMore={offset => {
setIsLoadingMore(true)
return loadTransactionBatch(offset, true).then(
(transactions: Transaction[]) => {
setIsLoadingMore(false)
return transactions.length > 0
}
)
}}
>
<GroupedTransactions
budget={budget}
accounts={accounts}
transactions={groupedTransactions}
/>
{isLoadingMore && <LoadingSpinner />}
</InfiniteScroll>
</div>
) : (
t('transactions.emptyList')
Expand Down
19 changes: 17 additions & 2 deletions src/lib/api/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,26 @@ const api = (linkKey: string) => {
url.searchParams.set(key, value.toString())
}
})
// Set the limit to -1 to retrieve all resources - if unset the backend defaults to 50,
// but we don't have endless scroll implemented yet
// Set the limit to -1 to retrieve all resources - if unset the backend defaults to 50
url.searchParams.set('limit', '-1')
return get(url.href)
},
getBatch: (
parent: ApiObject,
offset: Number,
limit: Number = 50,
filterOptions: FilterOptions = {}
) => {
const url = new URL(parent.links[linkKey])
Object.entries(filterOptions).forEach(([key, value]) => {
if (typeof value !== 'undefined') {
url.searchParams.set(key, value.toString())
}
})
url.searchParams.set('limit', limit.toString())
url.searchParams.set('offset', offset.toString())
return get(url.href)
},
get: (id: UUID, parent: ApiObject) => {
const url = new URL(parent.links[linkKey])
url.pathname += `/${id}`
Expand Down

0 comments on commit cc08fb6

Please sign in to comment.