Skip to content

Conversation

@sashankaryal
Copy link
Contributor

@sashankaryal sashankaryal commented Nov 12, 2025

What changes are proposed in this pull request?

In our current setup, if event payload is used as React state, multiple components might get a different read during React's concurrent rendering. This PR introduces the concept of "event state" which uses useSyncExternalStore to make sure no tearing occurs.

How is this patch tested? If it is not, please explain why.

(Details)

Release Notes

Is this a user-facing change that should be mentioned in the release notes?

  • No. You can skip the rest of this section.
  • Yes. Give a description of this change to be included in the release
    notes for FiftyOne users.

(Details in 1-2 sentences. You can just refer to another PR with a description
if this PR is part of a larger change.)

What areas of FiftyOne does this PR affect?

  • App: FiftyOne application changes
  • Build: Build and test infrastructure changes
  • Core: Core fiftyone Python library changes
  • Documentation: FiftyOne documentation changes
  • Other

Summary by CodeRabbit

  • New Features

    • Added synchronized event state hooks to prevent rendering inconsistencies across components
    • Introduced centralized per-event state store for event-driven state management
    • Added a demo component showcasing event-driven state patterns and best practices
    • Strengthened type safety by constraining event payloads to plain-data shapes
  • Documentation

    • Enhanced docs with guidance comparing synchronized reads vs. side-effect handlers and usage examples
  • Tests

    • Added comprehensive test coverage for event state store and related hooks

@sashankaryal sashankaryal requested a review from a team as a code owner November 12, 2025 04:23
@sashankaryal sashankaryal self-assigned this Nov 12, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Walkthrough

This PR adds a synchronized, event-driven state layer and related utilities to the events package. It introduces EventStateStore (per-channel registry, per-event trackers, subscribe/getSnapshot/getSnapshotWithDefault) and exposes a new hook useEventState that uses useSyncExternalStore to provide tear-free reads of the latest event payload. Two factory helpers—createUseSynchronizedEventState and createUseSynchronizedEventStateWithDefaults—are added to produce typed, synchronized hooks (with optional per-event defaults). Types are tightened with a new PlainData alias and EventGroup now restricts payloads to plain/serializable data. A demo component (StateDemo) illustrates usage, and extensive tests for store and hooks are added. Some JSDoc and examples were expanded.

Sequence Diagram

sequenceDiagram
    participant Source as Event Dispatcher (e.g., CounterSource)
    participant Bus as Event Bus
    participant Store as EventStateStore
    participant Hook as useEventState / factory hook
    participant Component as Consumer (CounterDisplay)

    Source->>Bus: dispatch("event:key", payload)
    activate Bus
    Bus->>Store: bus handler invoked with payload
    activate Store
    Store->>Store: update tracker.latestPayload
    Store->>Store: notify subscribers (listener callbacks)
    Store-->>Bus: (no direct response)
    deactivate Store
    deactivate Bus

    Note right of Hook: Subscribed via useSyncExternalStore
    Hook->>Store: subscription.getSnapshot() (render time)
    Hook->>Component: returns latest payload (or default)
    Component->>Component: render with synchronized state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Focus review areas:
    • EventStateStore.ts — tracker lifecycle, reference counting, memoization of subscriptions, cleanup, and channel registry correctness.
    • useEventState.ts — useSyncExternalStore integration, getSnapshot/getSnapshotWithDefault semantics, and default-value behavior.
    • createUseSynchronizedEventState{,WithDefaults}.ts — type inference, defaults precedence, and enforcement of PlainData constraints.
    • types/event.ts — PlainData definition and impacts on existing APIs.
    • Tests — EventStateStore and hook tests for tearing prevention, rapid dispatches, no-payload events, and cleanup correctness.
    • Demo and JSDoc updates — ensure examples accurately reflect intended usage and side-effect guidance.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the problem (tearing during concurrent rendering) and solution (useSyncExternalStore), but testing details are incomplete and release notes are not filled in. Complete the 'How is this patch tested?' section with specific test details, and add a 1-2 sentence release notes description explaining the user-facing changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main changes: introducing event state support and enforcing serializable event payloads.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/events-uses

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

Copy link
Contributor

@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

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 110fcfe and 736c1f2.

📒 Files selected for processing (13)
  • app/packages/events/src/demo-state.tsx (1 hunks)
  • app/packages/events/src/hooks/createUseEventHandler.ts (2 hunks)
  • app/packages/events/src/hooks/createUseSynchronizedEventState.test.tsx (1 hunks)
  • app/packages/events/src/hooks/createUseSynchronizedEventState.ts (1 hunks)
  • app/packages/events/src/hooks/index.ts (1 hunks)
  • app/packages/events/src/hooks/useEventBus.ts (1 hunks)
  • app/packages/events/src/hooks/useEventState.test.tsx (1 hunks)
  • app/packages/events/src/hooks/useEventState.ts (1 hunks)
  • app/packages/events/src/index.ts (1 hunks)
  • app/packages/events/src/state/EventStateStore.test.ts (1 hunks)
  • app/packages/events/src/state/EventStateStore.ts (1 hunks)
  • app/packages/events/src/state/index.ts (1 hunks)
  • app/packages/events/src/types/event.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

Review the Typescript and React code for conformity with best practices in React, Recoil, Graphql, and Typescript. Highlight any deviations.

Files:

  • app/packages/events/src/state/index.ts
  • app/packages/events/src/hooks/useEventBus.ts
  • app/packages/events/src/hooks/useEventState.ts
  • app/packages/events/src/types/event.ts
  • app/packages/events/src/hooks/createUseSynchronizedEventState.ts
  • app/packages/events/src/hooks/createUseEventHandler.ts
  • app/packages/events/src/hooks/useEventState.test.tsx
  • app/packages/events/src/hooks/createUseSynchronizedEventState.test.tsx
  • app/packages/events/src/index.ts
  • app/packages/events/src/state/EventStateStore.ts
  • app/packages/events/src/demo-state.tsx
  • app/packages/events/src/hooks/index.ts
  • app/packages/events/src/state/EventStateStore.test.ts
🧠 Learnings (3)
📚 Learning: 2024-11-06T20:52:30.520Z
Learnt from: benjaminpkane
Repo: voxel51/fiftyone PR: 5051
File: app/packages/utilities/src/index.ts:12-12
Timestamp: 2024-11-06T20:52:30.520Z
Learning: In `app/packages/utilities/src/index.ts`, when an export statement like `export * from "./Resource";` is moved within the file, it is not a duplication even if it appears in both the removed and added lines of a diff.

Applied to files:

  • app/packages/events/src/state/index.ts
  • app/packages/events/src/index.ts
  • app/packages/events/src/hooks/index.ts
📚 Learning: 2025-10-02T21:53:53.778Z
Learnt from: tom-vx51
Repo: voxel51/fiftyone PR: 6372
File: app/packages/core/src/client/annotationClient.ts:32-46
Timestamp: 2025-10-02T21:53:53.778Z
Learning: In `app/packages/core/src/client/annotationClient.ts`, the `delta` field in `PatchSampleRequest` intentionally does not accept simple scalar values (strings, numbers, booleans). The `AttributeField` type is correctly defined as `Record<string, unknown>` to enforce that all delta entries must be structured objects, not primitives.

Applied to files:

  • app/packages/events/src/types/event.ts
📚 Learning: 2025-04-23T15:22:03.452Z
Learnt from: benjaminpkane
Repo: voxel51/fiftyone PR: 5732
File: app/packages/core/src/components/Filters/use-query-performance-icon.tsx:26-38
Timestamp: 2025-04-23T15:22:03.452Z
Learning: React hooks (including useRecoilValue and other state management hooks) must be called unconditionally at the top level of a component or custom hook. They cannot be placed inside conditional statements, loops, or nested functions, as this violates React's Rules of Hooks and leads to unpredictable behavior.

Applied to files:

  • app/packages/events/src/demo-state.tsx
🧬 Code graph analysis (7)
app/packages/events/src/hooks/useEventState.ts (2)
app/packages/events/src/types/event.ts (1)
  • EventGroup (62-62)
app/packages/events/src/state/EventStateStore.ts (1)
  • getEventStateStore (153-160)
app/packages/events/src/hooks/createUseSynchronizedEventState.ts (2)
app/packages/events/src/types/event.ts (1)
  • EventGroup (62-62)
app/packages/events/src/hooks/useEventState.ts (1)
  • useEventState (59-71)
app/packages/events/src/hooks/useEventState.test.tsx (3)
app/packages/events/src/state/EventStateStore.ts (1)
  • __test__ (165-167)
app/packages/events/src/hooks/useEventState.ts (1)
  • useEventState (59-71)
app/packages/events/src/hooks/useEventBus.ts (1)
  • useEventBus (17-28)
app/packages/events/src/hooks/createUseSynchronizedEventState.test.tsx (3)
app/packages/events/src/state/EventStateStore.ts (1)
  • __test__ (165-167)
app/packages/events/src/hooks/createUseSynchronizedEventState.ts (2)
  • createUseSynchronizedEventState (55-61)
  • createUseSynchronizedEventStateWithDefaults (83-97)
app/packages/events/src/hooks/useEventBus.ts (1)
  • useEventBus (17-28)
app/packages/events/src/state/EventStateStore.ts (1)
app/packages/events/src/types/event.ts (1)
  • EventGroup (62-62)
app/packages/events/src/demo-state.tsx (4)
app/packages/events/src/hooks/createUseSynchronizedEventState.ts (1)
  • createUseSynchronizedEventState (55-61)
app/packages/events/src/hooks/createUseEventHandler.ts (1)
  • createUseEventHandler (44-56)
app/packages/events/src/hooks/useEventBus.ts (1)
  • useEventBus (17-28)
app/packages/events/src/hooks/useEventState.ts (1)
  • useEventState (59-71)
app/packages/events/src/state/EventStateStore.test.ts (1)
app/packages/events/src/state/EventStateStore.ts (2)
  • __test__ (165-167)
  • getEventStateStore (153-160)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test / test-python (ubuntu-latest-m, 3.12)
  • GitHub Check: test / test-python (ubuntu-latest-m, 3.11)
  • GitHub Check: test / test-python (ubuntu-latest-m, 3.9)
  • GitHub Check: test / test-python (ubuntu-latest-m, 3.10)
  • GitHub Check: lint / eslint
  • GitHub Check: test / test-app
  • GitHub Check: e2e / test-e2e
  • GitHub Check: build / build
🔇 Additional comments (10)
app/packages/events/src/hooks/useEventBus.ts (1)

9-12: Documentation enhancements are clear and helpful.

The guidance on when to use each hook type (dispatch/subscribe vs side effects vs render state) provides valuable context for developers choosing between the event system APIs.

app/packages/events/src/hooks/createUseEventHandler.ts (1)

9-20: Documentation effectively contrasts handler vs state patterns.

The explanation of when to use side-effect handlers versus synchronized state reading is clear and helps developers make the right architectural choice.

Also applies to: 33-38

app/packages/events/src/types/event.ts (2)

28-35: PlainData type provides compile-time guidance but can't catch all non-serializable types.

The recursive type definition correctly restricts to primitives, arrays, and plain objects. However, TypeScript's structural typing means instances of classes (with only plain-data fields), Date objects, Map, Set, and other non-serializable types could match the structure at compile time. The documentation appropriately emphasizes the serialization requirement, but developers must follow this convention—there's no runtime enforcement.

This is a reasonable trade-off given TypeScript's type system limitations, but be aware that as any or structurally-matching non-plain objects can bypass the constraint.


62-62: EventGroup constraint properly tightens payload typing.

Restricting event payloads to PlainData | undefined | null aligns with the serialization requirements for the state store. The constraint will catch most misuse at compile time.

app/packages/events/src/index.ts (1)

3-3: Public API expansion is appropriate.

Re-exporting the state module makes the EventStateStore and related utilities accessible to consumers.

app/packages/events/src/state/index.ts (1)

1-1: State module index follows standard patterns.

