Skip to content

[$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: @thelullabyy

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. jloa-dev commented on Oct 12, 2026

    @jloa-dev

    Proposal

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

    In \MoneyRequestParticipantsSelector, navigating or filtering participants throws an unhandled \TypeError: right operand of 'in' is not an object\ in \keyExtractor\ inside \BaseSelectionListWithSections.tsx\ (Sentry: APP-JQB).

    What is the root cause of that problem?

    In \src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx\ (line 367):
    \\ ypescript\keyExtractor={(item) => ('flatListKey' in item ? item.flatListKey : item.keyForList)}
    \
    When \MoneyRequestParticipantsSelector\ is blurred or wrapped in <Activity>, FlashList disposes its RecyclerView manager during effect cleanups without updating scroll/engaged indices. If the participant sections shrink (e.g. searching, clearing, or resetting participants), FlashList reuses engaged indices that exceed the newly shrunk dataset, passing \undefined\ to \keyExtractor(data[index]).
    Because \item\ is \undefined, 'flatListKey' in undefined\ throws \TypeError: right operand of 'in' is not an object.

    By contrast, \BaseSelectionList.tsx\ (line 523) already guards this with \item?.keyForList ?? index.toString(), noting that FlashList can query stale indices after dataset shrinkage.

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

    In \src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx:
    Make \keyExtractor\ null-safe and fall back to \index.toString():
    \\ ypescript
    keyExtractor={(item, index) => (item && 'flatListKey' in item ? item.flatListKey : item?.keyForList ?? index.toString())}
    \\

    What alternative solutions did you explore? (Optional)

    • Filtering out falsy items in \useFlattenedSections.ts: Unnecessary because the flattened sections only contain valid objects; the out-of-range index comes directly from FlashList's internal recycling stack.
    • Patching FlashList: Adding null guards directly to consumer components is idiomatic and matches the pattern already in \BaseSelectionList.tsx.

    What specific scenarios should we cover in automated tests?

    In \ ests/unit/BaseSelectionListWithSectionsTest.tsx:

    1. Verify that \keyExtractor\ returns \index.toString()\ without throwing when passed \undefined\ or null item.
    2. Verify that valid section headers and list items extract \ latListKey\ and \keyForList\ correctly.

    Contributor details
    Your Expensify account email: jloa.dev@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0163955a2ba22190aa

  12. melvin-bot commented on Oct 12, 2026

    @melvin-bot

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

  13. camesenin commented on Oct 13, 2026

    @camesenin

    Proposal

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

    In MoneyRequestParticipantsSelector and list views using BaseSelectionListWithSections, when the list is hidden or dataset items shrink during activity state transitions, keyExtractor crashes with TypeError: Cannot use 'in' operator to search for 'flatListKey' in undefined due to stale index evaluation on item without verifying that item is a valid object.

    What is the root cause of that problem?

    In BaseSelectionListWithSections.tsx:367, keyExtractor evaluates:

    'flatListKey' in item

    without verifying that item is a valid object.

    When MoneyRequestParticipantsSelector blurs, it wraps the list in <Activity mode={isActivityVisible ? 'visible' : 'hidden'}>. Hiding the component invokes recyclerViewManager.dispose() in FlashList, which halts scroll offset recalculations.
    If the underlying flattened sections shrink while the list remains mounted and hidden, FlashList's processDataUpdate attempts to reconcile layout using stale engaged indices that exceed the new dataset bounds. FlashList then calls keyExtractor(data[index], index) with data[index] === undefined.

    Evaluating 'flatListKey' in undefined throws:
    TypeError: Cannot use 'in' operator to search for 'flatListKey' in undefined.

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

    1. In src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx:367, guard item in keyExtractor before checking 'flatListKey':
    if (!item || typeof item !== 'object') {
        return index.toString();
    }

    If item is undefined (stale out-of-range index), safely fall back to index.toString().
    This mirrors the existing pattern already implemented in src/components/SelectionList/BaseSelectionList.tsx:523 (item?.keyForList ?? index.toString()).
    2. Apply the same defensive guard to getItemType in BaseSelectionListWithSections.tsx:36 to ensure stale indices don't trigger subsequent runtime errors.
    3. Add a unit test in tests/unit/BaseSelectionListWithSectionsTest.tsx:

    • Pass an undefined item to keyExtractor and assert that it returns index.toString() without throwing.

    What alternative solutions did you explore? (Optional)

    • Unmounting the list instead of toggling Activity visibility: Rejected because keeping the list mounted preserves scroll positions and layout caches for fast tab switching. Defensive guarding in keyExtractor matches the sibling BaseSelectionList pattern directly.

    Contributor details
    Your Expensify account email: camesenin@gmail.com
    Upwork Profile link: https://www.upwork.com/freelancers/~0170d6b02e9440e604

  14. melvin-bot commented on Oct 13, 2026

    @melvin-bot

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

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

Metadata

Metadata

Labels

BugSomething 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