Skip to content

fix(registration-form): break post-purchase modal remount loop#144

Merged
smarcet merged 3 commits into
mainfrom
fix/post-purchase-remount-loop
May 22, 2026
Merged

fix(registration-form): break post-purchase modal remount loop#144
smarcet merged 3 commits into
mainfrom
fix/post-purchase-remount-loop

Conversation

@gcutrini
Copy link
Copy Markdown
Member

@gcutrini gcutrini commented May 22, 2026

ref: https://app.clickup.com/t/86ba2ryp0

Summary

Post-payment, the modal entered an infinite <PurchaseComplete> remount cycle (nonstop reloads, repeated members/me, shareable-link, promo-codes/discover, ticket-types/allowed). Standalone /register page unaffected: it redirects out on payment success.

Root cause

Two compounding issues:

  1. Effects dep'd on profileData / summitData object references. Consumers mint new idpProfile refs on every dispatch (including no-op), so effects refired without any real change.

  2. ticketDataLoaded was used as both "have we loaded?" (milestone) and "are we fetching?" (transient). Resetting it to false on every refetch made the first-load null-render guard blank the whole form subtree mid-flow, unmounting <PurchaseComplete> and re-firing its mount-effect (onPurchaseCompletegetUserProfile → new idpProfile ref → loop).

Changes

  • Renamed ticketDataLoadedhasTicketData. Milestone-only: false initially, true after first successful fetch, never resets.
  • handleGetTicketTypesAndTaxes no longer resets the milestone; promotes it to true only on success (.then, not .finally).
  • Derived isAuthenticated, summitId, userId locally. Effects now dep on those primitives, not object refs.
  • Updated useMemo deps and the early-return guard to match.

Summary by CodeRabbit

  • Refactor
    • Improved registration form state handling to make ticket availability, invitations and promo-code discovery more reliable and to avoid unnecessary re-fetches during profile updates.
  • Tests
    • Added regression tests to ensure effects only re-run when actual event or user identifiers change, preventing redundant network calls and UI flicker.

Review Change Stack

- ticketDataLoaded was overloaded as both "have we ever loaded?" milestone and "are we currently fetching?" transient. Renamed to hasTicketData; stays true once set, never resets on refetch. Promoted to true on .then (success only) instead of .finally.
- Derived isAuthenticated/summitId/userId from props and used those as effect deps so effects fire on real auth/summit transitions, not on profileData/summitData object identity churn.
@gcutrini gcutrini requested a review from smarcet May 22, 2026 16:05
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

The registration-form component replaces ticketDataLoaded with hasTicketData (plus derived isAuthenticated, summitId, userId), updates effects and the ticket/tax handler to set hasTicketData on success, rewires the promo hook and render gate, and adds regression tests for effect dependency stability.

Changes

Registration Form State Refactor

Layer / File(s) Summary
State declaration and ticket-type derivations
src/components/registration-form/index.js
New hasTicketData state and derived values (isAuthenticated, summitId, userId) replace ticketDataLoaded; ticket-type availability flags now use hasTicketData, and profile-loading effect depends on userId.
Invitation, ticket-type fetch handler, and promo discovery
src/components/registration-form/index.js
Invitation effect now depends on summitId/isAuthenticated; promo discovery runs when those exist; handleGetTicketTypesAndTaxes sets hasTicketData only on successful fetch and sets ticketDataError on failure.
Promo-code hook wiring and render gate
src/components/registration-form/index.js
usePromoCode receives hasTicketData && !ticketDataError; the authenticated initial-render guard blocks UI until hasTicketData or ticketDataError is present.
Regression tests for effect dependencies
src/components/registration-form/__tests__/registration-form.test.js
Adds tests verifying effects do not re-run on object-reference-only changes to profileData but do re-run when summitData.id changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • smarcet
  • santipalenque

Poem

🐰 I sniffed the flags and found a clue,
Old loaded ghosts replaced by new,
Summit IDs now call the song,
Effects behave and tests belong,
Hoppity hop — the code’s anew!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(registration-form): break post-purchase modal remount loop' directly addresses the main change: fixing an infinite remount loop in the post-purchase modal by refactoring state dependencies and effect triggers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/post-purchase-remount-loop

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/components/registration-form/index.js (2)

