Skip to content

Commit b134b9e

Browse files
chore: refactoring our usage of jotai to make the patterns easier to use and evolve (#46)
* chore: refactoring the way we use jotai to make it simpler --------- Co-authored-by: Hoang Dinh <[email protected]>
1 parent 9af6491 commit b134b9e

Some content is hidden

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

45 files changed

+591
-680
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { indexer } from '@/features/common/data'
2+
import { atom } from 'jotai'
3+
import { ApplicationLookupResult, ApplicationResult } from '@algorandfoundation/algokit-utils/types/indexer'
4+
import { ApplicationId } from './types'
5+
import { atomsInAtom } from '@/features/common/data/atoms-in-atom'
6+
7+
const createApplicationResultAtom = (applicationId: ApplicationId) => {
8+
return atom<Promise<ApplicationResult> | ApplicationResult>(async (_get) => {
9+
return await indexer
10+
.lookupApplications(applicationId)
11+
.includeAll(true)
12+
.do()
13+
.then((result) => {
14+
return (result as ApplicationLookupResult).application
15+
})
16+
})
17+
}
18+
19+
export const [applicationResultsAtom, getApplicationResultAtom] = atomsInAtom(createApplicationResultAtom, (applicationId) => applicationId)

src/features/applications/data/application.ts

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,14 @@
1-
import { indexer } from '@/features/common/data'
21
import { JotaiStore } from '@/features/common/data/types'
32
import { atom, useAtomValue, useStore } from 'jotai'
4-
import { atomEffect } from 'jotai-effect'
5-
import { ApplicationLookupResult } from '@algorandfoundation/algokit-utils/types/indexer'
63
import { asApplication } from '../mappers'
74
import { useMemo } from 'react'
85
import { loadable } from 'jotai/utils'
96
import { ApplicationId } from './types'
10-
import { applicationResultsAtom } from './core'
11-
12-
const fetchApplicationResultAtomBuilder = (applicationId: ApplicationId) => {
13-
return atom(async (_get) => {
14-
return await indexer
15-
.lookupApplications(applicationId)
16-
.includeAll(true)
17-
.do()
18-
.then((result) => {
19-
return (result as ApplicationLookupResult).application
20-
})
21-
})
22-
}
23-
24-
export const getApplicationAtomBuilder = (store: JotaiStore, applicationId: ApplicationId) => {
25-
const fetchApplicationResultAtom = fetchApplicationResultAtomBuilder(applicationId)
26-
27-
const syncEffect = atomEffect((get, set) => {
28-
;(async () => {
29-
try {
30-
const applicationResult = await get(fetchApplicationResultAtom)
31-
set(applicationResultsAtom, (prev) => {
32-
const next = new Map(prev)
33-
next.set(applicationResult.id, applicationResult)
34-
return next
35-
})
36-
} catch (e) {
37-
// Ignore any errors as there is nothing to sync
38-
}
39-
})()
40-
})
7+
import { getApplicationResultAtom } from './application-result'
418

9+
export const createApplicationAtom = (store: JotaiStore, applicationId: ApplicationId) => {
4210
return atom(async (get) => {
43-
const applicationResults = store.get(applicationResultsAtom)
44-
const cachedApplicationResult = applicationResults.get(applicationId)
45-
if (cachedApplicationResult) {
46-
return asApplication(cachedApplicationResult)
47-
}
48-
49-
get(syncEffect)
50-
51-
const applicationResult = await get(fetchApplicationResultAtom)
11+
const applicationResult = await get(getApplicationResultAtom(store, applicationId))
5212
return asApplication(applicationResult)
5313
})
5414
}
@@ -57,7 +17,7 @@ const useApplicationAtom = (applicationId: ApplicationId) => {
5717
const store = useStore()
5818

5919
return useMemo(() => {
60-
return getApplicationAtomBuilder(store, applicationId)
20+
return createApplicationAtom(store, applicationId)
6121
}, [store, applicationId])
6222
}
6323

src/features/applications/data/core.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './application'
2+
export * from './application-result'

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export function AssetDetails({ asset }: Props) {
158158
<CardContent className={cn('text-sm space-y-2')}>
159159
<h1 className={cn('text-2xl text-primary font-bold')}>{assetTransactionsLabel}</h1>
160160
<div className={cn('border-solid border-2 grid p-4')}>
161-
<AssetTransactionHistory assetIndex={asset.id} />
161+
<AssetTransactionHistory assetId={asset.id} />
162162
</div>
163163
</CardContent>
164164
</Card>

src/features/assets/components/asset-transaction-history.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { LazyLoadDataTable } from '@/features/common/components/lazy-load-data-table'
2-
import { AssetIndex } from '../data/types'
2+
import { AssetId } from '../data/types'
33
import { Transaction, TransactionType } from '@/features/transactions/models'
44
import { DisplayAlgo } from '@/features/common/components/display-algo'
55
import { DisplayAssetAmount } from '@/features/common/components/display-asset-amount'
@@ -10,11 +10,11 @@ import { ColumnDef } from '@tanstack/react-table'
1010
import { useFetchNextAssetTransactionsPage } from '../data/asset-transaction-history'
1111

1212
type Props = {
13-
assetIndex: AssetIndex
13+
assetId: AssetId
1414
}
1515

16-
export function AssetTransactionHistory({ assetIndex }: Props) {
17-
const fetchNextPage = useFetchNextAssetTransactionsPage(assetIndex)
16+
export function AssetTransactionHistory({ assetId }: Props) {
17+
const fetchNextPage = useFetchNextAssetTransactionsPage(assetId)
1818

1919
return <LazyLoadDataTable columns={transactionsTableColumns} fetchNextPage={fetchNextPage} />
2020
}

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

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import { atom } from 'jotai'
2-
import { JotaiStore } from '@/features/common/data/types'
3-
import { atomEffect } from 'jotai-effect'
4-
import { assetMetadataAtom } from './core'
52
import { AssetResult, TransactionResult, TransactionSearchResults } from '@algorandfoundation/algokit-utils/types/indexer'
63
import { indexer } from '@/features/common/data'
74
import { flattenTransactionResult } from '@/features/transactions/utils/flatten-transaction-result'
@@ -12,14 +9,15 @@ import { getArc3Url, isArc3Url } from '../utils/arc3'
129
import { base64ToUtf8 } from '@/utils/base64-to-utf8'
1310
import { ZERO_ADDRESS } from '@/features/common/constants'
1411
import { executePaginatedRequest } from '@algorandfoundation/algokit-utils'
12+
import { atomsInAtom } from '@/features/common/data/atoms-in-atom'
1513

1614
// Currently, we support ARC-3, 19 and 69. Their specs can be found here https://github.com/algorandfoundation/ARCs/tree/main/ARCs
1715
// ARCs are community standard, therefore, there are edge cases
1816
// For example:
1917
// - An asset can follow ARC-69 and ARC-19 at the same time: https://allo.info/asset/1559471783/nft
2018
// - An asset can follow ARC-3 and ARC-19 at the same time: https://allo.info/asset/1494117806/nft
2119
// - ARC-19 doesn't specify the metadata format but generally people use the ARC-3 format
22-
export const buildAssetMetadataResult = async (
20+
const createAssetMetadataResult = async (
2321
assetResult: AssetResult,
2422
latestAssetCreateOrReconfigureTransaction?: TransactionResult
2523
): Promise<AssetMetadataResult> => {
@@ -71,7 +69,7 @@ const noteToArc69Metadata = (note: string | undefined) => {
7169
return undefined
7270
}
7371

74-
export const fetchAssetMetadataAtomBuilder = (assetResult: AssetResult) =>
72+
export const createAssetMetadataResultAtom = (assetResult: AssetResult) =>
7573
atom(async (_get) => {
7674
if (assetResult.index === 0) {
7775
return null
@@ -111,37 +109,10 @@ export const fetchAssetMetadataAtomBuilder = (assetResult: AssetResult) =>
111109
return null
112110
}
113111

114-
return await buildAssetMetadataResult(assetResult, assetConfigTransactionResults[0])
112+
return await createAssetMetadataResult(assetResult, assetConfigTransactionResults[0])
115113
})
116114

117-
export const getAssetMetadataAtomBuilder = (store: JotaiStore, assetResult: AssetResult) => {
118-
const fetchAssetMetadataAtom = fetchAssetMetadataAtomBuilder(assetResult)
119-
120-
const syncEffect = atomEffect((get, set) => {
121-
;(async () => {
122-
try {
123-
const assetMetadata = await get(fetchAssetMetadataAtom)
124-
set(assetMetadataAtom, (prev) => {
125-
const next = new Map(prev)
126-
next.set(assetResult.index, assetMetadata)
127-
return next
128-
})
129-
} catch (e) {
130-
// Ignore any errors as there is nothing to sync
131-
}
132-
})()
133-
})
134-
135-
return atom(async (get) => {
136-
const assetMetadata = store.get(assetMetadataAtom)
137-
const cachedAssetMetadata = assetMetadata.get(assetResult.index)
138-
if (cachedAssetMetadata) {
139-
return cachedAssetMetadata
140-
}
141-
142-
get(syncEffect)
143-
144-
const assetMetadataResult = await get(fetchAssetMetadataAtom)
145-
return assetMetadataResult
146-
})
147-
}
115+
export const [assetMetadataResultsAtom, getAssetMetadataResultAtom] = atomsInAtom(
116+
createAssetMetadataResultAtom,
117+
(assetResult) => assetResult.index
118+
)
Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11
import { atom } from 'jotai'
2-
import { AssetIndex, AssetResult } from './types'
2+
import { AssetId, AssetResult } from './types'
33
import { indexer, algod } from '@/features/common/data'
4-
import { atomEffect } from 'jotai-effect'
5-
import { assetResultsAtom } from './core'
6-
import { JotaiStore } from '@/features/common/data/types'
74
import { asError, is404 } from '@/utils/error'
5+
import { atomsInAtom } from '@/features/common/data/atoms-in-atom'
6+
import { ZERO_ADDRESS } from '@/features/common/constants'
87

9-
export const fetchAssetResultAtomBuilder = (assetIndex: AssetIndex) =>
10-
atom(async (_get) => {
8+
export const algoAssetResult = {
9+
index: 0,
10+
'created-at-round': 0,
11+
params: {
12+
creator: ZERO_ADDRESS,
13+
decimals: 6,
14+
total: 10_000_000_000_000_000n,
15+
name: 'ALGO',
16+
'unit-name': 'ALGO',
17+
url: 'https://www.algorand.foundation',
18+
},
19+
} as AssetResult
20+
21+
const createAssetResultAtom = (assetId: AssetId) =>
22+
atom<Promise<AssetResult> | AssetResult>(async (_get) => {
1123
try {
1224
// Check algod first, as there can be some syncing delays to indexer
1325
return await algod
14-
.getAssetByID(assetIndex)
26+
.getAssetByID(assetId)
1527
.do()
1628
.then((result) => result as AssetResult)
1729
} catch (e: unknown) {
1830
if (is404(asError(e))) {
1931
// Handle destroyed assets or assets that may not be available in algod potentially due to the node type
2032
return await indexer
21-
.lookupAssetByID(assetIndex)
33+
.lookupAssetByID(assetId)
2234
.includeAll(true) // Returns destroyed assets
2335
.do()
2436
.then((result) => result.asset as AssetResult)
@@ -27,33 +39,8 @@ export const fetchAssetResultAtomBuilder = (assetIndex: AssetIndex) =>
2739
}
2840
})
2941

30-
export const getAssetResultAtomBuilder = (store: JotaiStore, assetIndex: AssetIndex) => {
31-
return atom(async (get) => {
32-
// TODO: NC - If I don't use store here we get double fetching when an atom depends on this, due to depending on something that we directly set using an effect.
33-
// I'll be coming back and re-evaluating the patterns here.
34-
const assetResults = store.get(assetResultsAtom)
35-
const cachedAssetResult = assetResults.get(assetIndex)
36-
if (cachedAssetResult) {
37-
return cachedAssetResult
38-
}
39-
40-
const fetchAssetResultAtom = fetchAssetResultAtomBuilder(assetIndex)
41-
const syncEffect = atomEffect((get, set) => {
42-
;(async () => {
43-
try {
44-
const assetResult = await get(fetchAssetResultAtom)
45-
46-
set(assetResultsAtom, (prev) => {
47-
const next = new Map(prev)
48-
next.set(assetResult.index, assetResult)
49-
return next
50-
})
51-
} catch (e) {
52-
// Ignore any errors as there is nothing to sync
53-
}
54-
})()
55-
})
56-
get(syncEffect)
57-
return await get(fetchAssetResultAtom)
58-
})
59-
}
42+
export const [assetResultsAtom, getAssetResultAtom] = atomsInAtom(
43+
createAssetResultAtom,
44+
(assetId) => assetId,
45+
new Map([[algoAssetResult.index, atom(algoAssetResult)]])
46+
)
Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,12 @@
1-
import { atom, useAtomValue, useStore } from 'jotai'
1+
import { atom } from 'jotai'
22
import { JotaiStore } from '@/features/common/data/types'
3-
import { useMemo } from 'react'
4-
import { loadable } from 'jotai/utils'
53
import { asAssetSummary } from '../mappers/asset-summary'
6-
import { AssetIndex } from './types'
7-
import { getAssetResultAtomBuilder } from './asset-result'
4+
import { AssetId } from './types'
5+
import { getAssetResultAtom } from './asset-result'
86

9-
export const getAssetSummaryAtomBuilder = (store: JotaiStore, assetIndex: AssetIndex) => {
7+
export const createAssetSummaryAtom = (store: JotaiStore, assetId: AssetId) => {
108
return atom(async (get) => {
11-
const assetResult = await get(getAssetResultAtomBuilder(store, assetIndex))
9+
const assetResult = await get(getAssetResultAtom(store, assetId))
1210
return asAssetSummary(assetResult)
1311
})
1412
}
15-
16-
export const getAssetSummariesAtomBuilder = (store: JotaiStore, assetIndexes: AssetIndex[]) => {
17-
return atom((get) => {
18-
return Promise.all(assetIndexes.map((assetIndex) => get(getAssetSummaryAtomBuilder(store, assetIndex))))
19-
})
20-
}
21-
22-
export const useAssetSummaryAtom = (assetIndex: AssetIndex) => {
23-
const store = useStore()
24-
return useMemo(() => {
25-
return getAssetSummaryAtomBuilder(store, assetIndex)
26-
}, [store, assetIndex])
27-
}
28-
29-
export const useLoadableAssetSummary = (assetIndex: AssetIndex) => {
30-
return useAtomValue(loadable(useAssetSummaryAtom(assetIndex)))
31-
}
Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { AssetIndex } from '../data/types'
1+
import { AssetId } from '../data/types'
22
import { indexer } from '@/features/common/data'
33
import { TransactionResult, TransactionSearchResults } from '@algorandfoundation/algokit-utils/types/indexer'
44
import { useMemo } from 'react'
55
import { JotaiStore } from '@/features/common/data/types'
6-
import { fetchTransactionsAtomBuilder, transactionResultsAtom } from '@/features/transactions/data'
6+
import { createTransactionsAtom, transactionResultsAtom } from '@/features/transactions/data'
77
import { atomEffect } from 'jotai-effect'
88
import { atom, useStore } from 'jotai'
99

10-
const fetchAssetTransactionResults = async (assetIndex: AssetIndex, pageSize: number, nextPageToken?: string) => {
10+
const fetchAssetTransactionResults = async (assetId: AssetId, pageSize: number, nextPageToken?: string) => {
1111
const results = (await indexer
1212
.searchForTransactions()
13-
.assetID(assetIndex)
13+
.assetID(assetId)
1414
.nextToken(nextPageToken ?? '')
1515
.limit(pageSize)
1616
.do()) as TransactionSearchResults
@@ -20,14 +20,16 @@ const fetchAssetTransactionResults = async (assetIndex: AssetIndex, pageSize: nu
2020
} as const
2121
}
2222

23-
const syncEffectBuilder = (transactionResults: TransactionResult[]) => {
23+
const createSyncEffect = (transactionResults: TransactionResult[]) => {
2424
return atomEffect((_, set) => {
2525
;(async () => {
2626
try {
2727
set(transactionResultsAtom, (prev) => {
2828
const next = new Map(prev)
2929
transactionResults.forEach((transactionResult) => {
30-
next.set(transactionResult.id, transactionResult)
30+
if (!next.has(transactionResult.id)) {
31+
next.set(transactionResult.id, atom(transactionResult))
32+
}
3133
})
3234
return next
3335
})
@@ -38,13 +40,13 @@ const syncEffectBuilder = (transactionResults: TransactionResult[]) => {
3840
})
3941
}
4042

41-
const fetchAssetTransactionsAtomBuilder = (store: JotaiStore, assetIndex: AssetIndex, pageSize: number, nextPageToken?: string) => {
43+
const createAssetTransactionsAtom = (store: JotaiStore, assetId: AssetId, pageSize: number, nextPageToken?: string) => {
4244
return atom(async (get) => {
43-
const { transactionResults, nextPageToken: newNextPageToken } = await fetchAssetTransactionResults(assetIndex, pageSize, nextPageToken)
45+
const { transactionResults, nextPageToken: newNextPageToken } = await fetchAssetTransactionResults(assetId, pageSize, nextPageToken)
4446

45-
get(syncEffectBuilder(transactionResults))
47+
get(createSyncEffect(transactionResults))
4648

47-
const transactions = await get(fetchTransactionsAtomBuilder(store, transactionResults))
49+
const transactions = await get(createTransactionsAtom(store, transactionResults))
4850

4951
return {
5052
rows: transactions,
@@ -53,10 +55,10 @@ const fetchAssetTransactionsAtomBuilder = (store: JotaiStore, assetIndex: AssetI
5355
})
5456
}
5557

56-
export const useFetchNextAssetTransactionsPage = (assetIndex: AssetIndex) => {
58+
export const useFetchNextAssetTransactionsPage = (assetId: AssetId) => {
5759
const store = useStore()
5860

5961
return useMemo(() => {
60-
return (pageSize: number, nextPageToken?: string) => fetchAssetTransactionsAtomBuilder(store, assetIndex, pageSize, nextPageToken)
61-
}, [store, assetIndex])
62+
return (pageSize: number, nextPageToken?: string) => createAssetTransactionsAtom(store, assetId, pageSize, nextPageToken)
63+
}, [store, assetId])
6264
}

0 commit comments

Comments
 (0)