fix: bug bashing ChannelList + Channel (#2474, #2441, #2393)#3227
Conversation
useIsChannelMuted called channel.muteStatus() during render, which throws "hasn't been initialized yet" when the channel has not been watched. The ChannelList can briefly hold such a channel (e.g. a message.new for a channel that has not been watched yet), and the throw crashed the whole app. Guard the call behind channel.initialized and fall back to a not-muted status. Closes #2474
…ssage_new handleNotificationMessageNew called getChannel() (channel.watch()) before checking allowNewMessagesFromUnfilteredChannels, so a queryChannel was issued for every notification.message_new even when the result was discarded. When many such events arrive at once (e.g. a bulk campaign) this exhausted the rate limit. Move the guard ahead of the query so the flag actually suppresses the watch(). Closes #2441
Removing a channel from the list did not stop watching it, so the client kept receiving its events and a later message.new / notification.message_new re-added it to the list until a hard refresh. notification.removed_from_channel now calls channel.stopWatching(), and a new member.removed handler removes the channel and stops watching it when the current user is the removed member. Closes #2599
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a ChannelList early return for unfiltered new-message events, guards mute-status reads before channel initialization, and prevents Channel pagination and state reads after disconnect. Adds tests for the new ChannelList and Channel behaviors. ChangesChannelList message-new guard
Channel mute and disconnect guards
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
|
Size Change: +602 B (+0.08%) Total Size: 798 kB 📦 View Changed
ℹ️ View Unchanged
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3227 +/- ##
==========================================
- Coverage 84.20% 84.19% -0.01%
==========================================
Files 485 485
Lines 14867 14872 +5
Branches 4704 4708 +4
==========================================
+ Hits 12519 12522 +3
- Misses 2348 2350 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/components/ChannelListItem/hooks/useIsChannelMuted.ts`:
- Around line 21-24: The local muted state in useIsChannelMuted is only
initialized once and then updated on notification.channel_mutes_updated, so it
can stay stale if the channel mounts before watch()/query() finishes. Add a
resync path in useIsChannelMuted that re-reads getMuteStatus(channel) when the
channel initializes or becomes available, and keep the existing event-driven
update intact. Use the useEffect and getMuteStatus symbols in this hook to
locate the fix, and add a regression test for the mount-before-watch
initialization case.
🪄 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: 38a4028a-0f91-4f43-98cc-e2cc77805bbe
📒 Files selected for processing (4)
src/components/ChannelList/__tests__/ChannelList.test.tsxsrc/components/ChannelList/hooks/useChannelListShape.tssrc/components/ChannelListItem/hooks/__tests__/useIsChannelMuted.test.tsxsrc/components/ChannelListItem/hooks/useIsChannelMuted.ts
channel.lastRead() runs during render (useCreateChannelStateContext) and throws "You can't use a channel after client.disconnect() was called" once the shared client is disconnected - crashing the render, with no error boundary to catch it. Guard the call with channel.disconnected. Also skip loadMore's pagination query while the client is disconnected, mirroring the existing markRead guard. Closes #2393
Browser verification showed the stopWatching-based fix does not make removal durable: channel.stopWatching() does not evict the channel from client.activeChannels, so a reconnect (recoverState re-watches every active channel) or a queued/in-flight message.new re-adds the removed channel to the list - the exact reported symptom. The correct fix belongs in stream-chat core: evict activeChannels[cid] on notification.removed_from_channel / member.removed, mirroring the existing channel.deleted handling. Dropping #2599 from this PR until that core change lands. #2474, #2441 and #2393 are unaffected.
The notification.channel_mutes_updated effect depended on [muted], so it re-subscribed on its own output and could capture a stale channel when the channel prop changed without the muted value changing. Depend on [channel, client] instead and clean up via the unsubscribe handle returned by client.on(), dropping the exhaustive-deps override.
An initialized channel whose client has disconnected still routes channel.muteStatus() through channel.getClient(), which throws "You can't use a channel after client.disconnect() was called". Guard channel.disconnected alongside channel.initialized in getMuteStatus so rendering a ChannelListItem with a disconnected channel no longer crashes the app (same failure class as the #2393 Channel-state guard).
…nnels (#2599) Regression test for #2599: removing the current user from a channel must evict it from client.activeChannels, otherwise recoverState() re-watches it on the next reconnect (recoverStateOnReconnect defaults to true) and the list resurrects it. The eviction itself lives in stream-chat core (GetStream/stream-chat-js#1788). This test is red until that fix ships in a stream-chat release and the dependency here is bumped to it; the bump + green run will accompany closing #2599.
stream-chat 9.49.0 (GetStream/stream-chat-js#1788) evicts a channel from client.activeChannels on notification.removed_from_channel, so a reconnect no longer re-watches a channel the current user was removed from. Bumping the peer and dev requirements makes ChannelList removal durable and turns the #2599 regression test green.
The SDK now requires stream-chat ^9.49.0; leaving the examples at ^9.47.0 made yarn keep a second stream-chat copy, breaking the vite example build with duplicate-Thread type errors (subscribeRepliesUnread missing). Aligning the example deps dedupes to a single 9.49.0.
## [14.6.1](v14.6.0...v14.6.1) (2026-07-03) ### Bug Fixes * bug bashing ChannelList + Channel ([#2474](#2474), [#2441](#2441), [#2393](#2393)) ([#3227](#3227)) ([f790520](f790520)), closes [GetStream/stream-chat-js#1788](GetStream/stream-chat-js#1788) [#2599](#2599) [#2599](#2599) * **i18n:** use .js extensions on dayjs subpath imports for valid Node ESM ([#3231](#3231)) ([7663b18](7663b18)), closes [#3188](#3188) [#3188](#3188) * **MessageList:** don't count thread replies toward channel unread UI state ([#3229](#3229)) ([ca5ed16](ca5ed16)) * **renderText:** recognize uppercase URL schemes ([#3226](#3226)) ([21d57e3](21d57e3)) * **renderText:** recognize uppercase URL schemes in message links ([6178513](6178513)) * return background colors to LinkPreviewCard and TypingIndicator ([a768e30](a768e30))
|
🎉 This PR is included in version 14.6.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
🎯 Goal
Fixes three Tier‑1 issues from the bug‑bash triage. Each is an independent fix kept as its own self‑contained commit (with tests):
message.newfor a not‑yet‑watched channel inserts an uninitialized channel into the list; rendering it calledchannel.muteStatus(), which throws"hasn't been initialized yet"and crashed the whole app.ChannelListissued aqueryChannel(channel.watch()) for everynotification.message_new, even when the result was discarded, exhausting the rate limit during bulk channel activity.channel.lastRead()(called during render inuseCreateChannelStateContext) throws"You can't use a channel after client.disconnect() was called", crashing the render with no error boundary to catch it.🛠 Implementation details
#2474 —
fix(ChannelListItem): guard muteStatus against an uninitialized/disconnected channeluseIsChannelMutedreads the mute status through a helper that callschannel.muteStatus()only when the channel is initialized and not disconnected, falling back to a not‑muted status otherwise.muteStatus()throws on an uninitialized channel (#2474) and also after the client disconnects — viachannel.getClient()— even though the channel stays initialized (the bug: ChannelInner can call methods on a disconnected channel, causing uncaught errors #2393 failure class on the channel‑list path).[channel, client]and cleans up via theunsubscribehandle returned byclient.on(). It previously depended on[muted], so it re‑subscribed on its own output and could capture a stale channel.#2441 —
fix(ChannelList): do not query unfiltered channels on notification.message_newallowNewMessagesFromUnfilteredChannelsguard before thegetChannel()call inhandleNotificationMessageNew, so the flag actually suppresses the per‑eventwatch()instead of querying first and throwing the result away. Behaviour with the flag enabled is unchanged.#2393 —
fix(Channel): guard channel methods against a disconnected clientchannel.lastRead()inuseCreateChannelStateContextwith!channel.disconnectedso a disconnect while the channel is mounted does not crash the render.loadMorenow early‑returns whenchannel.disconnected, mirroring the existingmarkReadguard, to avoid a doomed pagination query.try/catchbackstops on the async paths remain); this removes the uncaught render‑time crash, which is the part with no safety net.#2599 —
fix(deps): require stream-chat ^9.49.0 to evict removed channelsstream-chatto^9.49.0inpeerDependenciesanddevDependencies. NoChannelListruntime change is required: the existinghandleNotificationRemovedFromChannelremoves the channel from the list, andstream-chat@9.49.0now evicts it fromclient.activeChannels, sorecoverState()won't re‑watch it on reconnect. The two compose into durable removal.Tests — added test‑first (each test was confirmed to fail before the fix):
useIsChannelMuteddoes not throw on an uninitialized channel, nor on an initialized‑but‑disconnected channel.notification.message_newdoes not callwatch()whenallowNewMessagesFromUnfilteredChannels={false}.loadMoredoes not query when the client is disconnected.notification.removed_from_channelevicts the channel fromclient.activeChannels(so reconnect cannot re‑watch it). Verified red→green — red onstream-chat@9.47.0, green after the^9.49.0bump.Verification:
yarn test(full suite — 2589 passing, incl. ChannelList, ChannelListItem, Channel, MessageList, Thread),yarn types, and ESLint (--max-warnings 0) all clean. #2474, #2441 and #2393 were additionally confirmed in a real browser against the example app; the #2599 regression test passes against the bumpedstream-chat@9.49.0.🎨 UI Changes
None — these are crash / correctness / rate‑limit fixes in
ChannelListandChannelevent handling, with no visual changes.Summary by CodeRabbit
Bug Fixes
Tests