Skip to content

fix(clock): stop tick-driven re-renders in registration widget - #145

Merged
smarcet merged 6 commits into
mainfrom
feature/clock-provider-migration
Jun 5, 2026
Merged

fix(clock): stop tick-driven re-renders in registration widget#145
smarcet merged 6 commits into
mainfrom
feature/clock-provider-migration

Conversation

@gcutrini

@gcutrini gcutrini commented May 28, 2026

Copy link
Copy Markdown
Member

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

Summary

The widget's Clock component dispatched UPDATE_CLOCK to Redux every second, causing every connected component to re-render on every tick — including TicketDropdownComponent, which only depends on the filtered list of available tickets.

Replace the internal Redux clock with uicore 4.2.31's ClockProvider and switch consumers to useClockSelector so the form only re-renders when the derived value actually changes.

  • registration-form: allowedTicketTypes recomputes every tick but only commits a new array when a sale window opens/closes (shallowEqual)
  • purchase-complete: isActive only flips when the summit transitions active/inactive
  • Drops UPDATE_CLOCK action, reducer case, and nowUtc state field
  • Renames withReduxProviderwithWidgetProviders since it now hosts both the Redux Provider and the ClockProvider
  • Bumps openstack-uicore-foundation to 4.2.31 (deps + peerDeps)

The widget remains self-contained: each instance mounts its own ClockProvider. If the host already provides one, React context shadowing keeps the widget's own active — no conflict.

Summary by CodeRabbit

  • Chores
    • Updated openstack-uicore-foundation to version 4.2.31
    • Added spark-md5 v3.0.2 as a dependency

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@gcutrini, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 56 minutes and 34 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c8fa06f-6219-4306-bf28-4b7b06e7008a

📥 Commits

Reviewing files that changed from the base of the PR and between c05158b and 94178c5.

📒 Files selected for processing (1)
  • src/utils/__tests__/withWidgetProviders.test.js
📝 Walkthrough

Walkthrough

This PR replaces Redux-based clock management with a provider-based pattern using ClockProvider and useClockSelector. The withWidgetProviders HOC now supplies live clock context to wrapped components, eliminating the need for Redux clock updates. Dependencies on openstack-uicore-foundation and spark-md5 are updated.

Changes

Clock Provider Refactor

Layer / File(s) Summary
Dependency bump and withWidgetProviders HOC implementation
package.json, src/utils/withWidgetProviders.js
openstack-uicore-foundation bumped to 4.2.31, spark-md5 added at ^3.0.2. New withWidgetProviders HOC wraps Redux Provider → PersistGate → ClockProvider, supplying timezone from summit config and live timestamp context.
withWidgetProviders test updates
src/utils/__tests__/withWidgetProviders.test.js
Tests updated to verify store creation, prop pass-through, and caching via withWidgetProviders; new test asserts ClockProvider supplies live UTC timestamp via useClockSelector.
Remove Redux clock infrastructure
src/actions.js, src/reducer.js
Delete UPDATE_CLOCK action constant and updateClock thunk; remove moment dependency, nowUtc initialization, and UPDATE_CLOCK reducer case.
PurchaseComplete: clock-driven isActive
src/components/purchase-complete/index.js
Import useClockSelector and useCallback; remove nowUtc prop; replace useMemo isActive with useClockSelector predicate keyed to summit window dates.
PurchaseComplete test suite (new)
src/components/purchase-complete/__tests__/purchase-complete.test.js
New comprehensive test file with deterministic clock mocking; tests conditional UI rendering when clock is inside vs. outside the summit window.
RegistrationForm: clock integration and HOC update
src/components/registration-form/index.js
Remove Clock component and updateClock wiring. Add isTicketCurrentlyAvailable helper encapsulating prepaid and sales-window logic. Replace allowedTicketTypes useMemo with useClockSelector+shallowEqual. Update default export to withWidgetProviders.
RegistrationForm tests update
src/components/registration-form/__tests__/registration-form.test.js
Mock withWidgetProviders as identity HOC; stub clock-context with controllable mockClockNow; remove updateClock action stub; drop nowUtc from test state; add ticket availability window regression test.
RegistrationModal: HOC wrapper update
src/components/registration-modal/index.js, src/components/registration-modal/__tests__/registration-modal.test.js
Switch default export wrapper and test mocks from withReduxProvider to withWidgetProviders.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • smarcet
  • santipalenque

