Skip to content

INTER-2332: avoid setState-in-effect and stale responses in immediate mount fetch#206

Open
JuroUhlar wants to merge 33 commits into
mainfrom
inter-2322/use-visitor-data-mount-fetch
Open

INTER-2332: avoid setState-in-effect and stale responses in immediate mount fetch#206
JuroUhlar wants to merge 33 commits into
mainfrom
inter-2322/use-visitor-data-mount-fetch

Conversation

@JuroUhlar

@JuroUhlar JuroUhlar commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

This is a tricky PR, and an argument can be made for rejecting it. It tries to follow best practices and improve handling of various React lifecycle edge cases, but the doing so makes the code a bit harder to read and reason about. Current state does not seem to be causing issues. Open to discussion. Maybe there are better ways to achieve some of the goals here.

  • Reworked automatic fetching to avoid setting loading state inside the effect.
  • Prevented stale automatic responses from overwriting newer state.
  • Documented and tested dynamic immediate behavior and error handling.

Behavior

Scenario Before After
Automatic request superseded by new options Stale result could overwrite current state Stale result is ignored
immediate: true → false during an automatic request Loading continued and the result was applied Loading clears and the automatic result is ignored
Options change or immediate: false → true Loading was set from the effect Loading is set during render
Automatic fetch on mount or false → true Starts a request Preserved
Options change while immediate: true Refetches Preserved
Manual getData() Updates state and throws failures Preserved

Changing immediate to false does not abort the network request. Only the automatic result is ignored; manual getData() results still apply. Because loading state is shared, the toggle also clears it for a concurrent manual request.

Discussion point: separate automatic and manual request flows

The automatic effect calls the provider directly because getData synchronously enters loading and throws failures. Both paths share state-transition helpers; automatic failures are logged, while manual failures are thrown.

Known remaining race (pre-existing)

A manual getData() call racing an automatic fetch is still last-write-wins.

JuroUhlar and others added 20 commits July 17, 2026 08:39
Widen the react peer range to >=18 <20, verify the SDK against React 19
via a second CI matrix job (catalog stays pinned to 18 as the default
dev/test toolchain), and bump the Next.js examples to 16.2.10.

Add @next/eslint-plugin-next lint rules scoped to the Next examples
instead of eslint-config-next, since the latter bundles
eslint-plugin-react which only supports ESLint below version 10.
Re-add the two @eslint-react rule overrides for React 19-only idioms
so linting stays green under both supported versions.
Next.js 16 requires Node >=20.9.0; declare it explicitly so
contributors on older Node get a clear engine error instead of a
confusing runtime failure.
no-html-link-for-pages (and other core-web-vitals rules) default to
looking for pages/app at the repo root, so they silently no-op since
both Next examples live under examples/. Set settings.next.rootDir to
point at them explicitly.
Add a CI matrix that boots each example app with Playwright and asserts the
Fingerprint React SDK identifies the visitor (a visitor ID renders in the
browser). Each example is tested against the React versions it supports.

- e2e/: a single Playwright harness driven by an EXAMPLE env var, with a
  per-example registry (examples.ts) and one framework-agnostic spec.
- .github/workflows/e2e.yml: matrix of {example} x {React 18, 19}. React 19
  jobs flip the pnpm catalog so the SDK and every example move in lockstep
  (a single @types/react, which the Next App Router type-check requires).
  The preact example runs once (it uses Preact via preact/compat).
- examples: optional region env support (the CI key is EU) that leaves the
  default behavior unchanged. Also fixes pre-existing build breakage: the CRA
  example was missing react-app-env.d.ts, and the preact example's ESM
  preact.config.js failed to load on Node 22 (and was redundant with
  preact-cli's native .env / PREACT_APP_* injection) so it was removed and a
  scoped process type declaration added.

INTER-2322

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014F14pjZxadmtLq8uTuzwXb
Read the Fingerprint region from the FPJS_REGION Actions variable (defaulting
to eu) instead of hardcoding it, so the region is configurable alongside the
public key secret.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014F14pjZxadmtLq8uTuzwXb
Drop the eu fallback so the region comes solely from the FPJS_REGION repo
variable, and fail fast in the pre-flight check when it (or the API key
secret) is missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014F14pjZxadmtLq8uTuzwXb
The Fingerprint public key isn't sensitive, so read FPJS_PUBLIC_API_KEY from
either a repo secret or a repo variable. Fixes the matrix failing when the key
was added under the Variables tab alongside FPJS_REGION.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014F14pjZxadmtLq8uTuzwXb
preact-cli's dev server (`preact watch`) fails to resolve its entrypoint on
the CI Node version and never serves, so the e2e job timed out. Build the
example once and serve the static output with sirv instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014F14pjZxadmtLq8uTuzwXb
Align examples on a shared data-testid for successful identifies, flip
React via pnpm overrides instead of sed, and verify the installed React
major in CI.
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements
86.36% (+3.93% 🔼)
152/176
🟡 Branches
73% (+4.03% 🔼)
73/100
🟢 Functions
86.54% (+5.69% 🔼)
45/52
🟢 Lines
86.47% (+3.95% 🔼)
147/170
Show files with reduced coverage 🔻
St.
File Statements Branches Functions Lines
🟢 use-visitor-data.ts
98.04% (+2.39% 🔼)
95.24% (-4.76% 🔻)
100% (+20% 🔼)
98% (+2.35% 🔼)

Test suite run success

28 tests passing in 15 suites.

Report generated by 🧪jest coverage report action from e02506d

Show full coverage report
St File % Stmts % Branch % Funcs % Lines Uncovered Line #s
🟢 All files 86.36 73 86.53 86.47
🟢  src 93.25 80 95.65 93.1
🟢   detect-env.ts 90 83.33 100 90 15,87
🟢   env.ts 100 100 100 100
🟢   env.types.ts 83.33 0 50 83.33 14
🟢   ...nt-context.ts 100 100 100 100
🟡   get-env.ts 60 0 100 60 8-9
🟢   ssr.ts 100 100 100 100
🟢   ...sitor-data.ts 98.03 95.23 100 98 131
🟢  src/components 91.66 58.33 91.66 91.17
🟢   ...-provider.tsx 90.32 54.54 90.9 89.65 47,52,85
🟢   ...vironment.tsx 100 100 100 100
🟡  src/utils 70.58 75 70.58 71.42
🟡   ...is-defined.ts 50 75 100 50 3
🟢   ...-cache-key.ts 80.76 83.33 85.71 84 5,10,43,52
🟢   to-error.ts 100 100 100 100
🟢   ...mise-store.ts 100 100 100 100
🔴   wait-until.ts 0 0 0 0 8-19

@JuroUhlar JuroUhlar changed the title fix: avoid setState-in-effect and stale responses in immediate mount fetch INTER-2332: avoid setState-in-effect and stale responses in immediate mount fetch Jul 20, 2026
@JuroUhlar
JuroUhlar requested a review from Copilot July 20, 2026 14:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors useVisitorData’s immediate mount/options-change fetch to avoid synchronous setState at effect start and to prevent stale in-flight responses from overwriting newer state, adding tests to cover the new behavior.

Changes:

  • Refactored immediate fetch to call getVisitorData directly in the effect with an ignore cleanup flag to prevent stale updates.
  • Centralized query state transitions into setLoading/setSuccess/setFailure, and added a render-phase loading reset when getOptions change.
  • Added tests for immediate mount-fetch failure and for an options-change mid-flight stale response race.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/use-visitor-data.ts Refactors immediate fetching and state transitions; adds ignore-based stale response protection and render-phase loading reset on options changes.
tests/use-visitor-data.test.tsx Adds tests for immediate error handling and stale-response avoidance when options change mid-flight.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/use-visitor-data.ts Outdated
Comment thread src/use-visitor-data.ts
Comment thread __tests__/use-visitor-data.test.tsx Outdated
@JuroUhlar
JuroUhlar force-pushed the inter-2322/use-visitor-data-mount-fetch branch from 8153bc8 to df6eefc Compare July 20, 2026 14:54
Base automatically changed from inter-2322/e2e-test-ci-matrix to main July 21, 2026 08:42
@JuroUhlar

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved. I merged origin/main into this branch and fixed conflicts in src/use-visitor-data.ts and examples/preact/tsconfig.json in commit ad87c55.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comment thread src/use-visitor-data.ts Outdated
Comment thread README.md Outdated
Comment thread __tests__/use-visitor-data.test.tsx Outdated
Comment thread __tests__/use-visitor-data.test.tsx
@JuroUhlar
JuroUhlar marked this pull request as ready for review July 21, 2026 12:07
@JuroUhlar
JuroUhlar requested a review from mcnulty-fp July 21, 2026 12:07
mcnulty-fp
mcnulty-fp previously approved these changes Jul 21, 2026

@mcnulty-fp mcnulty-fp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great work! These changes make sense to me. As I was reviewing the changes, I was comparing the mental model of useVisitorData to tanstack's useQuery and after these changes, it seems more inline with that. My review only includes some non-functional nits otherwise.

RE: the following change:

Scenario Before After
immediate: true → false during an automatic request Loading continued and the result was applied Loading clears and the automatic result is ignored

This might warrant a minor version bump. I see this as a bug fix because the "After" behavior makes more intuitive sense and was probably the intended way to make it work. But, there could be some code out there that is relying on being able to start out with immediate: true and then it is mistakenly changing to immediate: false and still expecting the "mount" get call to be returned. So, it could be useful to advertise this behavior change more prominently with a minor version.

Comment thread __tests__/use-visitor-data.test.tsx Outdated
import { useVisitorData, UseVisitorDataReturn } from '../src'
import { act, render, renderHook, screen } from '@testing-library/react'
import { act, render, renderHook, screen, waitFor } from '@testing-library/react'
import { actWait, createWrapper, wait } from './helpers'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: perhaps a change for another PR, but it looks like after these changes, all the actWait calls can be replaced with await act(async () => {}) calls, meaning the wrapped wait calls are unnecessary.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! Not worth the PR overhead imo, done here: 046727d

Comment thread src/use-visitor-data.ts Outdated
Comment thread src/use-visitor-data.ts Outdated
Comment thread src/use-visitor-data.ts Outdated
ignore = true
}
}, [immediate, getData])
}, [immediate, getVisitorData, currentGetOptions])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: while it doesn't functionally change the code, it could improve readability to use currentImmediate here instead to align with the usage currentGetOptions (rather than getOptions).

Just to spell it out, using currentImmediate provides the same behavior as using immediate because when currentImmediate !== immediate, the effect "scheduled" during the current render is not executed because there was a state update during the render. So, the effect that is always executed is from the render where currentImmediate == immediate`.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense to me, thanks: c11d5b1

Co-authored-by: Dan McNulty <212590662+mcnulty-fp@users.noreply.github.com>
JuroUhlar and others added 5 commits July 22, 2026 10:07
Co-authored-by: Dan McNulty <212590662+mcnulty-fp@users.noreply.github.com>
Remove arbitrary timeouts from useVisitorData tests now that async hook updates flush reliably.
@JuroUhlar

Copy link
Copy Markdown
Collaborator Author

So, it could be useful to advertise this behavior change more prominently with a minor version.

Thanks! How about this: 43b4d29 ?

@JuroUhlar
JuroUhlar requested a review from mcnulty-fp July 22, 2026 09:18
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Following releases will be created using changesets from this PR:

@fingerprint/react@3.1.0

Minor Changes

  • Behavior bug fixes:

    • Stale automatic useVisitorData requests (when immediate changes from true to false during an automatic request) no longer overwrite state (the result is ignored).
    • Loading state is now correctly synchronized when immediate changes.

    We recommend double-checking that your implementation isn't relying on the previous incorrect behavior when upgrading. (f1671b0)

  • Add support for React 19 and Next.js 16. The react peer range now accepts >=18 <20. (5307b3b)

Patch Changes

  • Bump @fingerprint/agent dependency to ^4.1.2 and drop the fast-deep-equal runtime dependency in favor of an internal cache-key comparison. (87b776f)

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.

5 participants