Skip to content

feat: migrate FlatList to FlashList for rooms lists#6995

Open
deepak0x wants to merge 4 commits intoRocketChat:developfrom
deepak0x:feat/flashlist-android-lists
Open

feat: migrate FlatList to FlashList for rooms lists#6995
deepak0x wants to merge 4 commits intoRocketChat:developfrom
deepak0x:feat/flashlist-android-lists

Conversation

@deepak0x
Copy link
Contributor

@deepak0x deepak0x commented Feb 19, 2026

Migrate the rooms list (RoomsListView) from FlatList to @shopify/flash-list (FlashList) for better scrolling performance on Android. FlashList recycles native views instead of creating/destroying them on scroll, reducing dropped frames and jank on long room lists.

  • RoomsListView: Replaced FlatList with FlashList; removed getItemLayout, removeClippedSubviews, initialNumToRender, and windowSize (handled by FlashList’s recycler). Wrapped renderItem and onPressItem in useCallback so FlashList doesn’t re-render items unnecessarily. Added drawDistance={300} to pre-render cells beyond the viewport.
  • RoomView message list: Left on Animated.FlatList. FlashList v2 does not support the inverted prop or onScrollToIndexFailed, which the chat list relies on for scroll direction, jump-to-message, and maintaining position when new messages arrive. Reverting to FlatList there avoids scroll/order/FAB regressions.
  • Tests: Added accessibility regression tests for the RoomView List (autocomplete visibility and accessibilityElementsHidden / importantForAccessibility).
  • Docs: Added docs/FLASHLIST_MIGRATION.md with steps to compare CPU/graphics performance on Android (e.g. adb dumpsys gfxinfo).

Issue(s)

None — feature/improvement.

How to test or reproduce

  1. Rooms list (FlashList) — Open the sidebar/rooms list, scroll through many rooms (e.g. 50+). Confirm scrolling is smooth, no blank rows or flicker, pull-to-refresh and taps work. Test on an Android device if possible.
  2. Room messages (unchanged) — Open a channel, scroll messages, receive new messages, use “scroll to bottom” FAB, jump to an old message. Confirm order (newest at bottom), position stability when new messages arrive, and that jump-to-message and FAB behave as before.
  3. Accessibility — Run List.test.tsx (RoomView List). With a screen reader, open a room and open/close the autocomplete; list should be hidden from the tree when autocomplete is visible.

Screenshots

No UI changes; behavior and layout match previous implementation. Optional: before/after scroll FPS or gfxinfo snippets if you captured them.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Summary by CodeRabbit

  • Tests

    • Added accessibility tests for the room list that verify visibility and accessibility props when autocomplete is shown or hidden.
  • New Features

    • Replaced previous list implementation with FlashList in the rooms list view for improved rendering and memoization.
  • Documentation

    • Added a FlashList migration guide with performance testing steps and evaluation metrics.

- Add @shopify/flash-list dependency
- Replace FlatList with FlashList in RoomsListView
- Replace Animated.FlatList with AnimatedFlashList in RoomView message list
- Update list ref/types to FlashListRef and FlashListProps
- Preserve a11y props and add List.test.tsx for accessibility parity
- Adjust scroll/viewability usage for FlashList API
@deepak0x deepak0x marked this pull request as draft February 19, 2026 18:35
- Replace FlatList with FlashList in RoomsListView for better view recycling on Android
- Wrap renderItem and onPressItem in useCallback to prevent unnecessary re-renders
- Remove getItemLayout, removeClippedSubviews, initialNumToRender, windowSize (unneeded with FlashList recycler)
- Add drawDistance={300} to pre-render cells beyond the viewport
- Revert RoomView message list to Animated.FlatList; FlashList v2 lacks inverted prop support required by the chat list
- Add accessibility regression tests for RoomView List component
- Add FLASHLIST_MIGRATION.md with Android performance measurement guide
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Walkthrough

Replaces RoomsListView's FlatList with FlashList, adds accessibility tests for RoomView List, adds the @shopify/flash-list dependency, and introduces migration documentation detailing Android performance capture and comparison steps.

Changes

Cohort / File(s) Summary
FlashList Migration
app/views/RoomsListView/index.tsx, package.json
Replaced FlatList with FlashList; added @shopify/flash-list dependency; introduced useCallback memoization for item handlers; added getItemType and drawDistance; removed or adjusted FlatList-specific layout props.
Accessibility Tests
app/views/RoomView/List/components/List.test.tsx
New tests rendering the List inside RoomContext.Provider with different isAutocompleteVisible values; asserts accessibilityElementsHidden and importantForAccessibility on testID: 'room-view-messages'.
Migration Documentation
docs/FLASHLIST_MIGRATION.md
New doc describing migration from FlatList to FlashList for RoomsListView and outlining Android CPU/GFX performance capture and before/after comparison steps and metrics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I swapped the list and gave it a dash,
swift FlashList feet and a memozing splash.
Tests peep and docs map the trail,
I nibble regressions and leave a soft tail. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title 'feat: migrate FlatList to FlashList for rooms lists' directly and accurately describes the main change in the PR, which is migrating from FlatList to FlashList in the RoomsListView component.

✏️ 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.

❤️ Share

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

@deepak0x deepak0x marked this pull request as ready for review February 24, 2026 21:55
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

🧹 Nitpick comments (3)
docs/FLASHLIST_MIGRATION.md (1)

5-5: Clarify the FlashList v2 inverted prop status — it's deprecated, not absent.

The v2 migration guide states the inverted prop is deprecated; the alternative is maintainVisibleContentPosition. Saying it is "not supported" may lead future contributors to believe a RoomView migration is impossible, when in fact there is a documented migration path. Consider rewording to: "…because the migration of inverted (deprecated in v2) and onScrollToIndexFailed to the v2 equivalents has not yet been done."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/FLASHLIST_MIGRATION.md` at line 5, Update the sentence describing
RoomView's use of Animated.FlatList to clarify that FlashList v2 deprecated the
inverted prop (not removed) and offers maintainVisibleContentPosition as the
alternative; reword the line referencing RoomView/Animated.FlatList to say the
migration of inverted (deprecated in v2) and onScrollToIndexFailed to their v2
equivalents (e.g., maintainVisibleContentPosition and appropriate
failure-handling patterns) has not yet been done, mentioning RoomView,
Animated.FlatList, inverted, maintainVisibleContentPosition, and
onScrollToIndexFailed so future contributors see the documented migration path.
app/views/RoomsListView/index.tsx (1)

136-136: extraData mirrors data — it is effectively a no-op here.

extraData is meant to carry state that affects rendering but is not in data (e.g., subscribedRoom, theme). Passing the same array reference as data provides no additional signal to FlashList. Since renderItem is already memoized with subscribedRoom in its dependency array, highlight-on-focus updates are handled correctly through the renderItem reference change. The extraData line can either be removed or updated to carry the independent state that drives re-renders.

♻️ Proposed simplification
-    extraData={searchEnabled ? searchResults : subscriptions}
+    extraData={[subscribedRoom, displayMode, showAvatar]}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/RoomsListView/index.tsx` at line 136, extraData is redundant
because it receives the same array as data (extraData={searchEnabled ?
searchResults : subscriptions}), so remove or change it to a true independent
signal; update the RoomsListView FlashList props by either deleting the
extraData prop entirely or set extraData to an independent piece of state (e.g.,
subscribedRoom or theme) that actually affects rendering, and ensure renderItem
(which already depends on subscribedRoom) remains memoized so highlights still
update correctly.
app/views/RoomView/List/components/List.test.tsx (1)

25-26: Replace UNSAFE_getByProps with the stable getByTestId query.

Since the target already has testID='room-view-messages', getByTestId is the idiomatic, non-fragile approach. The UNSAFE_ prefix signals the query relies on internal component structure and may break across library upgrades.

♻️ Proposed refactor
 it('hides message list from accessibility tree while autocomplete is visible', () => {
-  const { UNSAFE_getByProps } = renderList(true);
-  const list = UNSAFE_getByProps({ testID: 'room-view-messages' });
+  const { getByTestId } = renderList(true);
+  const list = getByTestId('room-view-messages');

   expect(list.props.accessibilityElementsHidden).toBe(true);
   expect(list.props.importantForAccessibility).toBe('no-hide-descendants');
 });

 it('keeps message list visible to accessibility tree while autocomplete is hidden', () => {
-  const { UNSAFE_getByProps } = renderList(false);
-  const list = UNSAFE_getByProps({ testID: 'room-view-messages' });
+  const { getByTestId } = renderList(false);
+  const list = getByTestId('room-view-messages');

   expect(list.props.accessibilityElementsHidden).toBe(false);
   expect(list.props.importantForAccessibility).toBe('yes');
 });

Also applies to: 33-34

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/RoomView/List/components/List.test.tsx` around lines 25 - 26,
Replace the fragile UNSAFE_getByProps usage with the stable getByTestId query:
in List.test.tsx where you call renderList(true) and destructure
UNSAFE_getByProps to locate the element with testID 'room-view-messages', switch
to using getByTestId('room-view-messages') (and similarly replace the other
occurrence around the second case on lines 33-34) so the test queries by the
public testID prop instead of internal props; update any destructuring from
renderList to include getByTestId and remove UNSAFE_getByProps references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/views/RoomsListView/index.tsx`:
- Around line 79-110: The FlashList render contains two distinct view types
(SectionHeader when item.separator is true and RoomItem otherwise) but no
getItemType is provided; add a getItemType prop to the FlashList that returns a
stable type string/value (e.g., item.separator ? 'section' : 'room') so
FlashList can maintain separate recycling pools and avoid recycled separators
being reused for RoomItem and vice versa; update both places where renderItem is
defined (the renderItem callback that returns SectionHeader/RoomItem and the
other similar renderer) and pass the new getItemType function into the FlashList
component instance.

---

Nitpick comments:
In `@app/views/RoomsListView/index.tsx`:
- Line 136: extraData is redundant because it receives the same array as data
(extraData={searchEnabled ? searchResults : subscriptions}), so remove or change
it to a true independent signal; update the RoomsListView FlashList props by
either deleting the extraData prop entirely or set extraData to an independent
piece of state (e.g., subscribedRoom or theme) that actually affects rendering,
and ensure renderItem (which already depends on subscribedRoom) remains memoized
so highlights still update correctly.

In `@app/views/RoomView/List/components/List.test.tsx`:
- Around line 25-26: Replace the fragile UNSAFE_getByProps usage with the stable
getByTestId query: in List.test.tsx where you call renderList(true) and
destructure UNSAFE_getByProps to locate the element with testID
'room-view-messages', switch to using getByTestId('room-view-messages') (and
similarly replace the other occurrence around the second case on lines 33-34) so
the test queries by the public testID prop instead of internal props; update any
destructuring from renderList to include getByTestId and remove
UNSAFE_getByProps references.

In `@docs/FLASHLIST_MIGRATION.md`:
- Line 5: Update the sentence describing RoomView's use of Animated.FlatList to
clarify that FlashList v2 deprecated the inverted prop (not removed) and offers
maintainVisibleContentPosition as the alternative; reword the line referencing
RoomView/Animated.FlatList to say the migration of inverted (deprecated in v2)
and onScrollToIndexFailed to their v2 equivalents (e.g.,
maintainVisibleContentPosition and appropriate failure-handling patterns) has
not yet been done, mentioning RoomView, Animated.FlatList, inverted,
maintainVisibleContentPosition, and onScrollToIndexFailed so future contributors
see the documented migration path.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae5e663 and a3fd229.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • app/views/RoomView/List/components/List.test.tsx
  • app/views/RoomsListView/index.tsx
  • docs/FLASHLIST_MIGRATION.md
  • package.json
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-05T13:55:00.974Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6930
File: package.json:101-101
Timestamp: 2026-02-05T13:55:00.974Z
Learning: In this repository, the dependency on react-native-image-crop-picker should reference the RocketChat fork (RocketChat/react-native-image-crop-picker) with explicit commit pins, not the upstream ivpusic/react-native-image-crop-picker. Update package.json dependencies (and any lockfile) to point to the fork URL and a specific commit, ensuring edge-to-edge Android fixes are included. This pattern should apply to all package.json files in the repo that declare this dependency.

Applied to files:

  • package.json
🧬 Code graph analysis (1)
app/views/RoomView/List/components/List.test.tsx (2)
app/definitions/IMessage.ts (1)
  • TAnyMessageModel (185-185)
app/views/RoomView/context.ts (1)
  • RoomContext (23-23)
🔇 Additional comments (1)
package.json (1)

54-54: No action needed — new architecture is already enabled.

The project has newArchEnabled=true in android/gradle.properties and RN 0.79.4 defaults to new arch on iOS. FlashList v2.2.2 is fully compatible with this configuration.

@deepak0x
Copy link
Contributor Author

hey @diegolmello
Can you please check and let me know your view ;)

Thanks

Section headers and room items use distinct view types; getItemType
ensures FlashList recycles them in separate pools to avoid layout
glitches and incorrect renders when scrolling.
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

🧹 Nitpick comments (1)
app/views/RoomsListView/index.tsx (1)

82-82: getItemType is a stable pure function — hoist it to module level instead of useCallback.

Since it only references ITEM_TYPE_SECTION and ITEM_TYPE_ROOM (both module-level constants), wrapping it in useCallback with [] is unnecessary overhead. A module-level function produces the same stable identity with less ceremony.

♻️ Proposed refactor
-const ITEM_TYPE_SECTION = 'section';
-const ITEM_TYPE_ROOM = 'room';
+const ITEM_TYPE_SECTION = 'section';
+const ITEM_TYPE_ROOM = 'room';
+
+const getItemType = (item: IRoomItem) => (item.separator ? ITEM_TYPE_SECTION : ITEM_TYPE_ROOM);

Then remove the in-component declaration:

-	const getItemType = useCallback((item: IRoomItem) => (item.separator ? ITEM_TYPE_SECTION : ITEM_TYPE_ROOM), []);
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/RoomsListView/index.tsx` at line 82, The getItemType function is a
pure, stable function that doesn't use component state or props, so hoist it out
of the component to module scope: create a top-level function named getItemType
that returns ITEM_TYPE_SECTION when item.separator is truthy and ITEM_TYPE_ROOM
otherwise (using the existing ITEM_TYPE_SECTION and ITEM_TYPE_ROOM constants),
then remove the useCallback declaration and replace usages in the component to
call the module-level getItemType directly; ensure there are no remaining
imports or references to useCallback for this function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/views/RoomsListView/index.tsx`:
- Around line 138-152: The FlashList's extraData currently mirrors data and
omits subscribedRoom, so changes to subscribedRoom won't reliably re-render
RoomItem's isFocused; update the FlashList extraData to include subscribedRoom
(e.g., pass a tuple/object containing the current list and subscribedRoom) so
FlashList knows to re-render items when subscribedRoom changes, leaving
getItemType, renderItem, keyExtractor and other props intact.

---

Nitpick comments:
In `@app/views/RoomsListView/index.tsx`:
- Line 82: The getItemType function is a pure, stable function that doesn't use
component state or props, so hoist it out of the component to module scope:
create a top-level function named getItemType that returns ITEM_TYPE_SECTION
when item.separator is truthy and ITEM_TYPE_ROOM otherwise (using the existing
ITEM_TYPE_SECTION and ITEM_TYPE_ROOM constants), then remove the useCallback
declaration and replace usages in the component to call the module-level
getItemType directly; ensure there are no remaining imports or references to
useCallback for this function.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3fd229 and 2968649.

