fix(react-native): stop false-positives on Expo Universal UI (@expo/ui)#645
Merged
Conversation
Universal UI (@expo/ui) ListItem renders raw string children in the native headline text area, and its compound slot markers (Leading/Supporting/Trailing) forward strings into native text — so raw text is safe, unlike RN's core View. Recognize them as text-handling, gated on the @expo/ui import (root and the swift-ui/jetpack-compose subpaths, plus renamed and namespace imports) so a same-named custom ListItem in a plain RN app still reports. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
commit: |
Contributor
|
React Doctor found 10 files changed in this pull request, but none matched the files covered by its enabled checks. Scope: 10 files changed on Generated by React Doctor. Questions? Contact founders@million.dev. |
Generalize the @expo/ui element detection into a reusable isExpoUiComponentElement helper (covers named, renamed, slot-member, and namespace imports across the @expo/ui root and swift-ui/jetpack-compose subpaths) and reuse it from both rn-no-raw-text (ListItem) and rn-no-scrollview-mapped-list (ScrollView). @expo/ui's ScrollView is a native scroll container — RN's virtualized lists can't compose inside its Host tree and @expo/ui ships its own List — so the FlashList/FlatList advice was a wrong-advice false positive there. Add the rule's first unit tests covering the exclusion and the still-fires cases. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Drop the two single-use EXPO_UI_LIST_ITEM_COMPONENT / EXPO_UI_SCROLL_VIEW_COMPONENT constants and pass the names inline, matching the canonical pattern in this rule family (rn-no-image-children inlines "react-native"/"Image"). Removes a layer of indirection and the triplicated rationale comment; the per-rule rationale now lives only at each call site. EXPO_UI_MODULE_SOURCES (a reused multi-value set) stays. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
rn-bottom-sheet-prefer-native and rn-prefer-pressable-over-gesture-detector
moved their actionable fix text into the `recommendation` field and reworded
`message` to the user-impact style, but two test assertions still checked the
old fix substrings ("prefer <Modal", "Pressable") against `message`. Assert
substrings that are actually present in the current messages, matching the
message-substring convention used across the sibling rule tests.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4f054b2. Configure here.
Bugbot: isExpoUiNamespaceImport used isImportedFromModule, which matches any import type (named/default/namespace) by source — so a named @expo/ui import reused via member access (<Row.ListItem>) was wrongly treated as the namespace form and suppressed. Add a namespace-specific isNamespaceImportFromModule predicate to the canonical import-source util (checks ImportInfo.isNamespace) and use it. Add a regression test locking the named-vs-namespace boundary. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
This was referenced Jun 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Problem
On Expo SDK 56 with Universal UI (
@expo/ui),rn-no-raw-textreports a wall of false positives — e.g."Raw text outside a Text component" ×69— for valid code:@expo/uiis a native UI layer (it delegates to SwiftUI / Jetpack Compose), not React Native's core primitives, so several RN-core assumptions don't hold for its components.Investigation — which
@expo/uicomponents / rules are actually affectedI audited the full Universal UI component set against the React Native rules:
@expo/uicomponentListItem(+Leading/Supporting/Trailingslots)rn-no-raw-textfalse positiveButtonlabelprop;childrendoc says "Only nested elements are supported, not plain strings"Collapsiblelabelprop; content is always<Text>-wrappedTextColumn/Row/Spacer<Text>ScrollViewrn-no-scrollview-mapped-listRule audit: most RN rules are already safe because they're import-gated to
react-native(rn-no-image-children,rn-prefer-expo-image,rn-no-deprecated-modules), package-gated (rn-bottom-sheet-prefer-native), or prop-shape/behavior-gated (rn-scrollview-flex-in-content-container,rn-scrollview-dynamic-padding,rn-no-scroll-state). The two genuinely affected rules are name-only gated.Fixes
Both fixes share a new reusable helper,
isExpoUiComponentElement, that resolves whether a JSX element is a given@expo/uiexport — covering named, renamed, slot-member (<ListItem.Supporting>), and namespace (<ExpoUI.ListItem>) imports across the@expo/uiroot and the@expo/ui/swift-ui/@expo/ui/jetpack-composesubpaths. Gating on the import (not the name) means same-named components from other libraries — or with no import — still report. The helper takes the targetcomponentNamesorn-no-raw-textsuppresses onlyListItem(raw text inColumn/Rowstill fires).rn-no-raw-textnow treats<ListItem>and its compound slot markers as text-handling.rn-no-scrollview-mapped-listno longer flags mapped children inside an@expo/ui<ScrollView>— RN's virtualized lists can't compose inside the native<Host>tree, and@expo/uiships its own virtualized<List>, so the FlashList/FlatList advice was actively wrong there.The ESLint mirror (
eslint-plugin-react-doctor) re-exports the oxlint rules directly, so both fixes propagate there automatically.Changes
constants/react-native.ts: addEXPO_UI_MODULE_SOURCES(the root + two platform subpaths).rules/react-native/utils/is-expo-ui-component-element.ts: new reusable helper (built on existingflattenJsxName+ import-source utilities).rules/react-native/rn-no-raw-text.ts&rn-no-scrollview-mapped-list.ts: one early-return each via the helper, with the target component name passed inline (matchingrn-no-image-children's inline"react-native"/"Image"pattern).rn-no-raw-textcases + a newrn-no-scrollview-mapped-listtest file (the rule previously had none).react-doctor: patch).Verification
rn-no-raw-text(18/18) andrn-no-scrollview-mapped-list(6/6) pass.typecheck,lint,formatclean on changed files.rn-bottom-sheet-prefer-native,rn-prefer-pressable-over-gesture-detector) are pre-existing and unrelated — confirmed failing on the base branch with my changes stashed.Refs: https://docs.expo.dev/versions/v56.0.0/sdk/ui/universal/list/ · https://docs.expo.dev/versions/v56.0.0/sdk/ui/universal/