Poem

🐰 The clock ticks on without Redux's grand array,
Context whispers time through Provider's way,
Tickets bloom and fade with nature's decree,
One HOC to bind them, clean and carefree!
✨ Clock Provider set the registration free.

🚥 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 accurately describes the main change: migrating from a tick-driven Redux clock to ClockProvider to reduce unnecessary re-renders in the registration widget.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/clock-provider-migration

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.

@gcutrini
gcutrini requested a review from smarcet May 28, 2026 20:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

90-97: 💤 Low value

Guard against partial-null sales window dates in isTicketCurrentlyAvailable.

The current logic treats “open-ended” only when both sales_start_date and sales_end_date are null; if exactly one is null, the comparison (nowUtc >= tt.sales_start_date && nowUtc <= tt.sales_end_date) can mis-evaluate due to JS null coercion. The repo fixtures/tests show the both-null case (sales_start_date: null + sales_end_date: null) but no partial-null example, so either handle the partial-null case explicitly or assert upstream that these fields are always paired. (src/components/registration-form/index.js:90-97; e2e/fixtures.js; src/components/ticket-dropdown/tests/ticket-dropdown.test.js)

🤖 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` around lines 90 - 97, The
isTicketCurrentlyAvailable helper currently only treats both sales_start_date
and sales_end_date null as open-ended and then uses a two-sided comparison which
misbehaves if one side is null; update isTicketCurrentlyAvailable to explicitly
handle partial-null windows: keep the prepaid check (tt.sub_type ===
TICKET_TYPE_SUBTYPE_PREPAID) and the both-null case, then if sales_start_date is
null treat the ticket as available when nowUtc <= tt.sales_end_date, if
sales_end_date is null treat it as available when nowUtc >= tt.sales_start_date,
otherwise use the existing two-sided check (nowUtc >= tt.sales_start_date &&
nowUtc <= tt.sales_end_date); reference the function isTicketCurrentlyAvailable
and the fields tt.sales_start_date / tt.sales_end_date to locate the change.
src/utils/__tests__/withWidgetProviders.test.js (1)

83-85: ⚡ Quick win

Strengthen the clock assertion to catch unit regressions.

year >= 2024 is too permissive; assert proximity to current epoch seconds instead so ms/sec mismatches fail loudly.

Suggested test hardening
-        const year = useClockSelector((nowUtc) =>
-            nowUtc ? new Date(nowUtc * 1000).getUTCFullYear() : null
-        );
-        return <div data-testid="year">{year ?? 'null'}</div>;
+        const nowUtc = useClockSelector((value) => value);
+        return <div data-testid="now-utc">{nowUtc ?? 'null'}</div>;
@@
-    expect(Number(getByTestId('year').textContent)).toBeGreaterThanOrEqual(2024);
+    const renderedNowUtc = Number(getByTestId('now-utc').textContent);
+    expect(Number.isFinite(renderedNowUtc)).toBe(true);
+    expect(Math.abs(renderedNowUtc - Math.floor(Date.now() / 1000))).toBeLessThanOrEqual(5);

Also applies to: 97-97

🤖 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/utils/__tests__/withWidgetProviders.test.js` around lines 83 - 85, The
test currently asserts a loose year check (year >= 2024) for the value returned
by useClockSelector; tighten this by asserting the returned epoch seconds
(nowUtc) or computed year is close to the current time to catch ms/sec unit
regressions: in the test(s) that call useClockSelector (the lines around the
useClockSelector call), assert nowUtc is non-null and that Math.abs(nowUtc -
Math.floor(Date.now()/1000)) is within a small delta (e.g. 2–5 seconds) or
equivalently compute year from Date.now() and assert the computed year equals
the selector year; update both occurrences (around lines with useClockSelector
at 83 and 97) to use this proximity check.
🤖 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.

