Skip to content

Commit 06347dc

Browse files
authored
feat: fetch related data lazily (#68)
* feat: fetch assets within account on demand * fix: lazy load table loading animation issue
1 parent 08aeb37 commit 06347dc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+404
-294
lines changed

src/features/accounts/components/account-asset-holdings-table-columns.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { ColumnDef } from '@tanstack/react-table'
22
import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount'
33
import { AssetHolding } from '../models'
4-
import { AssetLink } from '@/features/assets/components/asset-link'
4+
import { AssetIdLink } from '@/features/assets/components/asset-link'
5+
import { AssetSummary } from '@/features/assets/models'
6+
import { AsyncMaybeAtom } from '@/features/common/data/types'
7+
import { RenderInlineAsyncAtom } from '@/features/common/components/render-inline-async-atom'
58

69
export const accountAssetHoldingsTableColumns: ColumnDef<AssetHolding>[] = [
710
{
811
header: 'ID',
9-
accessorFn: (item) => item.asset.id,
10-
cell: (c) => <AssetLink assetId={c.getValue<number>()} />,
12+
accessorFn: (item) => item.assetId,
13+
cell: (c) => <AssetIdLink assetId={c.getValue<number>()} />,
1114
},
1215
{
1316
header: 'Name',
14-
accessorFn: (item) => item.asset.name,
17+
accessorFn: (item) => item.asset,
18+
cell: (c) => {
19+
const assetSummaryAtom = c.getValue<AsyncMaybeAtom<AssetSummary>>()
20+
return <RenderInlineAsyncAtom atom={assetSummaryAtom}>{(asset) => asset.name}</RenderInlineAsyncAtom>
21+
},
1522
},
1623
{
1724
header: 'Amount',

src/features/accounts/components/account-assets-created.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { ColumnDef } from '@tanstack/react-table'
2-
import { AccountAssetSummary } from '../models'
32
import { DataTable } from '@/features/common/components/data-table'
4-
import { AssetLink } from '@/features/assets/components/asset-link'
3+
import { AssetIdLink } from '@/features/assets/components/asset-link'
4+
import { AccountAssetSummary } from '../models'
5+
import { AssetSummary } from '@/features/assets/models'
6+
import { AsyncMaybeAtom } from '@/features/common/data/types'
7+
import { RenderInlineAsyncAtom } from '@/features/common/components/render-inline-async-atom'
58

69
type Props = {
710
assetsCreated: AccountAssetSummary[]
@@ -10,16 +13,24 @@ type Props = {
1013
const assetsCreatedTableColumns: ColumnDef<AccountAssetSummary>[] = [
1114
{
1215
header: 'ID',
13-
accessorFn: (item) => item.id,
14-
cell: (c) => <AssetLink assetId={c.getValue<number>()} />,
16+
accessorFn: (item) => item.assetId,
17+
cell: (c) => <AssetIdLink assetId={c.getValue<number>()} />,
1518
},
1619
{
1720
header: 'Name',
18-
accessorFn: (item) => item.name,
21+
accessorFn: (item) => item.asset,
22+
cell: (c) => {
23+
const assetSummaryAtom = c.getValue<AsyncMaybeAtom<AssetSummary>>()
24+
return <RenderInlineAsyncAtom atom={assetSummaryAtom}>{(asset) => asset.name}</RenderInlineAsyncAtom>
25+
},
1926
},
2027
{
2128
header: 'Unit',
22-
accessorFn: (item) => item.unitName,
29+
accessorFn: (item) => item.asset,
30+
cell: (c) => {
31+
const assetSummaryAtom = c.getValue<AsyncMaybeAtom<AssetSummary>>()
32+
return <RenderInlineAsyncAtom atom={assetSummaryAtom}>{(asset) => asset.unitName}</RenderInlineAsyncAtom>
33+
},
2334
},
2435
]
2536

src/features/accounts/data/account-transaction-history.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ export const createLoadableAccountTransactionsPage = (address: Address) => {
5757
return createLoadableViewModelPageAtom({
5858
fetchRawData: (nextPageToken?: string) => createAccountTransactionResultsAtom(address, nextPageToken),
5959
createViewModelPageAtom: (store, rawDataPage) =>
60-
atom(async (get) => {
60+
atom((get) => {
6161
return {
62-
items: await get(createTransactionsAtom(store, rawDataPage.items)),
62+
items: get(createTransactionsAtom(store, rawDataPage.items)),
6363
hasNextPage: rawDataPage.hasNextPage,
6464
}
6565
}),

src/features/accounts/data/account.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { createAssetResolver } from '@/features/assets/data'
1010
const createAccountAtom = (store: JotaiStore, address: Address) => {
1111
return atom(async (get) => {
1212
const accountResult = await get(getAccountResultAtom(store, address))
13-
return asAccount(accountResult, createAssetResolver(store, get))
13+
return asAccount(accountResult, createAssetResolver(store))
1414
})
1515
}
1616

src/features/accounts/mappers/index.ts

Lines changed: 32 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,10 @@ import { AssetSummary } from '@/features/assets/models'
22
import { AccountResult, AssetHoldingResult, AssetResult } from '../data/types'
33
import { Account, AccountAssetSummary, AssetHolding } from '../models'
44
import { microAlgos } from '@algorandfoundation/algokit-utils'
5+
import { AsyncMaybeAtom } from '@/features/common/data/types'
56

6-
export const asAccount = async (
7-
accountResult: AccountResult,
8-
assetResolver: (assetId: number) => Promise<AssetSummary> | AssetSummary
9-
): Promise<Account> => {
10-
const asAccountAssetSummary = createAccountSummaryMapper(assetResolver)
11-
12-
const assetsHoldings = await asAssetHoldings(accountResult.assets ?? [], asAccountAssetSummary)
13-
const [assetsHeld, assetsOpted] = assetsHoldings.reduce(
14-
(acc, asset) => {
15-
if (asset.amount === 0) {
16-
acc[1].push(asset)
17-
} else {
18-
acc[0].push(asset)
19-
}
20-
return acc
21-
},
22-
[[], []] as [AssetHolding[], AssetHolding[]]
23-
)
7+
export const asAccount = (accountResult: AccountResult, assetResolver: (assetId: number) => AsyncMaybeAtom<AssetSummary>): Account => {
8+
const [assetsHeld, assetsOpted] = asAssetHoldings(accountResult.assets ?? [], assetResolver)
249

2510
return {
2611
address: accountResult.address,
@@ -29,44 +14,43 @@ export const asAccount = async (
2914
applicationsCreated: (accountResult['created-apps'] ?? []).map((app) => ({ id: app.id })),
3015
applicationsOpted: (accountResult['apps-local-state'] ?? []).map((app) => ({ id: app.id })),
3116
assetsHeld,
32-
assetsCreated: await asAccountAssetSummaries(accountResult['created-assets'] ?? [], asAccountAssetSummary),
17+
assetsCreated: asAccountAssetSummaries(accountResult['created-assets'] ?? [], assetResolver),
3318
assetsOpted,
3419
rekeyedTo: accountResult['auth-addr'],
3520
json: accountResult,
3621
}
3722
}
3823

39-
const createAccountSummaryMapper =
40-
(assetResolver: (assetId: number) => Promise<AssetSummary> | AssetSummary) =>
41-
async (asset: AssetResult | AssetHoldingResult): Promise<AccountAssetSummary> => {
42-
const assetId = 'index' in asset ? asset.index : asset['asset-id']
43-
const { clawback: _clawback, ...assetSummary } = await assetResolver(assetId)
44-
return assetSummary
45-
}
46-
47-
const asAssetHoldings = async (
24+
const asAssetHoldings = (
4825
heldAssets: AssetHoldingResult[],
49-
asAccountSummary: (asset: AssetHoldingResult) => Promise<AccountAssetSummary>
50-
): Promise<AssetHolding[]> => {
51-
return Promise.all(
52-
heldAssets.map(async (asset) => {
53-
const assetSummary = await asAccountSummary(asset)
54-
return {
55-
asset: assetSummary,
56-
amount: asset.amount,
57-
isFrozen: asset['is-frozen'],
26+
assetResolver: (assetId: number) => AsyncMaybeAtom<AssetSummary>
27+
): [AssetHolding[], AssetHolding[]] =>
28+
heldAssets.reduce(
29+
(acc, result) => {
30+
const assetId = result['asset-id']
31+
const asset = {
32+
assetId,
33+
asset: assetResolver(assetId),
34+
amount: result.amount,
35+
isFrozen: result['is-frozen'],
5836
}
59-
})
37+
if (result.amount === 0) {
38+
acc[1].push(asset)
39+
} else {
40+
acc[0].push(asset)
41+
}
42+
return acc
43+
},
44+
[[], []] as [AssetHolding[], AssetHolding[]]
6045
)
61-
}
6246

63-
const asAccountAssetSummaries = async (
47+
const asAccountAssetSummaries = (
6448
createdAssets: AssetResult[],
65-
asAccountSummary: (asset: AssetResult) => Promise<AccountAssetSummary>
66-
): Promise<AccountAssetSummary[]> => {
67-
return Promise.all(
68-
createdAssets.map(async (asset) => {
69-
return await asAccountSummary(asset)
70-
})
71-
)
72-
}
49+
assetResolver: (assetId: number) => AsyncMaybeAtom<AssetSummary>
50+
): AccountAssetSummary[] =>
51+
createdAssets.map((asset) => {
52+
return {
53+
assetId: asset.index,
54+
asset: assetResolver(asset.index),
55+
}
56+
})

src/features/accounts/models/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import { AlgoAmount } from '@algorandfoundation/algokit-utils/types/amount'
22
import { Address } from '../data/types'
33
import { AssetSummary } from '@/features/assets/models'
44
import { ApplicationId } from '@/features/applications/data/types'
5+
import { AssetId } from '@/features/assets/data/types'
6+
import { AsyncMaybeAtom } from '@/features/common/data/types'
57

6-
export type AccountAssetSummary = Omit<AssetSummary, 'clawback'>
8+
export type AccountAssetSummary = {
9+
assetId: AssetId
10+
asset: AsyncMaybeAtom<AssetSummary>
11+
}
712

8-
export type AssetHolding = {
9-
asset: AccountAssetSummary
13+
export type AssetHolding = AccountAssetSummary & {
1014
amount: number | bigint
1115
isFrozen: boolean
1216
}

src/features/applications/data/application-transaction-history.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ export const createLoadableApplicationTransactionsPage = (applicationID: Applica
5757
return createLoadableViewModelPageAtom({
5858
fetchRawData: (nextPageToken?: string) => createApplicationTransactionResultsAtom(applicationID, nextPageToken),
5959
createViewModelPageAtom: (store, rawDataPage) =>
60-
atom(async (get) => {
60+
atom((get) => {
6161
return {
62-
items: await get(createTransactionsAtom(store, rawDataPage.items)),
62+
items: get(createTransactionsAtom(store, rawDataPage.items)),
6363
hasNextPage: rawDataPage.hasNextPage,
6464
}
6565
}),

src/features/assets/components/asset-link.tsx

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,62 @@ import { cn } from '@/features/common/utils'
22
import { TemplatedNavLink } from '@/features/routing/components/templated-nav-link/templated-nav-link'
33
import { Urls } from '@/routes/urls'
44
import { PropsWithChildren } from 'react'
5+
import { AssetSummary } from '../models'
6+
import { AsyncMaybeAtom } from '@/features/common/data/types'
7+
import { RenderInlineAsyncAtom } from '@/features/common/components/render-inline-async-atom'
58

6-
type Props = PropsWithChildren<{
7-
assetId: number
8-
assetName?: string
9+
type CommonProps = {
910
className?: string
10-
}>
11+
}
12+
13+
type AssetIdLinkProps = PropsWithChildren<
14+
{
15+
assetId: number
16+
} & CommonProps
17+
>
18+
19+
type AssetIdAndNameLinkProps = PropsWithChildren<
20+
{
21+
assetId: number
22+
assetName?: string
23+
} & CommonProps
24+
>
1125

12-
export function AssetLink({ assetId, assetName, className, children }: Props) {
26+
type AssetLinkProps = PropsWithChildren<
27+
{
28+
asset: AssetSummary | AsyncMaybeAtom<AssetSummary>
29+
} & CommonProps
30+
>
31+
32+
function Link(props: AssetIdLinkProps | AssetIdAndNameLinkProps) {
1333
return (
1434
<span>
1535
<TemplatedNavLink
16-
className={cn(!children && 'text-primary underline', className)}
36+
className={cn(!props.children && 'text-primary underline', props.className)}
1737
urlTemplate={Urls.Explore.Asset.ById}
18-
urlParams={{ assetId: assetId.toString() }}
38+
urlParams={{ assetId: props.assetId.toString() }}
1939
>
20-
{children ? children : assetId}
40+
{props.children ? props.children : props.assetId}
2141
</TemplatedNavLink>
22-
{assetName && ` (${assetName})`}
42+
{'assetName' in props && props.assetName && ` (${props.assetName})`}
2343
</span>
2444
)
2545
}
46+
47+
export function AssetLink({ asset, className }: AssetLinkProps) {
48+
return 'read' in asset ? (
49+
<RenderInlineAsyncAtom atom={asset}>
50+
{(asset) => <Link assetId={asset.id} assetName={asset.name} className={className} />}
51+
</RenderInlineAsyncAtom>
52+
) : (
53+
<Link assetId={asset.id} assetName={asset.name} className={className} />
54+
)
55+
}
56+
57+
export function AssetIdAndNameLink({ assetId, assetName, className }: AssetIdAndNameLinkProps) {
58+
return <Link assetId={assetId} assetName={assetName} className={className} />
59+
}
60+
61+
export function AssetIdLink({ assetId, className }: AssetIdLinkProps) {
62+
return <Link assetId={assetId} className={className} />
63+
}

src/features/assets/data/asset-summary.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import { Getter, atom } from 'jotai'
1+
import { atom } from 'jotai'
22
import { JotaiStore } from '@/features/common/data/types'
33
import { asAssetSummary } from '../mappers/asset-summary'
44
import { AssetId } from './types'
55
import { getAssetResultAtom } from './asset-result'
6-
import { AssetSummary } from '../models'
76

8-
export const createAssetResolver =
9-
(store: JotaiStore, get: Getter) =>
10-
(assetId: AssetId): Promise<AssetSummary> => {
11-
return get(createAssetSummaryAtom(store, assetId))
12-
}
7+
export const createAssetResolver = (store: JotaiStore) => (assetId: AssetId) => {
8+
return createAssetSummaryAtom(store, assetId)
9+
}
1310

1411
export const createAssetSummaryAtom = (store: JotaiStore, assetId: AssetId) => {
1512
return atom(async (get) => {

src/features/assets/data/asset-transaction-history.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ export const createLoadableAssetTransactionsPage = (assetId: AssetId) => {
5757
return createLoadableViewModelPageAtom({
5858
fetchRawData: (nextPageToken?: string) => createAssetTransactionResultsAtom(assetId, nextPageToken),
5959
createViewModelPageAtom: (store, rawDataPage) =>
60-
atom(async (get) => {
60+
atom((get) => {
6161
return {
62-
items: await get(createTransactionsAtom(store, rawDataPage.items)),
62+
items: get(createTransactionsAtom(store, rawDataPage.items)),
6363
hasNextPage: rawDataPage.hasNextPage,
6464
}
6565
}),

0 commit comments

Comments
 (0)