app/packages/events/src/hooks/index.ts (1)

2-4: Public hook API properly expanded.

The new synchronized state hooks are now accessible alongside existing event handler hooks.

app/packages/events/src/hooks/createUseSynchronizedEventState.test.tsx (2)

16-20: Proper test isolation with registry cleanup.

Clearing the __test__.registry in beforeEach prevents test cross-contamination and ensures each test starts with a clean state.


66-91: Tearing prevention test correctly validates synchronized reads.

Testing that three separate components reading the same event see identical values demonstrates the core benefit of useSyncExternalStore—preventing visual inconsistencies during concurrent rendering.

app/packages/events/src/hooks/useEventState.ts (1)

59-71: No changes needed—the review comment is incorrect.

The createSubscription method in EventStateStore.ts already implements internal memoization. It caches subscription objects in a Map keyed by event, returning the cached instance on subsequent calls (line 110: const existing = this.subscriptions.get(event); if (existing) return existing;).

When useEventState calls store.createSubscription(event) on every render, it's performing a cheap Map.get() lookup, not allocating new subscription objects. The EventStateStore instance itself is also memoized per channel via the registry.

The proposed useMemo wrapper is redundant. Tests confirm correctness, including tearing prevention across multiple components using the same event.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@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

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 736c1f2 and 5218a0f.

📒 Files selected for processing (1)
  • app/packages/events/src/hooks/createUseSynchronizedEventState.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

Review the Typescript and React code for conformity with best practices in React, Recoil, Graphql, and Typescript. Highlight any deviations.

Files:

  • app/packages/events/src/hooks/createUseSynchronizedEventState.ts
🧬 Code graph analysis (1)
app/packages/events/src/hooks/createUseSynchronizedEventState.ts (2)
app/packages/events/src/types/event.ts (1)
  • EventGroup (62-62)
app/packages/events/src/hooks/useEventState.ts (1)
  • useEventState (59-71)
🔇 Additional comments (4)
app/packages/events/src/hooks/createUseSynchronizedEventState.ts (4)

1-2: Imports look good.

Clean dependencies, properly scoped.


4-54: Excellent documentation.

The comparison with createUseEventHandler is clear, and the examples correctly demonstrate null-safe access patterns.


55-61: Implementation is correct.

Simple delegation with proper type preservation.


83-97: Implementation logic is sound.

The past review concern has been addressed—return type correctly reflects the possibility of undefined, and the unsafe cast has been removed. The fallback logic is correct.

Comment on lines +76 to +80
* function CounterDisplay() {
* // Always returns a value (never undefined), synchronized across all components
* const latestEvent = useCounterState("counter:updated");
* return <div>Count: {latestEvent.count}</div>;
* }
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix misleading JSDoc claim and unsafe example.

Line 77 claims "Always returns a value (never undefined)" but the return type is T[K] | undefined. This claim only holds when a default is provided for that specific event—users can call useCounterState with events that have no default, which returns undefined. The example's direct property access (latestEvent.count) would crash at runtime if no default exists.

Update the JSDoc to clarify the conditional guarantee:

  * function CounterDisplay() {
- *   // Always returns a value (never undefined), synchronized across all components
+ *   // Returns a defined value when a default is provided for the event
  *   const latestEvent = useCounterState("counter:updated");
- *   return <div>Count: {latestEvent.count}</div>;
+ *   // Safe because we provided a default for "counter:updated"
+ *   return <div>Count: {latestEvent.count}</div>;
  * }
🤖 Prompt for AI Agents
In app/packages/events/src/hooks/createUseSynchronizedEventState.ts around lines
76 to 80, the JSDoc incorrectly states "Always returns a value (never
undefined)" while the function signature returns T[K] | undefined, and the
example accesses latestEvent.count unsafely; update the JSDoc to state that the
hook returns the event value or undefined unless a default is provided for that
event, and update the example to either show providing a default or safely
handle undefined (e.g., optional chaining, nullish coalescing, or an explicit
guard) so property access cannot crash at runtime.

@sashankaryal sashankaryal marked this pull request as draft November 12, 2025 05:16
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