205-205: ⚡ Quick win

Consider memoizing allowedTicketTypes to prevent unnecessary recalculations.

Since allowedTicketTypes is computed on every render and used as a dependency in the useMemo calls on lines 207-208, those memos will recalculate on every render because the array reference changes each time. Wrapping this in a useMemo would stabilize the reference and improve performance.

♻️ Proposed fix to memoize allowedTicketTypes
-const allowedTicketTypes = hasTicketData ? ticketTypes.filter((tt) => tt.sub_type === TICKET_TYPE_SUBTYPE_PREPAID || (tt.sales_start_date === null && tt.sales_end_date === null) || (nowUtc >= tt.sales_start_date && nowUtc <= tt.sales_end_date)) : [];
+const allowedTicketTypes = useMemo(() => 
+  hasTicketData ? ticketTypes.filter((tt) => tt.sub_type === TICKET_TYPE_SUBTYPE_PREPAID || (tt.sales_start_date === null && tt.sales_end_date === null) || (nowUtc >= tt.sales_start_date && nowUtc <= tt.sales_end_date)) : [],
+  [hasTicketData, ticketTypes, nowUtc]
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/registration-form/index.js` at line 205, allowedTicketTypes is
recomputed on every render which destabilizes its reference and forces
downstream useMemo hooks to recompute; wrap the calculation in a useMemo so the
array reference is stable. Replace the inline filter with: const
allowedTicketTypes = useMemo(() => hasTicketData ? ticketTypes.filter(tt =>
tt.sub_type === TICKET_TYPE_SUBTYPE_PREPAID || (tt.sales_start_date === null &&
tt.sales_end_date === null) || (nowUtc >= tt.sales_start_date && nowUtc <=
tt.sales_end_date)) : [], [hasTicketData, ticketTypes, nowUtc,
TICKET_TYPE_SUBTYPE_PREPAID]); this keeps the same logic but memoizes the result
for use by the existing useMemo calls.

16-16: Reframe React 16.8.4 concern to standard Hooks correctness (and optionally plan an upgrade)

There’s no widely documented, React 16.8.4–specific bug in useEffect dependency handling or useState update semantics. The real risk in this hooks-heavy refactor is the usual ones: correct dependency arrays to avoid stale closures, stable dependency identities to prevent unnecessary re-renders/loops, and awareness that effects run after render and state updates are batched (with re-renders skipped when the next state is Object.is-equal). Consider upgrading React from 16.8.x when practical.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/registration-form/index.js` at line 16, The review comment
should be reframed from claiming a React 16.8.4-specific bug to calling out
general Hooks correctness: update the PR note to instruct reviewers/authors to
audit dependency arrays and stable identities for hooks used in this file
(useEffect, useCallback, useMemo, useState) — specifically check any closures in
the RegistrationForm component (or similarly named component/function) for stale
references, ensure functions passed as dependencies are memoized (e.g., via
useCallback), objects/arrays passed as deps are stable or memoized (useMemo),
and that effects expect they run after render; optionally recommend planning a
React upgrade from 16.8.x in the project roadmap.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/components/registration-form/index.js`:
- Line 205: allowedTicketTypes is recomputed on every render which destabilizes
its reference and forces downstream useMemo hooks to recompute; wrap the
calculation in a useMemo so the array reference is stable. Replace the inline
filter with: const allowedTicketTypes = useMemo(() => hasTicketData ?
ticketTypes.filter(tt => tt.sub_type === TICKET_TYPE_SUBTYPE_PREPAID ||
(tt.sales_start_date === null && tt.sales_end_date === null) || (nowUtc >=
tt.sales_start_date && nowUtc <= tt.sales_end_date)) : [], [hasTicketData,
ticketTypes, nowUtc, TICKET_TYPE_SUBTYPE_PREPAID]); this keeps the same logic
but memoizes the result for use by the existing useMemo calls.
- Line 16: The review comment should be reframed from claiming a React
16.8.4-specific bug to calling out general Hooks correctness: update the PR note
to instruct reviewers/authors to audit dependency arrays and stable identities
for hooks used in this file (useEffect, useCallback, useMemo, useState) —
specifically check any closures in the RegistrationForm component (or similarly
named component/function) for stale references, ensure functions passed as
dependencies are memoized (e.g., via useCallback), objects/arrays passed as deps
are stable or memoized (useMemo), and that effects expect they run after render;
optionally recommend planning a React upgrade from 16.8.x in the project
roadmap.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5615b258-6435-47db-80a8-8032245b4a0c

📥 Commits

Reviewing files that changed from the base of the PR and between 791d42d and d983575.

📒 Files selected for processing (1)
  • src/components/registration-form/index.js

@gcutrini
Copy link
Copy Markdown
Member Author

@coderabbitai

  1. Memoized allowedTicketTypes per your suggestion — c611344.

  2. The "React 16.8.4 concern" you flagged at line 16 doesn't exist in this PR. Line 16 is just an import statement; there is no comment, JSDoc, or PR note about a React 16.8.4-specific bug anywhere in the diff. Skipping as a false positive.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Tip

For best results, initiate chat on the files or code changes.

@gcutrini: Thanks for the updates!

  1. allowedTicketTypes memoization — Great, the fix at c611344 looks good. ✅

  2. React 16.8.4 false positive — You're correct, I apologize for the noise. I'll record this to avoid similar false positives in the future.

(ᵔᴥᵔ) 🐇


✏️ Learnings added
Learnt from: gcutrini
URL: https://github.com/fntechgit/summit-registration-lite/pull/144

Timestamp: 2026-05-22T16:18:42.687Z
Learning: In the `summit-registration-lite` repository (fntechgit/summit-registration-lite), do not flag React version-specific concerns (e.g., React 16.8.4 hook bugs) based on line numbers alone without verifying that the line actually contains relevant code, comments, or JSDoc. Line 16 of `src/components/registration-form/index.js` is just an import statement and not related to any React 16.8.4-specific behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@smarcet
Copy link
Copy Markdown
Collaborator

smarcet commented May 22, 2026

@gcutrini we are missing tests here

  1. Core regression guard (most important)
    The bug was a post-purchase remount loop. There's no test proving it's fixed:
  • Render with profileData set + step = STEP_COMPLETE → verify getTicketTypesAndTaxes is called exactly once on mount and does not re-fire when profileData reference changes.
  1. hasTicketData milestone
  • getTicketTypesAndTaxes resolves → hasTicketData becomes true (component unblocks from null-render).
  • Calling handleGetTicketTypesAndTaxes a second time does not reset hasTicketData to false (no blank-screen flash).
  1. ticketDataError path
  • getTicketTypesAndTaxes rejects → ticketDataError is true, hasTicketData stays false.
  1. Effect re-fires on summitId / isAuthenticated change
  • Changing summitId triggers a new getTicketTypesAndTaxes call.
  • profileData going from null → object (login) triggers the fetch.

Copy link
Copy Markdown
Collaborator

@smarcet smarcet left a comment

Choose a reason for hiding this comment

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

@gcutrini please provide regression tests

@gcutrini gcutrini requested a review from smarcet May 22, 2026 17:59
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/components/registration-form/__tests__/registration-form.test.js (2)

312-313: ⚡ Quick win

Stabilize async assertions by waiting on observable outcomes, not empty act ticks.

Using await act(async () => {}); is timing-sensitive here and can make call-count assertions flaky. Prefer waitFor around the specific expectation after render/rerender.

Suggested change
-import { cleanup, render, act } from '`@testing-library/react`';
+import { cleanup, render, act, waitFor } from '`@testing-library/react`';
...
-    await act(async () => {});
-
-    expect(mockGetTicketTypesAndTaxes).toHaveBeenCalledTimes(1);
+    await waitFor(() => {
+        expect(mockGetTicketTypesAndTaxes).toHaveBeenCalledTimes(1);
+    });
...
-    await act(async () => {});
-
-    expect(mockGetTicketTypesAndTaxes).not.toHaveBeenCalled();
+    await waitFor(() => {
+        expect(mockGetTicketTypesAndTaxes).not.toHaveBeenCalled();
+    });

Also applies to: 329-330, 351-352, 369-370, 391-392, 408-409

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/registration-form/__tests__/registration-form.test.js` around
lines 312 - 313, The tests use empty ticks like await act(async () => {}); which
is flaky; replace each of those (occurrences at the empty act calls) with await
waitFor(() => <the specific assertion>) from `@testing-library/react` so you wait
on observable outcomes (e.g., await waitFor(() =>
expect(mockFn).toHaveBeenCalledTimes(n)) or await waitFor(() =>
expect(screen.getByText('...')).toBeInTheDocument())) instead of an empty act;
update imports to include waitFor if needed and apply this change to all
empty-act instances mentioned (the ones around lines 312-313, 329-330, 351-352,
369-370, 391-392, 408-409).

295-414: ⚡ Quick win

Regression coverage is still missing milestone/error-path guards requested for this fix.

These tests cover object-reference churn and summit-id changes well, but they don’t yet assert: (1) hasTicketData stays true after first success and is not reset on subsequent refetches, and (2) rejected ticket-data fetch sets ticketDataError while leaving hasTicketData false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/registration-form/__tests__/registration-form.test.js` around
lines 295 - 414, Add two tests inside the RegistrationForm test suite to cover
the missing milestone/error-path guards: (1) after a successful ticket-types
fetch via mockGetTicketTypesAndTaxes verify the component/store flag
hasTicketData remains true after subsequent refetches (e.g., initial render,
clear mocks, force a refetch by changing summitData or profileData, then assert
hasTicketData is still true and ticketDataError is falsy); (2) simulate a
rejected ticket-types fetch by making mockGetTicketTypesAndTaxes return a
rejected promise on the next call and assert that ticketDataError is set
(non-null) and hasTicketData stays/returns false; locate assertions against the
component state or Redux slice the RegistrationForm uses (look for hasTicketData
and ticketDataError reads) and use the existing mock helpers
(mockGetTicketTypesAndTaxes) and rerender pattern to trigger and verify both
success and error paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/components/registration-form/__tests__/registration-form.test.js`:
- Around line 312-313: The tests use empty ticks like await act(async () => {});
which is flaky; replace each of those (occurrences at the empty act calls) with
await waitFor(() => <the specific assertion>) from `@testing-library/react` so you
wait on observable outcomes (e.g., await waitFor(() =>
expect(mockFn).toHaveBeenCalledTimes(n)) or await waitFor(() =>
expect(screen.getByText('...')).toBeInTheDocument())) instead of an empty act;
update imports to include waitFor if needed and apply this change to all
empty-act instances mentioned (the ones around lines 312-313, 329-330, 351-352,
369-370, 391-392, 408-409).
- Around line 295-414: Add two tests inside the RegistrationForm test suite to
cover the missing milestone/error-path guards: (1) after a successful
ticket-types fetch via mockGetTicketTypesAndTaxes verify the component/store
flag hasTicketData remains true after subsequent refetches (e.g., initial
render, clear mocks, force a refetch by changing summitData or profileData, then
assert hasTicketData is still true and ticketDataError is falsy); (2) simulate a
rejected ticket-types fetch by making mockGetTicketTypesAndTaxes return a
rejected promise on the next call and assert that ticketDataError is set
(non-null) and hasTicketData stays/returns false; locate assertions against the
component state or Redux slice the RegistrationForm uses (look for hasTicketData
and ticketDataError reads) and use the existing mock helpers
(mockGetTicketTypesAndTaxes) and rerender pattern to trigger and verify both
success and error paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1323d82e-2371-4287-ba34-c77d52c06ef4

📥 Commits

Reviewing files that changed from the base of the PR and between d983575 and 547a640.

📒 Files selected for processing (2)
  • src/components/registration-form/__tests__/registration-form.test.js
  • src/components/registration-form/index.js

Copy link
Copy Markdown
Collaborator

@smarcet smarcet left a comment

Choose a reason for hiding this comment

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

LGTM

@smarcet smarcet merged commit 97fe387 into main May 22, 2026
3 checks passed
@gcutrini gcutrini deleted the fix/post-purchase-remount-loop branch May 22, 2026 21:46
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.

2 participants