Refactor supporters fetching#6
Conversation
WalkthroughThis set of changes refactors the supporters feature, including the API endpoint, React component, and custom hook. The API endpoint now uses multicall batching and a simplified cache. The React component and hook are streamlined for simpler state management, pagination, and UI, removing previous error handling, caching, and aggregation logic. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as UI Component
participant Hook as useSupporters
participant API as /api/supporters
participant Contract as NFT Contract
Client->>Hook: Call loadMore()
Hook->>API: GET /api/supporters?contract=...&start=...&limit=...
API->>Contract: multicall ownerOf for token IDs
Contract-->>API: owners for token IDs
API-->>Hook: Aggregated supporters response
Hook-->>Client: Update visibleSupporters, hasMore, loading state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
src/app/api/supporters/route.ts (3)
9-12: Consider using an explicit RPC URL for production reliabilityThe
http()transport without parameters uses the default public RPC endpoint, which may have rate limits or reliability issues. Consider using a dedicated RPC provider URL.const publicClient = createPublicClient({ chain: base, - transport: http(), + transport: http(process.env.BASE_RPC_URL || 'https://mainnet.base.org'), });
33-35: Implement cache size limit to prevent unbounded memory growthThe in-memory cache has no size limit or eviction policy. Consider implementing an LRU cache or periodic cleanup to prevent memory issues.
// simple in-memory cache const cache = new Map<string, { data: SupportersResponse; time: number }>(); const TTL = 5 * 60 * 1000; // 5 minutes +const MAX_CACHE_SIZE = 100; + +// Clean up expired entries periodically +setInterval(() => { + const now = Date.now(); + for (const [key, value] of cache.entries()) { + if (now - value.time > TTL) { + cache.delete(key); + } + } +}, TTL);
76-78: Add validation for token ID rangeConsider validating that the token ID range is reasonable to prevent excessive resource usage.
const start = BigInt(searchParams.get('startTokenId') ?? '1'); const end = BigInt(searchParams.get('endTokenId') ?? (start + 19n)); const limit = parseInt(searchParams.get('limit') ?? '0'); + + if (start > end) { + return NextResponse.json({ error: 'Invalid token range: start > end' }, { status: 400 }); + } + if (end - start > 1000n) { + return NextResponse.json({ error: 'Token range too large (max 1000)' }, { status: 400 }); + }src/components/droposal/SupportersSection.tsx (3)
1-1: Fix typo in directory nameThe file is located in
src/components/droposal/but should likely besrc/components/proposal/.
10-15: Consider making batch sizes configurableThe
batchSizeanditemsPerPageare hardcoded. Consider making them configurable props with defaults for better flexibility.export const SupportersSection: React.FC<{ contractAddress: Address; totalSupply: bigint | null; + batchSize?: number; + itemsPerPage?: number; -}> = ({ contractAddress, totalSupply }) => { +}> = ({ contractAddress, totalSupply, batchSize = 40, itemsPerPage = 8 }) => { const { visibleSupporters, loadMore, loading, hasMore } = useSupporters({ contractAddress, totalSupply, - batchSize: 40, - itemsPerPage: 8, + batchSize, + itemsPerPage, });
28-28: Format address for better readabilityDisplaying the full address can be overwhelming. Consider truncating or using a formatted display.
- <Text>{holder.address}</Text> + <Text>{`${holder.address.slice(0, 6)}...${holder.address.slice(-4)}`}</Text>src/hooks/useSupporters.ts (1)
65-65: Optimize visible supporters update logicThe current implementation rebuilds the entire visible array on each update. Consider a more efficient approach.
- setVisibleSupporters(prev => [...prev, ...deserialized].slice(0, itemsPerPage)); + setVisibleSupporters(prev => { + if (prev.length >= itemsPerPage) return prev; + const needed = itemsPerPage - prev.length; + return [...prev, ...deserialized.slice(0, needed)]; + });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/app/api/supporters/route.ts(1 hunks)src/components/droposal/SupportersSection.tsx(1 hunks)src/hooks/useSupporters.ts(2 hunks)
🔇 Additional comments (3)
src/app/api/supporters/route.ts (2)
57-68: Well-implemented aggregation logicThe aggregation function efficiently groups tokens by holder using a Map and sorts by token count. Good implementation!
88-112: Proper implementation of data fetching and cachingThe code correctly fetches total supply, processes owners in batches, aggregates results, and implements proper caching with appropriate HTTP cache headers. Good job on serializing bigint values for JSON compatibility.
src/hooks/useSupporters.ts (1)
23-39: Well-structured hook with appropriate state managementThe hook properly manages state for pagination and visibility. Good separation between all supporters and visible supporters.
| const results = await publicClient.multicall({ contracts }); | ||
| return results | ||
| .filter(r => r.status === 'success' && r.result) | ||
| .map((r, i) => ({ address: r.result as Address, tokenId: startId + BigInt(i) })); | ||
| } |
There was a problem hiding this comment.
Critical: Token ID calculation is incorrect after filtering failed results
The current implementation filters out failed results before mapping, which causes token IDs to be misaligned. Failed ownerOf calls (e.g., for non-existent tokens) will shift all subsequent token IDs.
const results = await publicClient.multicall({ contracts });
- return results
- .filter(r => r.status === 'success' && r.result)
- .map((r, i) => ({ address: r.result as Address, tokenId: startId + BigInt(i) }));
+ return results
+ .map((r, i) => ({
+ address: r.status === 'success' && r.result ? r.result as Address : null,
+ tokenId: startId + BigInt(i)
+ }))
+ .filter(item => item.address !== null) as { address: string; tokenId: bigint }[];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const results = await publicClient.multicall({ contracts }); | |
| return results | |
| .filter(r => r.status === 'success' && r.result) | |
| .map((r, i) => ({ address: r.result as Address, tokenId: startId + BigInt(i) })); | |
| } | |
| const results = await publicClient.multicall({ contracts }); | |
| return results | |
| .map((r, i) => ({ | |
| address: r.status === 'success' && r.result ? r.result as Address : null, | |
| tokenId: startId + BigInt(i) | |
| })) | |
| .filter(item => item.address !== null) as { address: string; tokenId: bigint }[]; | |
| } |
🤖 Prompt for AI Agents
In src/app/api/supporters/route.ts around lines 51 to 55, the token ID
calculation is incorrect because filtering out failed results before mapping
causes misalignment of token IDs. To fix this, avoid filtering before mapping;
instead, map over all results and assign token IDs based on their original
index, then filter out failed results afterward. This ensures token IDs remain
correctly aligned with their original positions.
| useEffect(() => { | ||
| if (!autoLoad) { | ||
| return; | ||
| } | ||
|
|
||
| if (!contractAddress) { | ||
| return; | ||
| } | ||
|
|
||
| if (totalSupply === null || totalSupply === undefined) { | ||
| return; | ||
| } | ||
|
|
||
| if (loadedContracts.current.has(contractAddress)) { | ||
| return; | ||
| } | ||
|
|
||
| loadedContracts.current.add(contractAddress); | ||
|
|
||
| const fetchInitialSupporters = async () => { | ||
| try { | ||
| setLoading(true); | ||
| setError(null); | ||
|
|
||
| const initialEndTokenId = totalSupply > 0n | ||
| ? totalSupply < BigInt(batchSize) ? totalSupply : BigInt(batchSize) | ||
| : BigInt(batchSize); | ||
|
|
||
| const params = new URLSearchParams({ | ||
| contractAddress, | ||
| startTokenId: '1', | ||
| endTokenId: initialEndTokenId.toString() | ||
| }); | ||
|
|
||
| const response = await fetch(`/api/supporters?${params}`); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch supporters: ${response.statusText}`); | ||
| } | ||
|
|
||
| const apiData: SupportersApiResponse = await response.json(); | ||
|
|
||
| // Convert string tokenIds back to bigint | ||
| const supportersData = apiData.supporters.map(supporter => ({ | ||
| ...supporter, | ||
| tokenIds: supporter.tokenIds.map(id => BigInt(id)) | ||
| })); | ||
|
|
||
| setSupporters(supportersData); | ||
| setVisibleSupporters(supportersData.slice(0, itemsPerPage)); | ||
| setVisibleCount(itemsPerPage); | ||
| setHasMore(apiData.hasMore); | ||
| setNextTokenId(BigInt(apiData.nextTokenId)); | ||
| setActualTotalSupply(BigInt(apiData.totalSupply)); | ||
| setCached(apiData.cached); | ||
|
|
||
| } catch (err) { | ||
| const errorMessage = err instanceof Error ? err.message : 'Failed to fetch supporters'; | ||
| setError(errorMessage); | ||
| console.error('Error fetching supporters:', err); | ||
| // Remove from loaded contracts so it can be retried | ||
| loadedContracts.current.delete(contractAddress); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| fetchInitialSupporters(); | ||
| }, [contractAddress, totalSupply, autoLoad, batchSize, itemsPerPage]); // Add back necessary dependencies | ||
|
|
||
| // Load more supporters (fetch next batch from API) | ||
| const loadMore = useCallback(async () => { | ||
| if (!hasMore || loadingMore) return; | ||
|
|
||
| try { | ||
| setLoadingMore(true); | ||
|
|
||
| const endTokenId = nextTokenId + BigInt(batchSize) - 1n; | ||
| const params = new URLSearchParams({ | ||
| contractAddress, | ||
| startTokenId: nextTokenId.toString(), | ||
| endTokenId: endTokenId.toString() | ||
| }); | ||
|
|
||
| const response = await fetch(`/api/supporters?${params}`); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch supporters: ${response.statusText}`); | ||
| } | ||
|
|
||
| const apiData: SupportersApiResponse = await response.json(); | ||
|
|
||
| // Convert string tokenIds back to bigint | ||
| const newSupportersData = apiData.supporters.map(supporter => ({ | ||
| ...supporter, | ||
| tokenIds: supporter.tokenIds.map(id => BigInt(id)) | ||
| })); | ||
|
|
||
| // Merge with existing supporters and re-aggregate | ||
| setSupporters(currentSupporters => { | ||
| const allTokenData = [ | ||
| ...currentSupporters.flatMap(s => s.tokenIds.map(tokenId => ({ | ||
| address: s.address, | ||
| tokenId | ||
| }))), | ||
| ...newSupportersData.flatMap(s => s.tokenIds.map(tokenId => ({ | ||
| address: s.address, | ||
| tokenId | ||
| }))) | ||
| ]; | ||
|
|
||
| // Re-aggregate | ||
| const holdersMap = new Map<string, { count: number; tokens: bigint[] }>(); | ||
|
|
||
| allTokenData.forEach(({ address, tokenId }) => { | ||
| if (holdersMap.has(address)) { | ||
| const holder = holdersMap.get(address)!; | ||
| holder.count += 1; | ||
| holder.tokens.push(tokenId); | ||
| } else { | ||
| holdersMap.set(address, { count: 1, tokens: [tokenId] }); | ||
| } | ||
| }); | ||
|
|
||
| const aggregated = Array.from(holdersMap.entries()) | ||
| .map(([address, data]) => ({ | ||
| address, | ||
| tokenCount: data.count, | ||
| tokenIds: data.tokens, | ||
| })) | ||
| .sort((a, b) => b.tokenCount - a.tokenCount); | ||
|
|
||
| // Update visible supporters with current visible count | ||
| setVisibleSupporters(aggregated.slice(0, visibleCount)); | ||
|
|
||
| return aggregated; | ||
| }); | ||
|
|
||
| setHasMore(apiData.hasMore); | ||
| setNextTokenId(BigInt(apiData.nextTokenId)); | ||
| setActualTotalSupply(BigInt(apiData.totalSupply)); | ||
| setCached(apiData.cached); | ||
|
|
||
| } catch (err) { | ||
| const errorMessage = err instanceof Error ? err.message : 'Failed to load more supporters'; | ||
| setError(errorMessage); | ||
| console.error('Error loading more supporters:', err); | ||
| } finally { | ||
| setLoadingMore(false); | ||
| } | ||
| }, [hasMore, loadingMore, nextTokenId, batchSize, contractAddress, visibleCount]); | ||
|
|
||
| // Show more supporters (from already fetched data) | ||
| const showMore = useCallback(async () => { | ||
| const newVisibleCount = visibleCount + itemsPerPage; | ||
| setVisibleCount(newVisibleCount); | ||
| setVisibleSupporters(supporters.slice(0, newVisibleCount)); | ||
| }, [supporters, visibleCount, itemsPerPage]); | ||
|
|
||
| // Combined load more function that handles both cases | ||
| const handleLoadMore = useCallback(async () => { | ||
| const canShowMoreFromExisting = supporters.length > visibleCount; | ||
| if (canShowMoreFromExisting) { | ||
| await showMore(); | ||
| } else { | ||
| await loadMore(); | ||
| } | ||
| }, [supporters.length, visibleCount, showMore, loadMore]); | ||
|
|
||
| // Determine if we can show more from existing data or need to load more | ||
| const canShowMoreFromExisting = supporters.length > visibleCount; | ||
| const finalHasMore = canShowMoreFromExisting || hasMore; | ||
|
|
||
| return { | ||
| supporters, | ||
| visibleSupporters, | ||
| loading, | ||
| loadingMore, | ||
| error, | ||
| hasMore: finalHasMore, | ||
| loadMore: handleLoadMore, | ||
| showMore, | ||
| totalSupply: actualTotalSupply, | ||
| cached | ||
| }; | ||
| if (!contractAddress || totalSupply === null) return; | ||
| loadMore(); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [contractAddress, totalSupply]); |
There was a problem hiding this comment.
Fix potential stale closure and reset state on prop changes
The loadMore function uses state values that might be stale when called from useEffect. Also, state should be reset when props change.
useEffect(() => {
if (!contractAddress || totalSupply === null) return;
+ // Reset state when contract changes
+ setSupporters([]);
+ setVisibleSupporters([]);
+ setNextToken(1n);
+ setHasMore(true);
+ // Load initial batch
loadMore();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [contractAddress, totalSupply]);
+ }, [contractAddress, totalSupply]); // loadMore is intentionally omitted as we want fresh state📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (!autoLoad) { | |
| return; | |
| } | |
| if (!contractAddress) { | |
| return; | |
| } | |
| if (totalSupply === null || totalSupply === undefined) { | |
| return; | |
| } | |
| if (loadedContracts.current.has(contractAddress)) { | |
| return; | |
| } | |
| loadedContracts.current.add(contractAddress); | |
| const fetchInitialSupporters = async () => { | |
| try { | |
| setLoading(true); | |
| setError(null); | |
| const initialEndTokenId = totalSupply > 0n | |
| ? totalSupply < BigInt(batchSize) ? totalSupply : BigInt(batchSize) | |
| : BigInt(batchSize); | |
| const params = new URLSearchParams({ | |
| contractAddress, | |
| startTokenId: '1', | |
| endTokenId: initialEndTokenId.toString() | |
| }); | |
| const response = await fetch(`/api/supporters?${params}`); | |
| if (!response.ok) { | |
| throw new Error(`Failed to fetch supporters: ${response.statusText}`); | |
| } | |
| const apiData: SupportersApiResponse = await response.json(); | |
| // Convert string tokenIds back to bigint | |
| const supportersData = apiData.supporters.map(supporter => ({ | |
| ...supporter, | |
| tokenIds: supporter.tokenIds.map(id => BigInt(id)) | |
| })); | |
| setSupporters(supportersData); | |
| setVisibleSupporters(supportersData.slice(0, itemsPerPage)); | |
| setVisibleCount(itemsPerPage); | |
| setHasMore(apiData.hasMore); | |
| setNextTokenId(BigInt(apiData.nextTokenId)); | |
| setActualTotalSupply(BigInt(apiData.totalSupply)); | |
| setCached(apiData.cached); | |
| } catch (err) { | |
| const errorMessage = err instanceof Error ? err.message : 'Failed to fetch supporters'; | |
| setError(errorMessage); | |
| console.error('Error fetching supporters:', err); | |
| // Remove from loaded contracts so it can be retried | |
| loadedContracts.current.delete(contractAddress); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| fetchInitialSupporters(); | |
| }, [contractAddress, totalSupply, autoLoad, batchSize, itemsPerPage]); // Add back necessary dependencies | |
| // Load more supporters (fetch next batch from API) | |
| const loadMore = useCallback(async () => { | |
| if (!hasMore || loadingMore) return; | |
| try { | |
| setLoadingMore(true); | |
| const endTokenId = nextTokenId + BigInt(batchSize) - 1n; | |
| const params = new URLSearchParams({ | |
| contractAddress, | |
| startTokenId: nextTokenId.toString(), | |
| endTokenId: endTokenId.toString() | |
| }); | |
| const response = await fetch(`/api/supporters?${params}`); | |
| if (!response.ok) { | |
| throw new Error(`Failed to fetch supporters: ${response.statusText}`); | |
| } | |
| const apiData: SupportersApiResponse = await response.json(); | |
| // Convert string tokenIds back to bigint | |
| const newSupportersData = apiData.supporters.map(supporter => ({ | |
| ...supporter, | |
| tokenIds: supporter.tokenIds.map(id => BigInt(id)) | |
| })); | |
| // Merge with existing supporters and re-aggregate | |
| setSupporters(currentSupporters => { | |
| const allTokenData = [ | |
| ...currentSupporters.flatMap(s => s.tokenIds.map(tokenId => ({ | |
| address: s.address, | |
| tokenId | |
| }))), | |
| ...newSupportersData.flatMap(s => s.tokenIds.map(tokenId => ({ | |
| address: s.address, | |
| tokenId | |
| }))) | |
| ]; | |
| // Re-aggregate | |
| const holdersMap = new Map<string, { count: number; tokens: bigint[] }>(); | |
| allTokenData.forEach(({ address, tokenId }) => { | |
| if (holdersMap.has(address)) { | |
| const holder = holdersMap.get(address)!; | |
| holder.count += 1; | |
| holder.tokens.push(tokenId); | |
| } else { | |
| holdersMap.set(address, { count: 1, tokens: [tokenId] }); | |
| } | |
| }); | |
| const aggregated = Array.from(holdersMap.entries()) | |
| .map(([address, data]) => ({ | |
| address, | |
| tokenCount: data.count, | |
| tokenIds: data.tokens, | |
| })) | |
| .sort((a, b) => b.tokenCount - a.tokenCount); | |
| // Update visible supporters with current visible count | |
| setVisibleSupporters(aggregated.slice(0, visibleCount)); | |
| return aggregated; | |
| }); | |
| setHasMore(apiData.hasMore); | |
| setNextTokenId(BigInt(apiData.nextTokenId)); | |
| setActualTotalSupply(BigInt(apiData.totalSupply)); | |
| setCached(apiData.cached); | |
| } catch (err) { | |
| const errorMessage = err instanceof Error ? err.message : 'Failed to load more supporters'; | |
| setError(errorMessage); | |
| console.error('Error loading more supporters:', err); | |
| } finally { | |
| setLoadingMore(false); | |
| } | |
| }, [hasMore, loadingMore, nextTokenId, batchSize, contractAddress, visibleCount]); | |
| // Show more supporters (from already fetched data) | |
| const showMore = useCallback(async () => { | |
| const newVisibleCount = visibleCount + itemsPerPage; | |
| setVisibleCount(newVisibleCount); | |
| setVisibleSupporters(supporters.slice(0, newVisibleCount)); | |
| }, [supporters, visibleCount, itemsPerPage]); | |
| // Combined load more function that handles both cases | |
| const handleLoadMore = useCallback(async () => { | |
| const canShowMoreFromExisting = supporters.length > visibleCount; | |
| if (canShowMoreFromExisting) { | |
| await showMore(); | |
| } else { | |
| await loadMore(); | |
| } | |
| }, [supporters.length, visibleCount, showMore, loadMore]); | |
| // Determine if we can show more from existing data or need to load more | |
| const canShowMoreFromExisting = supporters.length > visibleCount; | |
| const finalHasMore = canShowMoreFromExisting || hasMore; | |
| return { | |
| supporters, | |
| visibleSupporters, | |
| loading, | |
| loadingMore, | |
| error, | |
| hasMore: finalHasMore, | |
| loadMore: handleLoadMore, | |
| showMore, | |
| totalSupply: actualTotalSupply, | |
| cached | |
| }; | |
| if (!contractAddress || totalSupply === null) return; | |
| loadMore(); | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [contractAddress, totalSupply]); | |
| useEffect(() => { | |
| if (!contractAddress || totalSupply === null) return; | |
| // Reset state when contract changes | |
| setSupporters([]); | |
| setVisibleSupporters([]); | |
| setNextToken(1n); | |
| setHasMore(true); | |
| // Load initial batch | |
| loadMore(); | |
| }, [contractAddress, totalSupply]); // loadMore is intentionally omitted as we want fresh state |
🤖 Prompt for AI Agents
In src/hooks/useSupporters.ts around lines 40 to 44, the useEffect calls
loadMore which may use stale state values due to closure. To fix this, wrap
loadMore in useCallback with proper dependencies to ensure it always has fresh
state. Additionally, reset relevant state variables inside the useEffect before
calling loadMore when contractAddress or totalSupply changes to avoid stale or
inconsistent state.
| async function loadMore() { | ||
| if (!hasMore || loading) return; | ||
| setLoading(true); | ||
|
|
||
| const end = nextToken + BigInt(batchSize) - 1n; | ||
| const params = new URLSearchParams({ | ||
| contractAddress, | ||
| startTokenId: nextToken.toString(), | ||
| endTokenId: end.toString(), | ||
| }); | ||
|
|
||
| const res = await fetch(`/api/supporters?${params}`); | ||
| const data: SupportersResponse = await res.json(); | ||
| const deserialized = data.supporters.map(h => ({ | ||
| ...h, | ||
| tokenIds: h.tokenIds.map(id => BigInt(id)), | ||
| })); | ||
|
|
||
| setSupporters(prev => [...prev, ...deserialized]); | ||
| setVisibleSupporters(prev => [...prev, ...deserialized].slice(0, itemsPerPage)); | ||
| setHasMore(data.hasMore); | ||
| setNextToken(BigInt(data.nextTokenId)); | ||
| setLoading(false); | ||
| } |
There was a problem hiding this comment.
Add error handling for API calls
The function lacks error handling, which could leave the UI stuck in a loading state if the API call fails.
async function loadMore() {
if (!hasMore || loading) return;
setLoading(true);
const end = nextToken + BigInt(batchSize) - 1n;
const params = new URLSearchParams({
contractAddress,
startTokenId: nextToken.toString(),
endTokenId: end.toString(),
});
- const res = await fetch(`/api/supporters?${params}`);
- const data: SupportersResponse = await res.json();
+ try {
+ const res = await fetch(`/api/supporters?${params}`);
+ if (!res.ok) {
+ throw new Error(`API error: ${res.status}`);
+ }
+ const data: SupportersResponse = await res.json();
const deserialized = data.supporters.map(h => ({
...h,
tokenIds: h.tokenIds.map(id => BigInt(id)),
}));
setSupporters(prev => [...prev, ...deserialized]);
setVisibleSupporters(prev => [...prev, ...deserialized].slice(0, itemsPerPage));
setHasMore(data.hasMore);
setNextToken(BigInt(data.nextTokenId));
+ } catch (error) {
+ console.error('Failed to load supporters:', error);
+ setHasMore(false); // Prevent further attempts
+ } finally {
setLoading(false);
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function loadMore() { | |
| if (!hasMore || loading) return; | |
| setLoading(true); | |
| const end = nextToken + BigInt(batchSize) - 1n; | |
| const params = new URLSearchParams({ | |
| contractAddress, | |
| startTokenId: nextToken.toString(), | |
| endTokenId: end.toString(), | |
| }); | |
| const res = await fetch(`/api/supporters?${params}`); | |
| const data: SupportersResponse = await res.json(); | |
| const deserialized = data.supporters.map(h => ({ | |
| ...h, | |
| tokenIds: h.tokenIds.map(id => BigInt(id)), | |
| })); | |
| setSupporters(prev => [...prev, ...deserialized]); | |
| setVisibleSupporters(prev => [...prev, ...deserialized].slice(0, itemsPerPage)); | |
| setHasMore(data.hasMore); | |
| setNextToken(BigInt(data.nextTokenId)); | |
| setLoading(false); | |
| } | |
| async function loadMore() { | |
| if (!hasMore || loading) return; | |
| setLoading(true); | |
| const end = nextToken + BigInt(batchSize) - 1n; | |
| const params = new URLSearchParams({ | |
| contractAddress, | |
| startTokenId: nextToken.toString(), | |
| endTokenId: end.toString(), | |
| }); | |
| try { | |
| const res = await fetch(`/api/supporters?${params}`); | |
| if (!res.ok) { | |
| throw new Error(`API error: ${res.status}`); | |
| } | |
| const data: SupportersResponse = await res.json(); | |
| const deserialized = data.supporters.map(h => ({ | |
| ...h, | |
| tokenIds: h.tokenIds.map(id => BigInt(id)), | |
| })); | |
| setSupporters(prev => [...prev, ...deserialized]); | |
| setVisibleSupporters(prev => | |
| [...prev, ...deserialized].slice(0, itemsPerPage) | |
| ); | |
| setHasMore(data.hasMore); | |
| setNextToken(BigInt(data.nextTokenId)); | |
| } catch (error) { | |
| console.error('Failed to load supporters:', error); | |
| setHasMore(false); // Prevent further attempts | |
| } finally { | |
| setLoading(false); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/hooks/useSupporters.ts between lines 46 and 69, the loadMore function
does not handle errors from the fetch call, which can cause the loading state to
remain true indefinitely if the API request fails. Wrap the fetch and subsequent
processing in a try-catch block, set loading to false in both success and error
cases, and handle errors gracefully, such as logging the error or showing a
user-friendly message.
Summary
/api/supporterswithviemmulticall and cachingTesting
pnpm lint(fails: How would you like to configure ESLint?)https://chatgpt.com/codex/tasks/task_e_688b71acff14832cadaa53599a240a0e
Summary by CodeRabbit