fix: set preferences data directly on fetch to prevent undefined on re-mount#10071
fix: set preferences data directly on fetch to prevent undefined on re-mount#10071a827681306 wants to merge 1 commit intonovuhq:nextfrom
Conversation
The usePreferences hook was not calling setData() with the fetch response, relying solely on event listeners to update the state. When a component unmounts and re-mounts, the event listeners are re-registered but the cached response may not trigger a new event, leaving preferences as undefined. This aligns the behavior with useNotifications which correctly calls setData() in its fetch callback. Fixes novuhq#10057
👷 Deploy request for dashboard-v2-novu-staging pending review.Visit the deploys page to approve it
|
|
Hey there and thank you for opening this pull request! 👋 We require pull request titles to follow specific formatting rules and it looks like your proposed title needs to be adjusted. Your PR title is: Requirements:
Expected format: Details: |
WalkthroughThe change modifies the usePreferences hook to update local state when a successful response is received. Instead of unconditionally invoking the onSuccess callback, the code now checks for response.data existence and updates the local data state via setData before calling onSuccess. Error handling and other control flow remain unchanged. The modification does not alter any public API signatures. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/react/src/hooks/usePreferences.ts (1)
38-50:props.filterchanges are silently ignored after mount.The empty dependency array means the initial fetch and event-listener registration happen only once. If the consumer passes a dynamically-derived
filter, the hook won't re-fetch when it changes.refetch()must be called manually, which is non-obvious.♻️ Suggested approach: re-run effect when filter changes
useEffect(() => { fetchPreferences(); const listUpdatedCleanup = on('preferences.list.updated', sync); const listPendingCleanup = on('preferences.list.pending', sync); const listResolvedCleanup = on('preferences.list.resolved', sync); return () => { listUpdatedCleanup(); listPendingCleanup(); listResolvedCleanup(); }; - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(props?.filter)]);Serialising the filter avoids object-identity false positives while keeping the dep shallow.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react/src/hooks/usePreferences.ts` around lines 38 - 50, The effect in useEffect currently runs only once so changes to the incoming filter prop are ignored; update the effect for useEffect that calls fetchPreferences and registers event listeners (the block referencing fetchPreferences, on(..., sync) and the cleanup functions listUpdatedCleanup/listPendingCleanup/listResolvedCleanup) to re-run whenever the external filter changes by including a stable serialized filter dependency (e.g. JSON.stringify(filter) or another stable key for props.filter) in the dependency array so fetchPreferences is called and listeners are re-registered/cleaned up whenever filter changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/react/src/hooks/usePreferences.ts`:
- Around line 58-61: The current response handling in usePreferences (the
else-if branch around setData and onSuccess) misses the edge case where response
has neither data nor error, leaving consumers with undefined preferences; update
the response handling in usePreferences to add a defensive fallback: when
response.error is falsy and response.data is falsy, set an explicit error state
(e.g., setError(new Error("No data or error in response"))) and invoke any
onError callback (onError?.(new Error(...))) or otherwise set a sensible default
for preferences, ensuring isLoading/isFetching are still cleared; locate the
branching logic around setData(response.data) and onSuccess?.(response.data) and
add the fallback branch there.
---
Nitpick comments:
In `@packages/react/src/hooks/usePreferences.ts`:
- Around line 38-50: The effect in useEffect currently runs only once so changes
to the incoming filter prop are ignored; update the effect for useEffect that
calls fetchPreferences and registers event listeners (the block referencing
fetchPreferences, on(..., sync) and the cleanup functions
listUpdatedCleanup/listPendingCleanup/listResolvedCleanup) to re-run whenever
the external filter changes by including a stable serialized filter dependency
(e.g. JSON.stringify(filter) or another stable key for props.filter) in the
dependency array so fetchPreferences is called and listeners are
re-registered/cleaned up whenever filter changes.
| } else if (response.data) { | ||
| setData(response.data); | ||
| onSuccess?.(response.data); | ||
| } |
There was a problem hiding this comment.
Core fix is correct — setData now ensures state is populated directly from the API response.
The else if (response.data) guard is also an improvement over the old else { onSuccess?.(response.data!) } which used a non-null assertion that could silently pass undefined to the callback.
One edge case worth noting: if the API somehow returns a response where neither response.error nor response.data is set, neither branch executes, isLoading and isFetching still flip to false, and the consumer sees { preferences: undefined, error: undefined, isLoading: false, isFetching: false } — the same broken state described in issue #10057. Consider adding a fallback or at minimum an explicit error for this case:
🛡️ Defensive fallback for the no-data/no-error path
} else if (response.data) {
setData(response.data);
onSuccess?.(response.data);
+ } else {
+ setError(new NovuError('Preferences response contained no data and no error.'));
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/react/src/hooks/usePreferences.ts` around lines 58 - 61, The current
response handling in usePreferences (the else-if branch around setData and
onSuccess) misses the edge case where response has neither data nor error,
leaving consumers with undefined preferences; update the response handling in
usePreferences to add a defensive fallback: when response.error is falsy and
response.data is falsy, set an explicit error state (e.g., setError(new
Error("No data or error in response"))) and invoke any onError callback
(onError?.(new Error(...))) or otherwise set a sensible default for preferences,
ensuring isLoading/isFetching are still cleared; locate the branching logic
around setData(response.data) and onSuccess?.(response.data) and add the
fallback branch there.
Summary
Fix
usePreferenceshook returningundefinedforpreferenceswhen a component unmounts and re-mounts.Root Cause
The
fetchPreferencesfunction was not callingsetData()with the API response data. It relied solely on event listeners (preferences.list.updated,preferences.list.pending,preferences.list.resolved) to update the state via thesynccallback.On first mount, the event fires and
syncsets the data correctly. However, when the component unmounts and re-mounts, the event listeners are re-registered but the cached response may not trigger a new event, leavingpreferencesasundefinedpermanently (withisLoading: falseand no error).Fix
Added
setData(response.data)in the success branch offetchPreferences, consistent with howuseNotificationshandles its fetch response. This ensures the data is always set directly from the API response, regardless of whether events fire.Comparison with useNotifications
useNotificationscorrectly callssetData(responseData.notifications)in itsfetchNotificationscallback — this PR alignsusePreferenceswith that pattern.Fixes #10057