Skip to content

fix(hooks): stable load identity, single mount fetch and in-flight cancellation - #518

Open
Sam-Rytech wants to merge 1 commit into
Sorokit:mainfrom
Sam-Rytech:fix/442-stale-closure-deps
Open

fix(hooks): stable load identity, single mount fetch and in-flight cancellation#518
Sam-Rytech wants to merge 1 commit into
Sorokit:mainfrom
Sam-Rytech:fix/442-stale-closure-deps

Conversation

@Sam-Rytech

Copy link
Copy Markdown
Contributor

Closes #442

load is already wrapped in useCallback and listed in the effect deps in both components, so ESLint react-hooks was already quiet. What was still unmet were the behaviours those deps are supposed to guarantee:

ContractEventFeed

  • live was seeded from pollInterval at mount and never re-synced, so a feed mounted with polling off never started polling when the prop turned on. pollInterval is now synced during render, mirroring the existing prevContractId pattern.
  • Added a generation counter to load. Every call claims a generation and the counter is bumped when contractId changes, so a response for the previous contract is discarded instead of writing its events - or its loading state - over the current feed.
  • The polling effect now only owns the timer: it clears and re-arms on pollInterval changes, and nulls intervalRef on teardown.
  • Dropped the duplicate setEvents([]) mount effect and its react-hooks/set-state-in-effect suppression; the render-phase reset above it already clears the previous contract's events.

FeeEstimator

  • onFeeLoad is usually an inline arrow, so a new identity on every parent render rebuilt load, re-ran the effect and fired another request per render (and the callback typically sets parent state, so it fed itself). It is held in a ref now and load depends only on client, so mount performs exactly one request while the newest callback is still the one invoked.
  • Same generation guard, so a slow estimate cannot overwrite fresher data.

Tests: both suites still mocked the removed getClient entrypoint (the client moved to SorokitContext), so they were red on main - a small useSorokit shim routes the hook at the same mock every existing test configures, and three export-button queries are matched against the current aria-label. New cases cover single fetch on mount, runtime pollInterval changes, timer re-arming, stale-response rejection and the inline-callback refetch loop.

…ncellation

Closes Sorokit#442

`load` is already wrapped in useCallback and listed in the effect deps in both
components, so ESLint react-hooks was already quiet. What was still unmet were
the behaviours those deps are supposed to guarantee:

ContractEventFeed
- `live` was seeded from `pollInterval` at mount and never re-synced, so a feed
  mounted with polling off never started polling when the prop turned on.
  `pollInterval` is now synced during render, mirroring the existing
  `prevContractId` pattern.
- Added a generation counter to `load`. Every call claims a generation and the
  counter is bumped when `contractId` changes, so a response for the previous
  contract is discarded instead of writing its events - or its loading state -
  over the current feed.
- The polling effect now only owns the timer: it clears and re-arms on
  `pollInterval` changes, and nulls `intervalRef` on teardown.
- Dropped the duplicate `setEvents([])` mount effect and its
  react-hooks/set-state-in-effect suppression; the render-phase reset above it
  already clears the previous contract's events.

FeeEstimator
- `onFeeLoad` is usually an inline arrow, so a new identity on every parent
  render rebuilt `load`, re-ran the effect and fired another request per render
  (and the callback typically sets parent state, so it fed itself). It is held
  in a ref now and `load` depends only on `client`, so mount performs exactly
  one request while the newest callback is still the one invoked.
- Same generation guard, so a slow estimate cannot overwrite fresher data.

Tests: both suites still mocked the removed `getClient` entrypoint (the client
moved to SorokitContext), so they were red on main - a small `useSorokit` shim
routes the hook at the same mock every existing test configures, and three
export-button queries are matched against the current aria-label. New cases
cover single fetch on mount, runtime pollInterval changes, timer re-arming,
stale-response rejection and the inline-callback refetch loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@drips-wave

drips-wave Bot commented Aug 26, 2026

Copy link
Copy Markdown

@Sam-Rytech Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ContractEventFeed and FeeEstimator have stale closure bug in useEffect deps

1 participant