Inline comments:
In `@src/utils/withWidgetProviders.js`:
- Line 36: The now prop currently uses Math.round(Date.now() / 1000) which can
advance the epoch-second by ~0.5s; change the conversion to use
Math.floor(Date.now() / 1000) so epoch seconds never jump forward prematurely.
Locate the now={...} assignment in withWidgetProviders.js (the now prop passed
into the widget/provider wrapper) and replace the Math.round call with
Math.floor to ensure floor semantics for sale-window checks.

---

Nitpick comments:
In `@src/components/registration-form/index.js`:
- Around line 90-97: The isTicketCurrentlyAvailable helper currently only treats
both sales_start_date and sales_end_date null as open-ended and then uses a
two-sided comparison which misbehaves if one side is null; update
isTicketCurrentlyAvailable to explicitly handle partial-null windows: keep the
prepaid check (tt.sub_type === TICKET_TYPE_SUBTYPE_PREPAID) and the both-null
case, then if sales_start_date is null treat the ticket as available when nowUtc
<= tt.sales_end_date, if sales_end_date is null treat it as available when
nowUtc >= tt.sales_start_date, otherwise use the existing two-sided check
(nowUtc >= tt.sales_start_date && nowUtc <= tt.sales_end_date); reference the
function isTicketCurrentlyAvailable and the fields tt.sales_start_date /
tt.sales_end_date to locate the change.

In `@src/utils/__tests__/withWidgetProviders.test.js`:
- Around line 83-85: The test currently asserts a loose year check (year >=
2024) for the value returned by useClockSelector; tighten this by asserting the
returned epoch seconds (nowUtc) or computed year is close to the current time to
catch ms/sec unit regressions: in the test(s) that call useClockSelector (the
lines around the useClockSelector call), assert nowUtc is non-null and that
Math.abs(nowUtc - Math.floor(Date.now()/1000)) is within a small delta (e.g. 2–5
seconds) or equivalently compute year from Date.now() and assert the computed
year equals the selector year; update both occurrences (around lines with
useClockSelector at 83 and 97) to use this proximity check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b00b062c-176b-430a-b8e8-6241d725aa7b

📥 Commits

Reviewing files that changed from the base of the PR and between 0ae4eaf and 3dd295c.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (10)
  • package.json
  • src/actions.js
  • src/components/purchase-complete/index.js
  • src/components/registration-form/__tests__/registration-form.test.js
  • src/components/registration-form/index.js
  • src/components/registration-modal/__tests__/registration-modal.test.js
  • src/components/registration-modal/index.js
  • src/reducer.js
  • src/utils/__tests__/withWidgetProviders.test.js
  • src/utils/withWidgetProviders.js
💤 Files with no reviewable changes (2)
  • src/reducer.js
  • src/actions.js

The widget's Clock component dispatched UPDATE_CLOCK to Redux every
second, so every connected component re-rendered on every tick —
including TicketDropdownComponent, which only depends on the filtered
list of available tickets.

Replace the internal Redux clock with uicore 4.2.31's ClockProvider
and switch consumers to useClockSelector so the form only re-renders
when the derived value actually changes:

- registration-form: allowedTicketTypes recomputes every tick but only
  commits a new array when a sale window opens/closes (shallowEqual)
- purchase-complete: isActive only flips when the summit transitions
  active/inactive

