Skip to content

fix(react): set preferences/schedule data directly on fetch to prevent undefined state on re-mount#11756

Open
BIGSUS24 wants to merge 1 commit into
novuhq:nextfrom
BIGSUS24:fix/issue-10057-preferences-undefined-on-remount
Open

fix(react): set preferences/schedule data directly on fetch to prevent undefined state on re-mount#11756
BIGSUS24 wants to merge 1 commit into
novuhq:nextfrom
BIGSUS24:fix/issue-10057-preferences-undefined-on-remount

Conversation

@BIGSUS24

@BIGSUS24 BIGSUS24 commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Fixes #10057

usePreferences (and the sibling useSchedule) can get permanently stuck in a broken state after a component that uses them unmounts and re-mounts:

State Expected on re-mount Actual on re-mount
isLoading truefalse truefalse
error undefined undefined
preferences valid array undefined (never resolves)

Root Cause

Both hooks populate their data state only via event listeners (preferences.list.pending / preferences.list.resolved, and the equivalent preference.schedule.get.* events). The useEffect starts the async fetch before it registers those listeners:

useEffect(() => {
  fetchPreferences();                              // fetch started first
  const c1 = on('preferences.list.pending', sync); // listeners registered after
  const c2 = on('preferences.list.resolved', sync);
  // ...
}, []);

The @novu/js event emitter is built on mitt, which dispatches synchronously. When the in-memory cache is already warm, preferences.list() emits pending and resolved synchronously, without awaiting any network request:

let data = this.#useCache ? this.cache.getAll(args) : undefined;
this._emitter.emit('preferences.list.pending', { args, data });
if (!data) { /* network fetch */ }
this._emitter.emit('preferences.list.resolved', { args, data });
  • First mount: the session isn't initialized yet, so callWithSession queues the call and resolves it later. That async gap lets the hook register its listeners first, so the events are captured and sync() sets the data. Works.
  • Any subsequent re-mount: the session is initialized and the cache is warm, so list() runs fully synchronously inside fetchPreferences() — the pending/resolved events fire before the listeners are registered and are lost. Since the success branch only calls onSuccess?.(response.data!) and never setData, preferences stays undefined forever.

useNotifications — which the issue reporter confirms works correctly — does not have this bug precisely because its success branch calls setData(responseData.notifications) directly from the response rather than depending on events.

Fix

Align usePreferences and useSchedule with the proven useNotifications pattern by setting state directly from the fetch response:

     if (response.error) {
       setError(response.error);
       onError?.(response.error);
-    } else {
-      onSuccess?.(response.data!);
+    } else if (response.data) {
+      setData(response.data);
+      onSuccess?.(response.data);
     }

This also removes the unsafe non-null assertion (response.data!) in favor of an explicit response.data guard, which additionally prevents clobbering state in the (previously unhandled) edge case where a response carries neither data nor error.

Scope

  • packages/react/src/hooks/usePreferences.ts
  • packages/react/src/hooks/useSchedule.ts

No public API/type changes, no new dependencies. A previous community PR (#10071) applied the same fix to usePreferences and was auto-closed for inactivity rather than rejected on merit; this PR revives that fix and additionally covers useSchedule, which has the identical defect.

Testing

  • Verified via code analysis that mitt dispatches synchronously and that a warm cache in Preferences.list() emits pending/resolved before the hook's listeners are attached on re-mount.
  • Manual repro: mount a component using usePreferences, unmount it, then re-mount — preferences now resolves to the cached array instead of remaining undefined.

Greptile Summary

This PR updates the React preferences hooks to set fetched data directly. The main changes are:

  • usePreferences now stores successful preferences.list() responses in hook state.
  • useSchedule now stores successful preferences.schedule.get() responses in hook state.
  • Both hooks avoid calling onSuccess with a non-null assertion when the response has no data.

Confidence Score: 5/5

Safe to merge with minimal risk.

The changes are narrowly scoped to two matching hook success paths and follow the existing useNotifications pattern.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Ran the use-preferences-remount sequence and captured before and after visuals showing undefined versus cached preferences.
  • Captured the Use Schedule Remount visuals to verify the before and after posters reflect the updated state.
  • Inspected verbose logs to confirm synchronous warm-cache emissions occurred before second-mount listeners registered.
  • Compared onSuccess behavior across hooks, noting the before state had onSuccess invoked once with undefined and the after state showed no onSuccess invocations and no unsafe payload.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
packages/react/src/hooks/usePreferences.ts Sets preferences state directly from successful fetch responses so warm-cache remounts no longer rely only on synchronous events.
packages/react/src/hooks/useSchedule.ts Sets schedule state directly from successful fetch responses so warm-cache remounts no longer rely only on synchronous events.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Component
participant Hook as usePreferences/useSchedule
participant Client as @novu/js preferences API
participant Cache

Component->>Hook: fetch preferences/schedule
Client->>Cache: read cached data
Cache-->>Client: cached response
Client-->>Hook: response.data
Hook->>Hook: setData(response.data)
Hook-->>Component: preferences/schedule available
Hook->>Client: register cache event listeners
Client-->>Hook: future updated/pending/resolved events
Hook->>Hook: sync event data
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Component
participant Hook as usePreferences/useSchedule
participant Client as @novu/js preferences API
participant Cache

Component->>Hook: fetch preferences/schedule
Client->>Cache: read cached data
Cache-->>Client: cached response
Client-->>Hook: response.data
Hook->>Hook: setData(response.data)
Hook-->>Component: preferences/schedule available
Hook->>Client: register cache event listeners
Client-->>Hook: future updated/pending/resolved events
Hook->>Hook: sync event data
Loading

Reviews (1): Last reviewed commit: "fix(react): set preferences/schedule dat..." | Re-trigger Greptile

Copilot AI review requested due to automatic review settings July 1, 2026 18:02
@netlify

netlify Bot commented Jul 1, 2026

Copy link
Copy Markdown

👷 Deploy request for dashboard-v2-novu-staging pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit a1a0115

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a re-mount edge case in packages/react where usePreferences / useSchedule could miss synchronous mitt events emitted from a warm in-memory cache, leaving preferences/schedule permanently undefined after unmount + re-mount. The change aligns these hooks with useNotifications by hydrating state directly from the fetch response instead of relying solely on event listeners.

Changes:

  • Update usePreferences to call setData(response.data) on successful fetch (and remove the unsafe response.data! path).
  • Update useSchedule to call setData(response.data) on successful fetch (and remove the unsafe response.data! path).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/react/src/hooks/usePreferences.ts Sets preferences state directly from preferences.list() response to avoid missing synchronous cache events on re-mount.
packages/react/src/hooks/useSchedule.ts Sets schedule state directly from preferences.schedule.get() response to avoid missing synchronous cache events on re-mount.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 49 to +53
const response = await preferences.schedule.get();
if (response.error) {
setError(response.error);
onError?.(response.error);
} else {
onSuccess?.(response.data!);
} else if (response.data) {
Comment on lines 54 to +58
const response = await preferences.list(props?.filter);
if (response.error) {
setError(response.error);
onError?.(response.error);
} else {
onSuccess?.(response.data!);
} else if (response.data) {
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

T-Rex pricing update — T-Rex was free through June 2026. Effective July 1, 2026, T-Rex adds 2 credits on top of the standard 1-credit review (3 total). T-Rex settings

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.

🐛 Bug Report: preferences value is undefined when the component using usePreferences re-mounts

2 participants