📒 Files selected for processing (1)
  • app/views/RoomsListView/index.tsx
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (1)
app/views/RoomsListView/index.tsx (6)
app/containers/RoomItem/interfaces.ts (1)
  • IRoomItem (170-173)
app/sagas/rooms.js (1)
  • subscribedRoom (62-62)
app/lib/methods/helpers/log/index.ts (2)
  • logEvent (35-42)
  • events (7-7)
app/lib/methods/helpers/goRoom.ts (1)
  • goRoom (85-130)
app/sagas/login.js (1)
  • isMasterDetail (54-54)
app/lib/methods/helpers/helpers.ts (3)
  • getUidDirectMessage (17-41)
  • getRoomTitle (43-60)
  • getRoomAvatar (10-15)
🔇 Additional comments (1)
app/views/RoomsListView/index.tsx (1)

139-152: FlashList v2 configuration verified — new architecture is unconditionally enabled.

The project has newArchEnabled=true set in Android's gradle.properties, and both product flavors (official and experimental) inherit this setting without overrides. iOS is configured with React Native 0.79.4 on platform 15.1, which enables new architecture by default. FlashList v2.2.2 will run without issues on supported devices.

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.

♻️ Duplicate comments (1)
app/views/RoomsListView/index.tsx (1)

141-141: ⚠️ Potential issue | 🟠 Major

Stabilize extraData to avoid avoidable FlashList invalidations.

Line 141 creates a new array literal on every render. extraData is used as a re-render marker in FlashList, so this triggers unnecessary row updates. Since data already covers subscriptions/searchResults, only subscribedRoom (used in renderItem to determine isFocused) needs to be in extraData.

♻️ Proposed fix
-			extraData={[searchEnabled ? searchResults : subscriptions, subscribedRoom]}
+			extraData={subscribedRoom}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/RoomsListView/index.tsx` at line 141, The FlashList's extraData is
currently recreated each render as an array literal (extraData={[searchEnabled ?
searchResults : subscriptions, subscribedRoom]}), causing unnecessary
invalidations; change it to only pass the stable value used by renderItem for
focus checks (extraData={subscribedRoom}) or, if you truly need multiple
markers, create a stable memoized value (useMemo) and pass that instead—update
the extraData prop near the FlashList usage in RoomsListView (where
searchEnabled, searchResults, subscriptions, and subscribedRoom are referenced)
and ensure renderItem's isFocused logic still reads subscribedRoom.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/views/RoomsListView/index.tsx`:
- Line 141: The FlashList's extraData is currently recreated each render as an
array literal (extraData={[searchEnabled ? searchResults : subscriptions,
subscribedRoom]}), causing unnecessary invalidations; change it to only pass the
stable value used by renderItem for focus checks (extraData={subscribedRoom})
or, if you truly need multiple markers, create a stable memoized value (useMemo)
and pass that instead—update the extraData prop near the FlashList usage in
RoomsListView (where searchEnabled, searchResults, subscriptions, and
subscribedRoom are referenced) and ensure renderItem's isFocused logic still
reads subscribedRoom.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2968649 and 9a1d3a5.

📒 Files selected for processing (1)
  • app/views/RoomsListView/index.tsx
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (1)
app/views/RoomsListView/index.tsx (3)
app/containers/RoomItem/interfaces.ts (1)
  • IRoomItem (170-173)
app/sagas/rooms.js (1)
  • subscribedRoom (62-62)
app/lib/methods/helpers/goRoom.ts (1)
  • goRoom (85-130)

@deepak0x deepak0x changed the title feat: migrate FlatList to FlashList for rooms and message lists feat: migrate FlatList to FlashList for rooms lists Feb 27, 2026
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.

1 participant