Skip to content

refactor(CDS-2123): migrate forwardRef to ref-as-prop in packages/mobile#741

Open
cb-ekuersch wants to merge 5 commits into
masterfrom
refactor-mobile-refs
Open

refactor(CDS-2123): migrate forwardRef to ref-as-prop in packages/mobile#741
cb-ekuersch wants to merge 5 commits into
masterfrom
refactor-mobile-refs

Conversation

@cb-ekuersch
Copy link
Copy Markdown
Contributor

@cb-ekuersch cb-ekuersch commented Jun 4, 2026

What changed? Why?

Note

TL;DR: Removed forwardRef wrappers across packages/mobile and replaced them with the React 19 ref-as-prop pattern. This is a required migration ahead of the React 19 upgrade tracked in CDS-2123.

Original, internal PR: https://coinbase.ghe.com/frontend/cds-internal/pull/151

The official React codemod was applied to all source files in packages/mobile, converting React.forwardRef(fn) calls to direct ref-as-prop function signatures. Manual fixups cover cases the codemod cannot handle: generic ForwardRefWithPeriod<Period> typings (e.g. SparklineInteractiveHoverDate), React.ForwardRefExoticComponent type aliases, displayName assignments, and re-exports.

Test infrastructure updates

React 19 promotes memo(fn) from MemoComponent (fiber tag 14) to SimpleMemoComponent (fiber tag 15), which exposes the inner function's .name on fiber.type. Two follow-on issues surfaced and were addressed without changing component runtime behavior:

  1. isPressable / isText accessibility helpers (packages/mobile/jest/accessibility/helpers.ts) previously matched both the CDS wrapper (Pressable/Text) and the leaf RN host node, producing duplicate matches and false positives in the a11y rule matchers. They now use node.findAll(...) length-based dedup so only the leaf node is considered a Pressable/Text. The 6 a11y matchers in rules.ts were updated to pass the node instead of node.type.
  2. UNSAFE_*ByType(Component) call sites broke because fiber reference identity changed under SimpleMemoComponent. Migrated four test files off type-identity queries:
    • IconCounterButton.test.tsx: UNSAFE_queryAllByType(Pressable)getAllByRole('button').
    • Text.test.tsx: UNSAFE_queryAllByType(Text)getAllByTestId('text-${fontName}').
    • Button.test.tsx: UNSAFE_getByType(Text)UNSAFE_getAllByProps({ testID: 'text-headline' }) filtered by wrapper-only props (does not depend on fiber-type identity).
    • RollingNumber.a11y.test.tsx: added testID="rolling-number-sr-only" to the screen-reader <Text> in RollingNumber.tsx and switched the helper to screen.queryByTestId(...).

UI changes

No visual changes — this is a pure code-pattern refactor with no behavioral or rendering differences.

Testing

How has it been tested?

  • Unit tests

Testing instructions

yarn nx run mobile:lint
yarn nx run mobile:typecheck
yarn nx run mobile:build
yarn nx run mobile:test

All four pass: 169 test suites / 1828 passing tests, 0 lint or type errors.

Illustrations/Icons Checklist

Not applicable — no changes to packages/illustrations/** or packages/icons/**.

Change management

type=routine
risk=low
impact=sev5

automerge=false

forge[bot] and others added 5 commits June 4, 2026 17:27
Apply the official React codemod and manual fixups to replace
React.forwardRef wrapper pattern with ref-as-prop across 134 files
in packages/mobile.

Manual fixups cover: generic ForwardRefWithPeriod<Period> typings,
React.ForwardRefExoticComponent aliases, displayName patterns, and
re-exports. Test infrastructure changes are pending a resolution on
React 19 SimpleMemoComponent fiber semantics that affect
UNSAFE_*ByType queries and the isPressable accessibility helper.

Co-authored-by: Erich Kuerschner <erich.kuerschner@coinbase.com>
Replace UNSAFE_*ByType(Component) reference-identity calls with role-
and testID-based queries in Button, IconCounterButton, Text, and
RollingNumber test files. Update isPressable/isText matchers in the
accessibility helpers to dedup CDS wrapper nodes via findAll-length
checks, matching the existing pattern in canBeDisabled. Add a stable
testID to RollingNumber's screen-reader Text node to enable testID
lookup. All 169 suites / 1828 tests pass; lint and typecheck clean.

Co-authored-by: Erich Kuerschner <erich.kuerschner@coinbase.com>
- Add ref-forwarding tests for TextInput and ContentCard
- Remove React.FC from Tour.tsx; use direct callable signatures
- Widen TourStepArrowComponent type in common/useTour to accept
  ref-as-prop components without structural mismatch
- Fix isText helper to only dedup when direct Text child shares
  the same onPress, accessibilityRole, and accessibilityLabel,
  preserving a11y violations on legitimate nested compositions
- Remove shipped testID from RollingNumber SR-only Text node and
  update test to query via prop-based UNSAFE_queryAllByProps

Co-authored-by: Erich Kuerschner <erich.kuerschner@coinbase.com>
…obile Tour

Reverts the TourStepArrowComponent type change in packages/common so that
packages/web (still on forwardRef) is not coupled to mobile's React 19
migration timing.

packages/mobile/src/tour/Tour.tsx now handles the type mismatch via
narrow `as unknown as TourStepArrowComponent` casts at the consumer
boundary, with comments explaining the legacy forwardRef shape and the
StyleProp<ViewStyle> vs Record<string,string|number> divergence.

All validation gates pass: mobile lint, typecheck, build, test
(1830 passed), common typecheck, and web typecheck.

Co-authored-by: Erich Kuerschner <erich.kuerschner@coinbase.com>
… RollingNumber a11y tests

Use `screen.queryAllByText(/.+/)` filtered by `accessibilityLiveRegion`
prop to locate the SR-only Text node, eliminating the last UNSAFE_*
query in RollingNumber.a11y.test.tsx. Visible and measured-digit Text
nodes are excluded by RNTL's default `includeHiddenElements: false`
because they carry `accessibilityElementsHidden` /
`importantForAccessibility="no-hide-descendants"`, leaving only the
screen-reader Text for the `.find()` predicate to match.

Co-authored-by: Erich Kuerschner <erich.kuerschner@coinbase.com>
@cb-heimdall
Copy link
Copy Markdown
Collaborator

cb-heimdall commented Jun 4, 2026

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 1
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1
CODEOWNERS 🟡 See below

🟡 CODEOWNERS

Code Owner Status Calculation
ui-systems-eng-team 🟡 0/1
Denominator calculation
Additional CODEOWNERS Requirement
Show calculation
Sum 0
0
From CODEOWNERS 1
Sum 1

@linear
Copy link
Copy Markdown

linear Bot commented Jun 4, 2026

CDS-2123

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

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

Development

Successfully merging this pull request may close these issues.

2 participants