INTER-2332: avoid setState-in-effect and stale responses in immediate mount fetch#206
INTER-2332: avoid setState-in-effect and stale responses in immediate mount fetch#206JuroUhlar wants to merge 33 commits into
Conversation
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.
Coverage report
Show files with reduced coverage 🔻
Test suite run success28 tests passing in 15 suites. Report generated by 🧪jest coverage report action from e02506d Show full coverage report
|
There was a problem hiding this comment.
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
getVisitorDatadirectly in the effect with anignorecleanup flag to prevent stale updates. - Centralized query state transitions into
setLoading/setSuccess/setFailure, and added a render-phase loading reset whengetOptionschange. - 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.
8153bc8 to
df6eefc
Compare
|
@copilot resolve the merge conflicts in this pull request |
Resolved. I merged |
There was a problem hiding this comment.
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.
| 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' |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Good catch! Not worth the PR overhead imo, done here: 046727d
| ignore = true | ||
| } | ||
| }, [immediate, getData]) | ||
| }, [immediate, getVisitorData, currentGetOptions]) |
There was a problem hiding this comment.
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`.
Co-authored-by: Dan McNulty <212590662+mcnulty-fp@users.noreply.github.com>
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.
Thanks! How about this: 43b4d29 ? |
🚀 Following releases will be created using changesets from this PR:@fingerprint/react@3.1.0Minor Changes
Patch Changes
|
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.
immediatebehavior and error handling.Behavior
immediate: true → falseduring an automatic requestimmediate: false → truefalse → trueimmediate: truegetData()Changing
immediatetofalsedoes not abort the network request. Only the automatic result is ignored; manualgetData()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
getDatasynchronously 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.