Skip to content

[Due for payment 2026-11-02] [$175] [Sentry: APP-JQB] FlashList "right operand of 'in' is not an object" in participant selector #103776

Open

Description

@mountiny

Sentry

https://expensify.sentry.io/issues/APP-JQB

Impact (snapshot at filing, 2026-10-12)

  • Users (total since first seen): 80
  • Users (last 30d, releases 9.5.5 to 9.5.7 only): 26
  • Events (30d window): 84
  • First seen: 2026-08-02
  • Last seen: ongoing
  • Platform: iOS hybrid app
  • App version(s): 9.5.5
  • Affected route(s): Dynamic money request step: participants (create expense flow)
  • Mechanism: generic (unhandled TypeError during render)

Stack trace (top frames, first-party only)

FlashList
useRecyclerViewManager
processDataUpdate
getDataKey
_temp2 (BaseSelectionListWithSections)

Suspected cause

getDataKey in the FlashList data processing runs the 'in' operator on a value that is not an object. The data comes from BaseSelectionListWithSections inside MoneyRequestParticipantsSelector.

Reproduction

Unknown. See the events linked from the Sentry issue.

Related

  • Sibling Sentry IDs: none
  • Prior GH issues: none found
Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. self-assigned this
    on Oct 12, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 12, 2026
  3. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @thelullabyy (External)

  4. changed the title [-][Sentry: APP-JQB] FlashList "right operand of 'in' is not an object" in participant selector[/-] [+][$175] [Sentry: APP-JQB] FlashList "right operand of 'in' is not an object" in participant selector[/+] on Oct 12, 2026
  5. melvin-bot commented on Oct 12, 2026

    @melvin-bot
  6. mountiny commented on Oct 12, 2026

    @mountiny
    ContributorAuthor

    (Vit's agent) @thelullabyy, there is no proposal on this issue yet. Can you look at the Sentry issue and tell us whether we should open it for contributors as it stands, or ask for expert contributor help because it needs deeper FlashList knowledge?

  7. taieddie commented on Oct 12, 2026

    @taieddie

    Contributor details
    Your Expensify account email: taieddie8@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~01af851c9a7f771775

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    A runtime TypeError ("right operand of 'in' is not an object") crashes the application on iOS during render in the participant selector when creating an expense.

    What is the root cause of that problem?

    The crash occurs in src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx at line 367:
    keyExtractor={(item) => ('flatListKey' in item ? item.flatListKey : item.keyForList)}

    When FlashList performs list recycling, layout measurement, or data updates in useRecyclerViewManager.processDataUpdate -> getDataKey, keyExtractor can be invoked with an item that is undefined or not an object (for example, during rapid search filter transitions, list resets, or if section.data contains uninitialized entries).

    In JavaScript, evaluating the in operator on a non-object ('flatListKey' in undefined) immediately throws TypeError: right operand of 'in' is not an object in JavaScriptCore on iOS.

    By contrast, the flat list variant in src/components/SelectionList/BaseSelectionList.tsx (line 523) safely guards the item with optional chaining and provides a fallback to the index string:
    keyExtractor={(item, index) => item?.keyForList ?? index.toString()}

    In BaseSelectionListWithSections.tsx, keyExtractor lacks both the object type guard and the index fallback.

    What changes do you think we should make in order to solve the problem?

    1. In src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx:
      Update keyExtractor to accept (item, index) and guard against null or non-object values before evaluating property access:
    • Check if item is a valid object (item && typeof item === 'object').
    • If valid, return item.flatListKey when present (or item.keyForList).
    • Fall back safely to index.toString() if item is null, undefined, or missing key properties.
    1. In src/components/SelectionList/hooks/useFlattenedSections.ts:
      In the inner loop iterating over section.data, add an explicit guard (if (!item || typeof item !== 'object') continue;) to prevent null or undefined entries from being pushed into flattenedData during dynamic search calculations.

    2. In automated tests:
      Add unit test coverage in tests/unit/BaseSelectionListSectionsTest.tsx asserting that:

    • keyExtractor safely returns a deterministic key when given a standard row item with flatListKey.
    • keyExtractor safely returns keyForList when given a section header.
    • keyExtractor returns index.toString() without throwing when invoked with undefined or null.

    What alternative solutions did you explore? (Optional)

    I explored filtering nullish items only inside ParticipantSearchResults.tsx. However, patching individual consumer screens is fragile because any screen utilizing SelectionListWithSections (such as workspace members, approver picker, or currency picker) could trigger the same crash if FlashList invokes keyExtractor during internal data recycling. Hardening keyExtractor directly in BaseSelectionListWithSections and useFlattenedSections resolves the issue at the framework layer for all consumers, matching the defensive pattern already used in BaseSelectionList.tsx.

  8. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  9. MelvinBot commented on Oct 12, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    FlashList asks for a key for an item index that is past the end of the current list. Our keyExtractor then runs 'flatListKey' in undefined, which throws.

    This happens because the participants list is wrapped in <Activity>. When the screen blurs, Activity hides the list and runs its effect cleanups, but the list stays mounted:

    1. MoneyRequestParticipantsSelector.tsx:114 wraps the list in <Activity mode={isActivityVisible ? 'visible' : 'hidden'}>. Deferring & deprioritizing participant search render added it. That PR merged on 2026-07-27, six days before this crash was first seen.
    2. When the list is hidden, FlashList's effect cleanup calls recyclerViewManager.dispose(). A disposed manager skips updateScrollOffset, so it never refreshes its engaged indices (the item indices it keeps rendered).
    3. The hidden list still re-renders when the sections change. If the list gets shorter, processDataUpdate reuses the old engaged indices. Then getDataKey calls keyExtractor(data[index]) with data[index] === undefined.
    4. Our keyExtractor at BaseSelectionListWithSections.tsx:367 has no guard, so it throws.

    The flattened data itself is fine. useFlattenedSections.ts only pushes object literals. The undefined comes from an out-of-range index, not from a bad entry.

    What changes do you think we should make in order to solve the problem?

    Make the keyExtractor in BaseSelectionListWithSections null-safe. When item is missing, fall back to index.toString().

    This matches existing code:

    • BaseSelectionList.tsx:523 already does item?.keyForList ?? index.toString(). Its comment notes that FlashList can pass a stale index after the dataset shrinks.
    • getItemType in the same file (line 36) already uses item?.type.

    FlashList recycles the stale slots on its next sync, after the list becomes visible again.

    What alternative solutions did you explore? (Optional)

    Add a @shopify/flash-list patch (017) so FlashList never uses stale indices. Either skip processDataUpdate while the manager is disposed, or ignore engaged indices >= data.length in updateRenderStack. This would protect every FlashList that renders under Activity. It is the same kind of bug that patch 016 fixed on another code path, in Guard FlashList ViewHolderCollection render against stale layout index (web crash). We can do this as well as the app-side guard. It is not a replacement for the guard.

    I didn't choose filtering falsy items in useFlattenedSections. The data never contains falsy items, so the filter wouldn't fix the crash.

    Confidence and open questions
    • Mechanism: high confidence. In FlashList 2.3.2 plus our patches, this is the only path where processDataUpdate uses an out-of-range index. React disconnects passive effects only for Activity/Offscreen, not for Suspense or react-freeze. That explains why the crash started only after the Activity change.
    • Trigger PR: medium-high confidence. The timing and the code match. I didn't confirm that the first-seen release contains it, because Sentry wasn't reachable from this run.
    • Not reproduced on a device. The exact data change while the list is hidden is unknown. The likely candidates are a selection or participants reset after picking someone, or an Onyx or search refresh during the blur or refocus transition. Sentry breadcrumbs around navigation would confirm.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/103776) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  10. janiya2 commented on Oct 12, 2026

    @janiya2

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    [Sentry: APP-JQB] Unhandled TypeError: right operand of 'in' is not an object (or Cannot use 'in' operator to search for 'flatListKey' in undefined) in MoneyRequestParticipantsSelector / BaseSelectionListWithSections on iOS/Android.


    What is the root cause of that problem?

    The crash occurs because FlashList evaluates keyExtractor with an out-of-range/stale index (data[index] === undefined) when the list dataset shrinks or updates while blurred under <Activity>:

    1. The <Activity> Blur Lifecycle:
      In src/pages/iou/request/MoneyRequestParticipantsSelector.tsx (line 114), the participants list is wrapped in:
      <Activity mode={isActivityVisible ? 'visible' : 'hidden'}>
      When the user selects a participant or navigates away, the screen blurs, triggering the Activity hidden state. Under Activity hidden mode, FlashList's cleanup calls recyclerViewManager.dispose(). When disposed, updateScrollOffset is suspended, leaving the cached "engaged indices" (the range of item indices FlashList keeps rendered) untouched.

    2. Dataset Shrinking and Stale Engaged Indices:
      When sections or participant results update (e.g. selection resets or search query clear), the new flattenedData length is shorter than the previously cached engaged indices. During processDataUpdate, FlashList iterates over these stale engaged indices and calls getDataKey(index), which executes:
      keyExtractor(data[index], index)
      Because index >= data.length, data[index] evaluates to undefined.

    3. Missing Guard in BaseSelectionListWithSections.tsx:
      In src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx (line 367):

      keyExtractor={(item) => ('flatListKey' in item ? item.flatListKey : item.keyForList)}

      Unlike its flat counterpart in BaseSelectionList.tsx (lines 522-523), which explicitly documents this behavior:

      // FlashList can pass a stale index here while its render stack syncs after the dataset shrinks, so item can be undefined.
      keyExtractor={(item, index) => item?.keyForList ?? index.toString()}

      BaseSelectionListWithSections.tsx neither accepts the second index argument nor guards against item being undefined. Evaluating 'flatListKey' in undefined throws:
      TypeError: right operand of 'in' is not an object.

    (Note: The data emitted by useFlattenedSections.ts is completely valid and only contains constructed object literals; the undefined is strictly produced by FlashList's out-of-bounds indexing during data updates).


    What changes do you think we should make in order to solve the problem?

    In src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx (line 367), update keyExtractor to accept (item, index) and guard against null/undefined:

    -   keyExtractor={(item) => ('flatListKey' in item ? item.flatListKey : item.keyForList)}
    +   // FlashList can pass a stale index here while its render stack syncs after the dataset shrinks, so item can be undefined.
    +   keyExtractor={(item, index) => (item && typeof item === 'object' && 'flatListKey' in item ? item.flatListKey : (item?.keyForList ?? index.toString()))}

    This brings BaseSelectionListWithSections into full parity with the defensive pattern already established in BaseSelectionList.tsx:523 and getItemType (BaseSelectionListWithSections.tsx:36). When FlashList syncs on the subsequent pass, it safely recycles the stale slots without crashing.


    What alternative solutions did you explore? (Optional)

    1. Filtering in useFlattenedSections.ts:
      Filtering falsy items inside useFlattenedSections.ts does not solve the crash because useFlattenedSections.ts never produces undefined items—the undefined value originates inside FlashList when indexing beyond flattenedData.length.
    2. Patching @shopify/flash-list:
      While a patch could be added to ignore out-of-bounds indices in RecyclerViewManager.ts, handling the fallback in keyExtractor adheres to Expensify's standard architecture in BaseSelectionList.tsx and prevents requiring patch maintenance across future FlashList upgrades.

    Automated Test Scenarios

    Add a unit test in tests/unit/BaseSelectionListWithSectionsTest.tsx:

    describe('BaseSelectionListWithSections keyExtractor', () => {
        // Extracted keyExtractor logic under test
        const keyExtractor = (item: any, index: number) =>
            item && typeof item === 'object' && 'flatListKey' in item ? item.flatListKey : (item?.keyForList ?? index.toString());
    
        it('does not throw and falls back to index string when item is undefined (FlashList dataset shrinking)', () => {
            expect(() => keyExtractor(undefined, 5)).not.toThrow();
            expect(keyExtractor(undefined, 5)).toBe('5');
        });
    
        it('does not throw and falls back to index string when item is null', () => {
            expect(() => keyExtractor(null, 3)).not.toThrow();
            expect(keyExtractor(null, 3)).toBe('3');
        });
    
        it('extracts flatListKey when item is a section row item', () => {
            const item = {flatListKey: '0-user_123', keyForList: 'user_123'};
            expect(keyExtractor(item, 0)).toBe('0-user_123');
        });
    
        it('extracts keyForList when item is a section header', () => {
            const item = {keyForList: 'section_header_0'};
            expect(keyExtractor(item, 0)).toBe('section_header_0');
        });
    
        it('falls back to index string when object lacks both flatListKey and keyForList', () => {
            const item = {};
            expect(keyExtractor(item, 4)).toBe('4');
        });
    });

    Contributor Details

  11. 17 remaining items

  12. thelullabyy commented on Oct 22, 2026

    @thelullabyy
    Contributor

    It is ready for review @mountiny #104311

  13. changed the title [-][$175] [Sentry: APP-JQB] FlashList "right operand of 'in' is not an object" in participant selector[/-] [+][Due for payment 2026-11-02] [$175] [Sentry: APP-JQB] FlashList "right operand of 'in' is not an object" in participant selector[/+] on Oct 26, 2026
  14. melvin-bot commented on Oct 26, 2026

    @melvin-bot

    @thelullabyy

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.20-5 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-11-02. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  15. melvin-bot commented on Oct 29, 2026

    @melvin-bot

    @thelullabyy Whoops! This issue is 2 days overdue. Let's get this updated quick!

  16. melvin-bot commented on Nov 1, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  17. melvin-bot commented on Nov 1, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions