Skip to content

🔥 [🐛] Offline support: channels hydrated from the local DB get hasPrev = false, so older messages never load #3903

Closed

Description

@Mathitarazonad

Issue

With offline support enabled, channels hydrated from the local DB end up with messagePagination.hasPrev = false even when the server has many older messages. Scrolling up in the MessageList never calls the server (loadMore returns early on !hasPrev), so the visible history is stuck at whatever is cached (24 messages at most).

There are two causes:

1. hasPrev is inferred from the local cache (stream-chat, client.hydrateActiveChannels)

ChannelManager.queryChannels first hydrates channels from the DB with:

this.client.hydrateActiveChannels(channelsFromDB, {
  offlineMode: true,
  skipInitialization: [],
}); // no queryChannelsOptions

The pagination is then computed with messageSetPagination against DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE (25). Since the DB returns fewer messages than that, hasPrev is set to false.

That inference ("the page came back shorter than requested, so there is nothing older") is valid for a server response, but not for the local cache: the DB only holds the messages this device has already downloaded, which is often less than a page even when the channel has a long history (e.g. a channel only seen in the channel list has message_limit messages stored).

The online queryChannels response that follows does not correct it. With enableOfflineSupport, usePaginatedChannels passes skipInitialization: undefined, so the server page is merged into the existing message set, and messagePaginationLinear only updates hasPrev when the first message of the returned page is also the first message of the set. With 11–24 cached messages and message_limit: 10, that is never the case, so hasPrev stays false for the whole session.

2. Off-by-one in the offline query (stream-chat-react-native, src/store/apis/queries/selectMessagesForChannels.ts)

WHERE RowNum < 25

ROW_NUMBER() starts at 1, so this returns 24 messages per channel, which is always below the page size of 25 used above. This is already fixed in 10.0.0-rc.2 (RowNum <= 25), but it is still present in 9.10.1 and 9.10.2-beta.2.

Fixing only the off-by-one is not enough: a channel with fewer than 25 cached messages still ends up with hasPrev = false.

Workaround we are using (patch on stream-chat 9.53.0, inside hydrateActiveChannels, right after the pagination is computed):

// local cache may hold fewer messages than the server; let the
// first loadMore (server response) decide whether older messages exist.
if (offlineMode && channelState.messages.length > 0) {
  updatedMessagesSet.pagination.hasPrev = true;
}

ChannelManager is the only caller of hydrateActiveChannels with offlineMode: true, so this only affects DB hydration. The trade-off is that, for a channel that is already fully cached, reaching the top triggers one extra loadMore that returns an empty page, which then sets hasPrev = false correctly.

Steps to reproduce

Steps to reproduce the behavior:

  1. Enable offline support (<Chat enableOfflineSupport>) and render a ChannelList.
  2. Use a channel with more than 50 messages. Open it while online and scroll up until at least one older page is loaded, so more than 24 of its messages get stored in the local DB. Then leave it.
  3. Kill the app and open it again (online).
  4. Open that channel from the channel list and scroll up.
  5. Only the cached messages (24 at most) are shown, and no request for older messages is made. At this point channel.state.messagePagination.hasPrev is false, channel.initialized is true and channel.offlineMode is false.

It is intermittent: it depends on whether the channel state comes from the DB or from the server first, and on how many messages of that channel are cached.

If 10 or fewer messages of the channel are cached (e.g. it was only seen in the channel list), the online queryChannels page covers the whole cached set, hasPrev gets recomputed, and the bug does not show up.

Expected behavior

A channel hydrated from the local DB should not be marked as having no older messages based on the size of the cache. Scrolling up should request older messages from the server, and the server response should decide whether the start of the channel was reached.

Project Related Information

Customization

Click To Expand

We use a copy of ChannelList's data layer (usePaginatedChannels) with the same skipInitialization: enableOfflineSupport ? undefined : ... logic as the SDK, plus the stock Channel and MessageList. The behavior reproduces with the stock components.

<Chat client={client} enableOfflineSupport>
  <ChannelList
    filters={filters}
    sort={sort}
    options={{ limit: 30, message_limit: 10 }}
  />
</Chat>

Offline support

  • I have enabled offline support.
  • The feature I'm having does not occur when offline support is disabled. (stripe out if not applicable)

Environment

Click To Expand

package.json:

"react-native": "0.80.3",
"stream-chat": "9.53.0",
"stream-chat-react-native": "9.8.2"

react-native info output:

react-native info
System:
  OS: macOS 26.7.1
  CPU: (11) arm64 Apple M3 Pro
  Memory: 253.83 MB / 18.00 GB
  Shell:
    version: "5.9"
Binaries:
  Node:
    version: 22.23.4
  Yarn:
    version: 1.22.22
  npm:
    version: 10.9.9
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.16.2
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.2
      - iOS 26.2
      - macOS 26.2
      - tvOS 26.2
      - visionOS 26.2
      - watchOS 26.2
  Android SDK: Not Found
IDEs:
  Android Studio: 2025.2 AI-252.27397.103.2522.14617522
  Xcode:
    version: 26.2/17C52
Languages:
  Java:
    version: 17.0.15
  Ruby:
    version: 3.1.6
npmPackages:
  "@react-native-community/cli":
    installed: 19.1.2
    wanted: 19.1.2
  react:
    installed: 19.1.0
    wanted: 19.1.0
  react-native:
    installed: 0.80.3
    wanted: 0.80.3
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: true
  • Platform that you're experiencing the issue on:
    • iOS
    • Android
    • iOS but have not tested behavior on Android
    • Android but have not tested behavior on iOS
    • Both
  • stream-chat-react-native version you're using that has this issue:
    • 9.8.2 (the same code is present in 9.10.1 and 9.10.2-beta.2)
  • Device/Emulator info:
    • I am using a physical device
    • OS version: Android 15
    • Device/Emulator: Samsung Galaxy A55

Additional context

  • New Architecture is enabled.
  • In 10.0.0-rc the same inference seems to remain: ChannelPaginator hydrates from the DB without a requested limit, and MessageIntervalPaginator.pageReachedChannelStart treats a page shorter than the default page size (25) as the start of the channel. With the off-by-one fixed, channels with 25+ cached messages would work, but channels with fewer cached messages would likely still be marked as having no older messages. We have not run v10, this is from reading the source.
  • Related: if the user opens the channel before the online queryChannels response arrives, Channel calls channel.watch() without messages.limit, so query() assumes DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE (100) while the server returns its own default page, which also yields hasPrev = false.

Screenshots

Click To Expand

Both recordings follow the same steps on the same device: the channel was opened and paginated once while online (so more than 24 of its messages are stored in the local DB), the app was killed and reopened, and then the channel was opened from the channel list and scrolled up.

The loading indicator is a custom FooterComponent; the pagination logic is the stock SDK except for the proposed fix.

Current behavior (stock SDK) With the proposed fix
History stops at the cached messages. hasPrev is false, so scrolling up never requests older messages. Older messages are requested from the server and the history loads page by page up to the first message.
android-without-fixes.mov
android-with-fixes.mp4


Activity

  1. isekovanic commented on Oct 28, 2026

    @isekovanic
    Contributor

    Hi @Mathitarazonad ,

    Thanks a bunch for opening a very detailed PR !

    Yes, I noticed most of these things while working on V10 as well and they are consequently fixed there. There are a few rough edges that I'm resolving as we speak but most of the things should continue working there.

    I'll also backport stuff to V9 too that should cover all of these scenarios directly. One sideeffect of the fixes will indeed be that what the offline DB loads for channels will always respect the provided message_limit (if there) and only default to 25 as it is now. The reason for that is because this is in fact a bugfix for these particular scenarios as loading anything above message_limit can be very wrong.

    For example:

    1. Loading 10 messages (through the ChannelList)
    2. Closing the app
    3. Receiving 15 more messages while we were offline
    4. Loading the app again and loading 10 more messages (again through the ChannelList)
    5. Closing the app and opening it offline

    This will now display messages 25-16 followed by 10-1 (with 15-11 missing), which is incorrect. Only messages we're sure of should be displayed.

    I'll let you know when I have something ready so you can give it a shot (probably in a beta release or so).

  2. Mathitarazonad commented on Oct 28, 2026

    @Mathitarazonad
    Author

    Hi @isekovanic, thank you for the quick and detailed response, we really appreciate it.

    The trade-off you describe makes sense. Showing only messages that are known to be consistent is better than showing a history with gaps, especially when the app is opened offline.

    In the meantime, we'll keep using a small patch until a version with the fix is available. We'd be glad to test the V9 beta and share the results here.

    Thanks again on behalf of our team!

  3. isekovanic commented on Oct 28, 2026

    @isekovanic
    Contributor

    Hi again @Mathitarazonad ,

    The fix for this should now be available in version stream-chat-react-native-9.11.1-beta.2.

    Would it be possible to try it out and see if it works for you ?

    You can also check the PR for it for a full list of fixes it includes !

    I'll close the ticket once you've confirmed stuff works on your end too.

  4. Mathitarazonad commented on Oct 29, 2026

    @Mathitarazonad
    Author

    Hi @isekovanic!

    We tested 9.11.1-beta.2 and can confirm the fix works on our end. Channels hydrated from the offline DB now load older messages when scrolling up. With this, we'll be able to drop our local patch once the fix lands in a stable release.

    Since we've verified it works, we'll go ahead and close this issue. Thank you for the quick turnaround and for addressing everything so thoroughly.

    Thanks again on behalf of our team!

  5. isekovanic commented on Oct 29, 2026

    @isekovanic
    Contributor

    Cool ! Thanks for letting me know ! I'll cut an actual SDK release soon

  6. stream-ci-bot commented on Oct 29, 2026

    @stream-ci-bot
    Contributor

    🎉 This issue has been resolved in version 9.11.1 🎉

    The release is available on:

    Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions