Skip to content

Refactor supporters fetching#6

Open
sktbrd wants to merge 1 commit into
mainfrom
codex/study-supporters-component-and-api-usage
Open

Refactor supporters fetching#6
sktbrd wants to merge 1 commit into
mainfrom
codex/study-supporters-component-and-api-usage

Conversation

@sktbrd

@sktbrd sktbrd commented Jul 31, 2025

Copy link
Copy Markdown
Owner

Summary

  • improve /api/supporters with viem multicall and caching
  • simplify supporters hook
  • simplify supporters section display

Testing

  • pnpm lint (fails: How would you like to configure ESLint?)

https://chatgpt.com/codex/tasks/task_e_688b71acff14832cadaa53599a240a0e

Summary by CodeRabbit

  • Refactor
    • Streamlined the supporters API for improved efficiency and batch fetching, with simplified caching and response handling.
    • Simplified the Supporters section UI, introducing explicit pagination with a "Load More" button and a cleaner grid layout.
    • Overhauled the supporters data hook for easier incremental loading and minimal state, removing error handling and complex caching logic.

@coderabbitai

coderabbitai Bot commented Jul 31, 2025

Copy link
Copy Markdown

Walkthrough

This 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

Cohort / File(s) Change Summary
API Endpoint Refactor
src/app/api/supporters/route.ts
Refactored the supporters API to use multicall batching for fetching NFT holders, simplified cache structure and key generation, removed per-entry TTL, consolidated aggregation logic, flattened control flow, and minimized error handling. GET handler now uses direct contract calls, streamlined parameter parsing, and returns results with cache headers.
Component Simplification
src/components/droposal/SupportersSection.tsx
The SupportersSection component was refactored for simpler state, UI, and pagination. It now uses fixed batch/page sizes, a "Load More" button, and a simplified grid layout. Removed complex card styling, detailed error/loading states, and the SupporterAvatar component. Props and internal API were updated to match the new hook.
Hook Refactor
src/hooks/useSupporters.ts
The useSupporters hook was significantly simplified: removed error handling, caching, and aggregation logic. Now maintains only minimal state for supporters, loading, and pagination. Refactored API response types, removed unused options, and changed the function signature to match the new usage pattern.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

In the warren of code, we’ve tidied the nest,
Batched up our calls, let the old state rest.
No more tangled caches, no more error fright—
Supporters now load with a hop and a byte.
“Load More!” we cheer, as the grid fills with glee,
A streamlined new flow for all bunnies to see!
🥕✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/study-supporters-component-and-api-usage

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercel Bot commented Jul 31, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
termignar ❌ Failed (Inspect) Jul 31, 2025 2:33pm

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
src/app/api/supporters/route.ts (3)

9-12: Consider using an explicit RPC URL for production reliability

The 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 growth

The 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 range

Consider 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 name

The file is located in src/components/droposal/ but should likely be src/components/proposal/.


10-15: Consider making batch sizes configurable

The batchSize and itemsPerPage are 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 readability

Displaying 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 logic

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 03a58f8 and c5c5722.

📒 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 logic

The 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 caching

The 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 management

The hook properly manages state for pagination and visibility. Good separation between all supporters and visible supporters.

Comment on lines +51 to 55
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) }));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 40 to +44
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +46 to +69
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant