Skip to content

Conversation

@Rohit3523
Copy link
Contributor

@Rohit3523 Rohit3523 commented Jan 21, 2026

Proposed changes

Search functionality in the room/group members list fails to return results for users who haven’t been loaded into the local state. Only users loaded initially or after scrolling to the bottom are searchable.

Issue(s)

https://rocketchat.atlassian.net/browse/SUP-973

How to test or reproduce

  1. Go to the member search in any room with a large number of members.
  2. Type the name of a user who is a member of the room but not currently visible in the list.
  3. The app fails to display the member in the search results.

Screenshots

iOS E2E
Screenshot 2026-01-23 at 11 40 54 PM

Android E2E
Screenshot 2026-01-23 at 11 23 05 PM

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

Further comments

Summary by CodeRabbit

  • Improvements

    • Debounced member search (500ms) for smoother typing and fewer requests.
    • Member list now reflects server-side filtered results and uses a single source of truth.
    • Pagination deduplicates results and preserves correct end/page behavior.
    • Stale search responses are ignored to prevent flicker or incorrect lists.
    • Search input wired into the list header for consistent behavior.
  • Tests

    • Added an automated end-to-end test covering the "Search Member" flow.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Walkthrough

Adds a 500ms debounced search to RoomMembersView that triggers server-side filtered fetches, tracks request IDs to ignore stale responses, resets pagination on new filters, deduplicates paginated results into component state, and removes client-side sanitizeLikeString usage.

Changes

Cohort / File(s) Summary
Room members view
app/views/RoomMembersView/index.tsx
Added useRef and useDebounce imports; removed sanitizeLikeString. Introduced latestSearchRequest ref and debounceFilterChange (500ms) to trim input, reset pagination/state, and trigger filtered fetches.
Fetch & pagination logic
app/views/RoomMembersView/index.tsx
Added fetchMembersWithNewFilter(searchFilter) with requestId validation; updated fetchMembers to guard by requestId and deduplicate/merge paginated results into state.members, preserving end/page logic.
UI wiring & error handling
app/views/RoomMembersView/index.tsx
Replaced client-side inline filtering; SearchBox now uses debounced handler; list now renders from state.members; on fetch error isLoading updates only when requestId matches latestSearchRequest.current.
Test addition
.maestro/tests/room/search-member.yaml
New Maestro test for "Search Member" flow: creates user, logs in, opens room members, types search, and asserts member visibility.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant SB as SearchBox
  participant RMV as RoomMembersView
  participant API as ServerAPI

  User->>SB: type query
  SB->>RMV: onChangeText (debounced 500ms)
  rect rgba(200,200,255,0.5)
    RMV->>RMV: latestSearchRequest++\nreset pagination/state
  end
  RMV->>API: fetchMembersWithNewFilter(searchFilter, requestId)
  API-->>RMV: response (members, page, requestId)
  alt response.requestId == latestSearchRequest
    RMV->>RMV: replace members / set end & page
  else stale response
    RMV-->>RMV: ignore response
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • diegolmello

Poem

🐇 I nibble on keystrokes, wait a soft beat,
Half a second hush before results meet.
I hop through pages, drop echoes away,
Stitch fresh members so lists show today.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: migrating room member search to use server data instead of local data.
Linked Issues check ✅ Passed The PR implements server-side search for room members (changes in RoomMembersView with fetchMembersWithNewFilter), addressing SUP-973's requirement to find members not in the locally loaded list.
Out of Scope Changes check ✅ Passed All changes are directly related to the scope: debounced search filtering, server-side member fetching with requestId validation, pagination deduplication, and a Maestro E2E test for the search flow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch search-filter-fix

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.

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/views/RoomMembersView/index.tsx (2)

44-44: Refetch on cleared search + use sanitizeLikeString (fixes lint).
Line 44 currently fails lint, and when the filter is cleared the list is reset but never reloaded. Normalize the filter and always fetch so the list repopulates and the unused import is resolved.

✅ Proposed fix
- const handleFilterChange = (text: string) => {
- 	const trimmedFilter = text.trim();
+ const handleFilterChange = (text: string) => {
+ 	const normalizedFilter = sanitizeLikeString(text.trim());
 
 	updateState({
-		filter: trimmedFilter,
+		filter: normalizedFilter,
 		page: 0,
 		members: [],
 		end: false
 	});
 
-	if (trimmedFilter.length > 0) {
-		fetchMembersWithNewFilter(trimmedFilter);
-	}
+	fetchMembersWithNewFilter(normalizedFilter);
 };

Also applies to: 425-437


440-468: Use stop() instead of cancel() for debounce cleanup, and remove the redundant filteredMembers variable.

The debounce helper exposes a stop() method (not cancel()), and filteredMembers is redundant—it always equals state.members because the condition state.members.length > 0 ? state.members : null followed by filteredMembers || state.members always evaluates to state.members.

The memoization pattern is correct to prevent recreating the debounced function per render and avoid orphaned callbacks after unmount.

♻️ Corrected refactor
-import React, { useEffect, useReducer } from 'react';
+import React, { useEffect, useReducer, useCallback, useMemo } from 'react';

-const handleFilterChange = (text: string) => {
+const handleFilterChange = useCallback((text: string) => {
 	// existing body
-};
+}, [fetchMembersWithNewFilter]);

-const debounceFilterChange = debounce(handleFilterChange, 500);
-const filteredMembers = state.members.length > 0 ? state.members : null;
+const debounceFilterChange = useMemo(() => debounce(handleFilterChange, 500), [handleFilterChange]);
+useEffect(() => () => debounceFilterChange.stop?.(), [debounceFilterChange]);

And update the FlatList data prop from filteredMembers || state.members to simply state.members.

🤖 Fix all issues with AI agents
In `@app/views/RoomMembersView/index.tsx`:
- Around line 371-381: When filter is active the code assigns newMembers =
membersResult which replaces prior pages and breaks scrolling; update the logic
in the block that references filter, membersResult, newMembers and members so
that when filter is truthy you either (A) append membersResult to existing
members with de-duplication by _id (e.g. merge members + membersResult and
remove duplicates by _id) to preserve pagination, or (B) explicitly disable
pagination while filtered (set end = true or prevent page increments) so
filtered requests don't replace earlier pages—apply the chosen approach
consistently where PAGE_SIZE, membersResult, newMembers and members are used.
- Around line 395-418: The fetchMembersWithNewFilter function can have
out-of-order debounced responses overwrite newer results; add a request-tracking
guard (e.g., increment a requestId counter or store the latestFilter in
component state) before calling getRoomMembers and capture the current
id/filter, then when the response returns only call updateState(members...) if
the captured id/filter still matches the latest one in state; ensure
requestId/latestFilter is updated at the start of fetchMembersWithNewFilter and
used to ignore stale responses so members/end/page/isLoading updates come only
from the most recent request.

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: 2

🤖 Fix all issues with AI agents
In `@app/views/RoomMembersView/index.tsx`:
- Line 447: debounceFilterChange is recreated every render which breaks
debouncing; memoize the debounced function instead of creating it in the
component body by wrapping the debounce(handleFilterChange, 500) call in useMemo
(or store it in a useRef) so the same debounced instance persists across
renders; if handleFilterChange needs stable identity or fresh state, make
handleFilterChange a useCallback with appropriate deps or keep a ref to the
latest handler and call that from the debounced function.
- Around line 432-445: handleFilterChange currently clears members and resets
state but only calls fetchMembersWithNewFilter when trimmedFilter.length > 0,
leaving the list empty when the user clears the search; modify
handleFilterChange so that when trimmedFilter is empty it also triggers the
unfiltered fetch (e.g., call fetchMembersWithNewFilter('') or a dedicated
fetchUnfilteredMembers function) after updateState, keeping the page reset and
end flag behavior intact; reference the handleFilterChange function,
trimmedFilter variable, updateState call, and fetchMembersWithNewFilter to
locate and change the logic.
♻️ Duplicate comments (1)
app/views/RoomMembersView/index.tsx (1)

374-380: Filtered pagination still drops earlier pages.

When a filter is active and the user scrolls to load more, fetchMembers is triggered via onEndReached. The current logic replaces members with just the new page instead of appending. Consider either appending with de-duplication for filtered paging, or disabling onEndReached while a filter is active.

🐛 Proposed fix: Append with de-dupe for all cases
-		let newMembers: TUserModel[];
-		if (filter) {
-			newMembers = membersResult || [];
-		} else {
-			const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
-			newMembers = [...members, ...membersResultFiltered];
-		}
+		const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
+		const newMembers = [...members, ...(membersResultFiltered || [])];
🧹 Nitpick comments (1)
app/views/RoomMembersView/index.tsx (1)

448-448: Redundant filteredMembers variable.

The expression filteredMembers || state.members always evaluates to state.members:

  • If state.members.length > 0: filteredMembers = state.members
  • If state.members.length === 0: filteredMembers = null, fallback = state.members (empty array)
♻️ Simplify by using state.members directly
-const filteredMembers = state.members.length > 0 ? state.members : null;
 ...
-data={filteredMembers || state.members}
+data={state.members}

Also applies to: 453-453

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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/views/RoomMembersView/index.tsx (1)

352-386: Prevent stale pagination responses from mixing with a new search.
If a page fetch started before a filter/status change returns late, it can append unfiltered members into the filtered list because fetchMembers has no staleness guard. Add a shared request token (or query signature ref) so only the latest request updates state.

🐛 Proposed fix (shared request guard)
@@
-const latestSearchRequest = useRef(0);
+const latestSearchRequest = useRef(0);
+const latestMembersRequest = useRef(0);
+const latestQueryRef = useRef({ filter: '', allUsers: false });
+
+useEffect(() => {
+	latestQueryRef.current = { filter: state.filter, allUsers: state.allUsers };
+}, [state.filter, state.allUsers]);
@@
 const fetchMembers = async (status: boolean) => {
+	const requestId = ++latestMembersRequest.current;
+	const filterAtCall = state.filter;
+	const allUsersAtCall = status;
 	const { members, isLoading, end, room, filter, page } = state;
@@
 	try {
 		const membersResult = await getRoomMembers({
@@
 		const end = membersResult?.length < PAGE_SIZE;
 
 		const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
 		const newMembers = [...members, ...(membersResultFiltered || [])];
 
+		if (
+			requestId !== latestMembersRequest.current ||
+			filterAtCall !== latestQueryRef.current.filter ||
+			allUsersAtCall !== latestQueryRef.current.allUsers
+		) {
+			return;
+		}
+
 		updateState({
 			members: newMembers,
 			isLoading: false,
 			end,
 			page: page + 1
 		});
 	} catch (e) {
 		log(e);
-		updateState({ isLoading: false });
+		if (requestId === latestMembersRequest.current) {
+			updateState({ isLoading: false });
+		}
 	}
 };
♻️ Duplicate comments (1)
app/views/RoomMembersView/index.tsx (1)

442-470: Debounced handler is recreated every render (debounce breaks + stale timers).
Line 442 creates a new debounce instance each render, so timeouts from prior renders can still fire and the debounce no longer coalesces rapid input. Memoize the debounced function and clean it up on unmount.

🐛 Proposed fix (stable debounce + cleanup)
-import React, { useEffect, useReducer, useRef } from 'react';
+import React, { useEffect, useMemo, useReducer, useRef } from 'react';
@@
-const debounceFilterChange = debounce(handleFilterChange, 500);
+const handleFilterChangeRef = useRef(handleFilterChange);
+useEffect(() => {
+	handleFilterChangeRef.current = handleFilterChange;
+}, [handleFilterChange]);
+
+const debounceFilterChange = useMemo(
+	() => debounce((text: string) => handleFilterChangeRef.current(text), 500),
+	[]
+);
+useEffect(() => () => debounceFilterChange.stop(), [debounceFilterChange]);

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

🤖 Fix all issues with AI agents
In `@app/views/RoomMembersView/index.tsx`:
- Around line 171-184: The filter debounce handler (debounceFilterChange) and
fetchMembersWithNewFilter call flow can be clobbered by in-flight fetchMembers
responses because there’s no shared request-id guard; add a single shared
request identifier (e.g., currentRequestId) used by both fetchMembers and
fetchMembersWithNewFilter so every fetch captures the id before dispatch and
each response checks it and ignores stale ids, and when the filter is cleared
(trimmedFilter.length === 0) increment/invalidate the currentRequestId so any
pending filtered responses are ignored; updateState calls remain the same but
ensure all code paths that call fetchMembers or fetchMembersWithNewFilter use
that guard to prevent stale overwrites.
🧹 Nitpick comments (1)
app/views/RoomMembersView/index.tsx (1)

442-447: Simplify redundant filteredMembers logic.

filteredMembers is either state.members or null, and data={filteredMembers || state.members} always resolves to state.members. You can remove the intermediate variable.

♻️ Suggested simplification
-const filteredMembers = state.members.length > 0 ? state.members : null;
...
-	data={filteredMembers || state.members}
+	data={state.members}

@Rohit3523
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/views/RoomMembersView/index.tsx (1)

454-459: Search becomes a no‑op on servers < 3.16 because the fallback API ignores the filter parameter.

The issue is confirmed: getRoomMembers only applies the filter parameter on servers >= 3.16.0; the legacy getUsersOfRoom path doesn't accept it at all. With no client-side filtering fallback, the SearchBox won't filter results on older servers.

Current filtering flow

Both fetchMembers() and fetchMembersWithNewFilter() pass the filter parameter to getRoomMembers(), which:

  • For servers ≥ 3.16.0: Uses {roomType}.members API with the filter applied
  • For servers < 3.16.0: Falls back to getUsersOfRoom() method call, passing only rid, allUsers, and pagination—filter is silently dropped

The FlatList then displays state.members directly with no client-side filtering.

Consider implementing client-side filtering (or disabling search) when the server doesn't support the filter parameter, similar to the proposed solution. If the app's minimum supported server version is 3.16 or higher, document and enforce that requirement instead.

@Rohit3523 Rohit3523 requested a review from diegolmello January 23, 2026 17:34
@Rohit3523 Rohit3523 deployed to approve_e2e_testing January 23, 2026 18:15 — with GitHub Actions Active
@Rohit3523 Rohit3523 had a problem deploying to official_android_build January 23, 2026 18:18 — with GitHub Actions Error
@Rohit3523 Rohit3523 had a problem deploying to experimental_android_build January 23, 2026 18:18 — with GitHub Actions Error
@Rohit3523 Rohit3523 had a problem deploying to experimental_ios_build January 23, 2026 18:18 — with GitHub Actions Error
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

🤖 Fix all issues with AI agents
In @.maestro/tests/room/search-member.yaml:
- Around line 35-39: The test uses a hardcoded username ("rohit.bansal") in
inputText and waits for id 'room-members-view-item-rohit.bansal', creating an
external dependency; instead, create a second test user at runtime via
output.utils.createUser() (as in unread-badge.yaml), assign the returned
username to the inputText and update the extendedWaitUntil id to use that
dynamic username (replace 'room-members-view-item-rohit.bansal' with the
generated user's identifier) so the test is self-contained and no longer depends
on an existing external user.

Copy link
Contributor

@OtavioStasiak OtavioStasiak left a comment

Choose a reason for hiding this comment

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

I've tested this against Prod and Web behaviors and found a failing case.

Scenario: If you search for a user and then switch the filter to 'Online', the app loads the entire list of online users, ignoring the current search term.

Expected behavior: It should apply the status filter to the current search results (matching both the name and the status).

state.members && state.members.length > 0 && state.filter
? state.members.filter(m => m.username.toLowerCase().match(filter) || m.name?.toLowerCase().match(filter))
: null;
const fetchMembersWithNewFilter = async (searchFilter: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We have two functions that do almost the same thing... can't we unify it?
Example:

	const loadMembers = async ({
		filterParam,
		allUsersParam,
		reset = false
	}: {
		filterParam?: string;
		allUsersParam?: boolean;
		reset?: boolean;
	} = {}) => {
		const { members, isLoading, end, room, filter, page, allUsers } = state;
		
		// Determine values to use (param override -> state -> default)
		const currentFilter = filterParam !== undefined ? filterParam : filter;
		const currentAllUsers = allUsersParam !== undefined ? allUsersParam : allUsers;

		// Prevent fetching if loading or reached end (unless it's a reset/new search)
		if (!reset && (isLoading || end)) {
			return;
		}

		const requestId = ++latestSearchRequest.current;

		// Optimistic State Updates
		if (reset) {
			updateState({
				isLoading: true,
				members: [],
				end: false,
				filter: currentFilter,
				allUsers: currentAllUsers,
				page: 0
			});
		} else {
			updateState({ isLoading: true });
		}

		try {
			const membersResult = await getRoomMembers({
				rid: room.rid,
				roomType: room.t,
				type: !currentAllUsers ? 'all' : 'online',
				filter: currentFilter,
				skip: reset ? 0 : PAGE_SIZE * page,
				limit: PAGE_SIZE,
				allUsers: !currentAllUsers
			});

			// Race Condition Check
			if (requestId !== latestSearchRequest.current) {
				return;
			}

			const isEnd = (membersResult?.length ?? 0) < PAGE_SIZE;
			let newMembers = membersResult || [];

			// If appending (not resetting), filter out duplicates
			if (!reset) {
				const filteredNewMembers = newMembers.filter(member => !members.some(m => m._id === member._id));
				newMembers = [...members, ...filteredNewMembers];
			}

			updateState({
				members: newMembers,
				isLoading: false,
				end: isEnd,
				page: reset ? 1 : page + 1
			});
		} catch (e) {
			log(e);
			if (requestId === latestSearchRequest.current) {
				updateState({ isLoading: false });
			}
		}
	};

- extendedWaitUntil:
visible:
id: 'room-members-view-item-rohit.bansal'
timeout: 60000
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing cases like search and filter.

@Rohit3523 Rohit3523 requested a deployment to approve_e2e_testing January 23, 2026 20:59 — with GitHub Actions Waiting
@Rohit3523 Rohit3523 requested a deployment to experimental_android_build January 23, 2026 21:02 — with GitHub Actions Waiting
@Rohit3523 Rohit3523 requested a deployment to official_android_build January 23, 2026 21:02 — with GitHub Actions Waiting
@Rohit3523 Rohit3523 requested a deployment to experimental_ios_build January 23, 2026 21:02 — with GitHub Actions Waiting
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

🤖 Fix all issues with AI agents
In `@app/views/RoomMembersView/index.tsx`:
- Around line 171-189: The debounced callback debounceFilterChange captures a
stale fetchMembersWithNewFilter because useDebounce/useDebouncedCallback has no
dependency array; fix by making fetchMembersWithNewFilter stable with
useCallback and include all values it reads (room, allUsers, any state setters
like updateState) in its dependency list so the debounced callback always calls
the current logic; ensure latestSearchRequest.current usage remains as-is to
discard responses and that debounceFilterChange continues to call the memoized
fetchMembersWithNewFilter.
🧹 Nitpick comments (1)
app/views/RoomMembersView/index.tsx (1)

416-452: Consider unifying fetch functions.

Both fetchMembers and fetchMembersWithNewFilter share significant logic (request ID tracking, error handling, API call structure). As noted in a previous review comment, these could be consolidated into a single loadMembers function with a reset parameter to reduce duplication and simplify maintenance.

This is an optional refactor that can be deferred.

Comment on lines +171 to +189
const debounceFilterChange = useDebounce((text: string) => {
const trimmedFilter = text.trim();

if (!trimmedFilter) {
latestSearchRequest.current += 1;
}

updateState({
filter: trimmedFilter,
page: 0,
members: [],
end: false,
isLoading: false
});

if (trimmedFilter.length > 0) {
fetchMembersWithNewFilter(trimmedFilter);
}
}, 500);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find and examine the debounce implementation
find . -name "debounce.ts" -o -name "debounce.js" | head -20

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Search for useDebounce hook definition
rg -n "export.*useDebounce|function useDebounce" --type ts --type tsx -B 2 -A 15

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Find the exact file path from the repo structure
git ls-files | grep -i debounce

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 149


🏁 Script executed:

#!/bin/bash
# Examine the debounce.ts implementation
wc -l app/lib/methods/helpers/debounce.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Read the debounce.ts file
cat -n app/lib/methods/helpers/debounce.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1165


🏁 Script executed:

#!/bin/bash
# Examine RoomMembersView to see how fetchMembersWithNewFilter is defined and used
wc -l app/views/RoomMembersView/index.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 117


🏁 Script executed:

#!/bin/bash
# Read RoomMembersView around the debounceFilterChange definition (lines 171-189)
# Also need to see the context before and after
sed -n '150,210p' app/views/RoomMembersView/index.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1401


🏁 Script executed:

#!/bin/bash
# Find where fetchMembersWithNewFilter is defined
rg -n "fetchMembersWithNewFilter" app/views/RoomMembersView/index.tsx -B 5 -A 10

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 904


🏁 Script executed:

#!/bin/bash
# Read more context to see the full useDebounce call and state setup
sed -n '1,50p' app/views/RoomMembersView/index.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2337


🏁 Script executed:

#!/bin/bash
# Search for state definition and useReducer/useState usage
rg -n "const state|const \[state|useReducer|useState" app/views/RoomMembersView/index.tsx | head -20

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Get more context around the debounceFilterChange definition to see if there's a deps array
sed -n '165,195p' app/views/RoomMembersView/index.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 737


🏁 Script executed:

#!/bin/bash
# Check the exact structure around the debounceFilterChange - is there a deps array?
sed -n '171,195p' app/views/RoomMembersView/index.tsx | cat -n

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 796


🌐 Web query:

use-debounce useDebouncedCallback dependency array options

💡 Result:

use-debounce’s useDebouncedCallback does not take a “dependency array” argument (older versions did; it was removed). If your debounced function needs to “depend on” props/state, wrap the function you pass in with useCallback(..., deps) so the callback updates when its dependencies change. [2] (You can also include the debounced function itself in effect deps, e.g. for flush() on unmount.) [1]

Current signature (conceptually):

const debounced = useDebouncedCallback(fn, delayMs, options?)

[1]

Options (options argument): maxWait, leading, trailing (and equalityFn is for useDebounce value debouncing only, not useDebouncedCallback). [1]

Example with “deps”:

const debouncedSave = useDebouncedCallback(
  useCallback(() => saveDraft(draftId, text), [draftId, text]),
  500,
  { maxWait: 2000, leading: false, trailing: true }
);

[1][2]


🏁 Script executed:

#!/bin/bash
# Verify if fetchMembersWithNewFilter is wrapped in useCallback or defined without it
sed -n '410,440p' app/views/RoomMembersView/index.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 761


🏁 Script executed:

#!/bin/bash
# Check if there's a useCallback import and usage pattern elsewhere in the file
rg -n "useCallback|useMemo" app/views/RoomMembersView/index.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


Stale closure in debounced callback may cause requests to use outdated state.

The useDebounce hook wraps useDebouncedCallback, which does not support a dependency array. The fetchMembersWithNewFilter function is redefined on every render and reads state (specifically room and allUsers) through closure. Since debounceFilterChange has no dependency array, it captures the first version of fetchMembersWithNewFilter and never updates this reference on subsequent renders. When state changes and a new fetchMembersWithNewFilter is created, the debounced callback still invokes the stale closure, potentially using outdated room or allUsers values.

While the request tracking via latestSearchRequest.current helps discard stale responses, the stale closure can still cause unnecessary API calls or brief race conditions during rapid state changes.

Wrap fetchMembersWithNewFilter in useCallback with proper dependencies, or move it outside the component if it doesn't depend on component state.

🤖 Prompt for AI Agents
In `@app/views/RoomMembersView/index.tsx` around lines 171 - 189, The debounced
callback debounceFilterChange captures a stale fetchMembersWithNewFilter because
useDebounce/useDebouncedCallback has no dependency array; fix by making
fetchMembersWithNewFilter stable with useCallback and include all values it
reads (room, allUsers, any state setters like updateState) in its dependency
list so the debounced callback always calls the current logic; ensure
latestSearchRequest.current usage remains as-is to discard responses and that
debounceFilterChange continues to call the memoized fetchMembersWithNewFilter.

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.

3 participants