Skip to content

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

Open
gcutrini wants to merge 1 commit into
mainfrom
feature/clock-provider-migration
Open

fix(clock): stop tick-driven re-renders in registration widget#145
gcutrini wants to merge 1 commit into
mainfrom
feature/clock-provider-migration

Conversation

@gcutrini
Copy link
Copy Markdown
Member

@gcutrini gcutrini commented May 28, 2026

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 foundation library dependencies to version 4.2.31.
  • Improvements

    • Refactored time-based ticket availability logic for improved real-time accuracy. The system now evaluates ticket eligibility on each clock tick, ensuring event windows and prepaid ticket availability are checked precisely without unnecessary re-renders.

Review Change Stack

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

📝 Walkthrough

Walkthrough

This PR refactors the clock/time management system by replacing Redux-based UPDATE_CLOCK actions with an OpenStack UICore ClockProvider and useClockSelector hook pattern. The openstack-uicore-foundation dependency is upgraded to 4.2.31, a new withWidgetProviders HOC wraps components with ClockProvider, and time-based logic in registration and purchase components now subscribes to live clock ticks instead of Redux state.

Changes

Clock Provider Refactor

Layer / File(s) Summary
Dependency upgrade and withWidgetProviders HOC
package.json, src/utils/withWidgetProviders.js
openstack-uicore-foundation bumped to 4.2.31. The withReduxProvider HOC is replaced with withWidgetProviders, which wraps WrappedComponent in Redux ProviderPersistGateClockProvider, passing timezone from summit config and current Unix timestamp in seconds.
withWidgetProviders test suite
src/utils/__tests__/withWidgetProviders.test.js
Tests updated to validate the new HOC: store creation via getStore/getPersistor, props passthrough, store caching across re-renders, displayName handling, and useClockSelector resolution against a live timestamp.
Remove Redux clock infrastructure
src/actions.js, src/reducer.js
UPDATE_CLOCK action type constant and updateClock(timestamp) thunk removed from actions; UPDATE_CLOCK case handler and nowUtc state field removed from reducer.
RegistrationForm component refactor
src/components/registration-form/index.js
Removes Clock component import/render and updateClock action wiring. Adds isTicketCurrentlyAvailable helper. Replaces useMemo-based allowedTicketTypes filter with useClockSelector hook using shallowEqual to avoid unnecessary re-renders on tick. Removes nowUtc prop from PurchaseComplete. Updates default export to use withWidgetProviders instead of withReduxProvider.
RegistrationForm test suite
src/components/registration-form/__tests__/registration-form.test.js
Mocks updated: withWidgetProviders replaces withReduxProvider; clock-context stubbed with fixed timestamp selector; updateClock removed from mocked actions; clock component mock removed; nowUtc removed from default Redux state.
PurchaseComplete component update
src/components/purchase-complete/index.js
Adds useCallback import and useClockSelector hook. Replaces useMemo calculation of isActive with useClockSelector-driven subscription that evaluates summit start/end dates on each clock tick.
RegistrationModal wrapper and tests
src/components/registration-modal/index.js, src/components/registration-modal/__tests__/registration-modal.test.js
Default export and test mocks updated from withReduxProvider to withWidgetProviders.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • smarcet

Poem

🐰 Clock ticks now flow through Provider's vein,
No Redux actions causing refrain,
Selectors pluck timestamps from the air,
Components refresh when moments are there!
Time marches on, both lean and fair! ⏰

🚥 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 summarizes the main change: migrating from Redux-based clock tick updates to a more efficient clock provider approach that reduces 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/clock-provider-migration

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.

@gcutrini gcutrini requested a review from smarcet May 28, 2026 20:31
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.

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

<WrappedComponent {...this.props} />
<ClockProvider
timezone={summitData?.time_zone_id || 'UTC'}
now={Math.round(Date.now() / 1000)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use floor for epoch-second conversion to avoid early boundary flips.

Math.round(Date.now() / 1000) can move the effective time ~0.5s into the future. For sale-window checks, use floor semantics.

Suggested fix
-                            now={Math.round(Date.now() / 1000)}
+                            now={Math.floor(Date.now() / 1000)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
now={Math.round(Date.now() / 1000)}
now={Math.floor(Date.now() / 1000)}
🤖 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/withWidgetProviders.js` at 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.

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.

1 participant