Also drops the UPDATE_CLOCK action, reducer case, and nowUtc state
field, and renames withReduxProvider to withWidgetProviders since it
now hosts both the Redux Provider and the ClockProvider.
@gcutrini
gcutrini force-pushed the feature/clock-provider-migration branch 2 times, most recently from 17b110d to b1b1295 Compare June 4, 2026 21:09
@fntechgit fntechgit deleted a comment from coderabbitai Bot Jun 4, 2026
Comment thread src/utils/withWidgetProviders.js Outdated
<WrappedComponent {...this.props} />
<ClockProvider
timezone={summitData?.time_zone_id || 'UTC'}
now={Math.round(Date.now() / 1000)}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

use Math.floor

uicore 4.2.31's compiled output (notably company-input-v2) imports
spark-md5, declared as a peer dep. CI's clean install can't resolve
it, so webpack-dev-server emits "Module not found" and the widget
renders the error overlay instead of the React tree — every e2e
test failed looking for elements the React app never mounted.
Locally the dep was hoisted from elsewhere so the issue stayed
hidden until rebuilding node_modules from the lockfile.

// Stub the uicore clock-context so the selector runs once against a fixed timestamp
jest.mock('openstack-uicore-foundation/lib/components/clock-context', () => ({
useClockSelector: (selector) => selector(1000000),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The 1000000 seed (≈ Unix epoch 1970-01-12) means isTicketCurrentlyAvailable returns false for every ticket with a real sales window, so allowedTicketTypes is always [] in this suite.

Missing: a test that supplies a ticket fixture with sales_start_date <= 1000000 <= sales_end_date (or bumps the seed to a realistic value and matches the fixture), then asserts the ticket appears in the rendered component. Without it, a bug in the new isTicketCurrentlyAvailable helper — the core of what this PR optimizes — would go undetected.

// active/inactive transitions (typically zero times per session).
const isActive = useClockSelector(
useCallback(
(nowUtc) => summit.start_date <= nowUtc && summit.end_date >= nowUtc,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No __tests__/ directory exists for PurchaseComplete. The isActive value is now derived from useClockSelector, but neither branch is exercised: the active-summit CTA path (isActive === true) and the "event will start on…" path (isActive === false) are both untested.

Missing: mount PurchaseComplete with a mocked useClockSelector that returns a timestamp inside summit.start_date / summit.end_date and assert the active CTA renders; then one where the timestamp is outside the window and assert the "event will start" copy renders.

apiBaseUrl="https://api.test.com"
getAccessToken={() => 'token'}
summitData={{ time_zone_id: 'UTC' }}
/>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This test confirms the clock is live at initial mount. Missing: a test that re-renders Wrapped with a different summitData.time_zone_id prop and asserts useClockSelector still resolves a valid timestamp — covering the WithWidgetProviders PureComponent re-render path where a fresh timezone and now value propagate to ClockProvider. That path is the one flagged for risk in the review.

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@gcutrini please review

gcutrini added 4 commits June 4, 2026 18:35
Per review: Math.floor is the standard truncate-to-second; Math.round
could nudge the seed up by ~0.5s on average. Functionally tiny but
floor is the convention.
Per review: the 1970-shaped clock seed used everywhere else in the
suite makes every realistic sales window evaluate to false, so a bug
in the new isTicketCurrentlyAvailable helper would not surface here.
Makes the clock seed and TicketTypeComponent's last props settable per
test, then adds a regression that mixes in-window, expired, always-open
and prepaid fixtures and asserts only the expected ones reach the
rendered allowedTicketTypes prop.
Per review: PurchaseComplete had no test directory and the new
useClockSelector-derived isActive boolean was unexercised. Adds one
test per branch — seed inside summit.start_date/end_date asserts the
active CTA path renders, seed outside asserts the "event will start
on…" copy renders.
…zone

Per review: the existing test only confirms the clock is live at
initial mount. Adds a rerender with a different summitData.time_zone_id
and asserts useClockSelector still resolves a valid year — exercising
the PureComponent re-render that propagates a fresh timezone/now into
ClockProvider, the path flagged as the risk in review.
@smarcet
smarcet self-requested a review June 5, 2026 14:29

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

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 167a675 into main Jun 5, 2026
3 checks passed
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