useSmartPolling stops polling after the first re-render because useBountyStatus passes inline fetchFn/compareFn/onDataChange

Open
#568 6 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@Biglamar is already working on this.

Since Sep 27, 2026.

  • #577 by @Biglamar — open
  • #808 by @Biglamar — open

Assessment

This issue has not been assessed yet.

Description

bug performance Stellar Wave very hard

Problem

src/hooks/useSmartPolling.ts's main polling effect is keyed on [enabled, fetchData], and fetchData is a useCallback over [fetchFn, compareFn, interval, backoffMultiplier, maxBackoff, unchangedThreshold, onDataChange]. The effect only ever creates the interval once, guarded by hasInitialized:

useEffect(() => {
  isMountedRef.current = true;
  if (!enabled) return;
  if (!hasInitialized.current) {
    hasInitialized.current = true;
    dispatch({ type: 'START_POLLING' });
    fetchData();
    intervalRef.current = setInterval(fetchData, currentIntervalRef.current);
  }
  return () => {
    isMountedRef.current = false;
    if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; }
  };
}, [enabled, fetchData]);

src/hooks/useBountyStatus.ts, the only consumer, passes fetchFn, compareFn and onDataChange as inline arrow functions, so they get a new identity on every render. ClaimButton.tsx's own comment already acknowledges that useBountyStatus "rebuilds its fetchFn / compareFn / onDataChange inline on every render".

What actually happens

  1. Mount: the effect runs, calls fetchData(), and starts setInterval.
  2. The first fetch resolves: setData and setIsLoading(false) trigger a re-render, so there's a new fetchData identity.
  3. The effect's cleanup runs: clearInterval(...) and isMountedRef.current = false.
  4. The effect body re-runs: isMountedRef.current = true, but hasInitialized.current is already true, so no new interval is created.

From then on, nothing polls until the tab is hidden and shown again, because the separate visibilitychange effect restarts the interval. Meanwhile:

  • isPolling stays true, since STOP_POLLING is deliberately not dispatched in the cleanup.
  • BountyStatus keeps showing the pulsing green "live" dot.
  • ClaimButton's 2s status refresh used for claim-race detection (#46) never fires.

Suggested fix

  • Keep fetchFn, compareFn and onDataChange in refs, updated every render, so fetchData is identity-stable.
  • And/or drop the hasInitialized guard so the effect re-creates the interval whenever it re-runs.
  • Add a test: render a component using useBountyStatus, advance timers past several intervals, and assert fetchBounty was called more than once.

Related: #46 (introduced smart polling), #359 (test coverage for these hooks), #361 (isMountedRef guard).

Dominant language
TypeScript
Stars
1
Forks
94
Avg merge
1h 2m
Merged PRs (30d)
25

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from MergeFi/frontend

All issues in MergeFi/frontend

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.