Skip to content

fix(ios): resolve reactHost lazily so paste works under Expo bridgeless - #56

Open
janicduplessis wants to merge 1 commit into
mattermost:masterfrom
janicduplessis:fix/ios-lazy-reacthost-bridgeless
Open

fix(ios): resolve reactHost lazily so paste works under Expo bridgeless#56
janicduplessis wants to merge 1 commit into
mattermost:masterfrom
janicduplessis:fix/ios-lazy-reacthost-bridgeless

Conversation

@janicduplessis

@janicduplessisjanicduplessis commented Jul 9, 2026

Copy link
Copy Markdown

Problem

On v2.0.x with the New Architecture (bridgeless), iOS paste interception silently never registers when the app is hosted by Expo (ExpoReactNativeFactory / EXReactRootViewFactory). onPaste never fires and the system "Paste" affordance falls through to the default UITextView handler — so for image-only clipboard contents the library appears to do nothing.

This is distinct from #54 / #55: even with a valid native tag (registration reaches the native module), the native side can't locate the backing text view to swizzle, so the interception is never installed.

Reproduced on React Native 0.85.3 + Expo SDK 56, RCT_NEW_ARCH_ENABLED=1 (bridgeless).

Root cause

PasteInputModule resolves the Fabric surface presenter (used to look up the backing UITextView by tag) from a reactHost captured once in +setup::

+ (void)setup:(RCTRootViewFactory *)rootViewFactory {
#ifdef RCT_NEW_ARCH_ENABLED
_reactHost = rootViewFactory.reactHost; // nil at this point under Expo
#endif
}

But the base RCTRootViewFactory creates the RCTHostlazily, inside viewWithModuleName:createReactHostIfNeeded: — it only assigns self.reactHost when the first surface mounts. Under Expo that mount happens afterapplication:didFinishLaunchingWithOptions: returns, i.e. after the app's PasteInputModule.setup(factory.rootViewFactory) call. So rootViewFactory.reactHost is still nil when setup captures it, and because it's stored in a static it is never refreshed.

The bridge fallback in getSurfacePresenter doesn't help either: in bridgeless mode self.bridge is an RCTBridgeProxy whose surfacePresenter is nil.

Net effect: getSurfacePresenter returns nilfindBackingTextViewForTag: finds nothing → the register retry loop exhausts and gives up → the paste: / canPerformAction: swizzle is never applied.

Evidence

Captured with temporary NSLogs while diagnosing.

Before:

setup: rootViewFactory=<EXReactRootViewFactory: 0x…> reactHost=(null)
findBackingTextViewForTag 5556: reactHost=(null) bridge=<RCTBridgeProxy> surfacePresenter=(null)
view not found for PasteInput_5556 retry=1 … retry=9
GAVE UP finding view for PasteInput_5556 after 10 retries

After:

lazy reactHost from rootViewFactory=<RCTHost: 0x…>
registry lookup tag=4152 -> view=RCTTextInputComponentView
found backing view for PasteInput_4152 -> RCTUITextView
after subclassing PasteInput_4152 -> PasteInput_RCTUITextView
canPerformAction(paste:) hasImages=1 -> 1
onPaste event data=[{ image/png … }]

Fix

Drop the _reactHost static captured in +setup: and instead retain the rootViewFactory, reading reactHost fresh from it in getSurfacePresenter at view-lookup time. By then the host has been created. Reading it fresh each time (rather than caching a value that may have been nil at setup) keeps a single source of truth.

Only touches ios/PasteInputModule.mm. No behavior change on setups where rootViewFactory.reactHost is already populated at setup time.

Testing

  • RN 0.85.3 + Expo SDK 56, bridgeless. <PasteInput onPaste={…} /> focused, PNG on the pasteboard (host → sim via xcrun simctl pbsync host). System Paste → onPaste fires with the image after this change; nothing before.
  • Verified the swizzle installs (canPerformAction(paste:) returns YES for image contents) and unregisters cleanly on unmount.

Notes

@coderabbitai

coderabbitaiBot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@janicduplessis, you've reached your PR review limit, so we couldn't start this review.

Next review available in:14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92c49f25-ea15-4fa8-ab97-8a36e5cf67b5

📥 Commits

Reviewing files that changed from the base of the PR and between 4d2b76b and 7344307.

📒 Files selected for processing (1)
  • ios/PasteInputModule.mm
📝 Walkthrough

Walkthrough

Updated ios/PasteInputModule.mm so +setup: stores the RCTRootViewFactory and getSurfacePresenter resolves reactHost from that factory at call time under RCT_NEW_ARCH_ENABLED.

Changes

Lazy reactHost resolution

Layer / File(s)Summary
Store factory and lazily resolve reactHost
ios/PasteInputModule.mm
Adds a static _rootViewFactory variable, stores it during +setup:, and reads reactHost from it in getSurfacePresenter when needed.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Title check✅ PassedThe title clearly describes the main fix: lazily resolving reactHost so paste works in Expo bridgeless iOS.
Description check✅ PassedThe description matches the change and explains the bug, root cause, fix, and testing.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

janicduplessis added a commit to tloncorp/tlon-apps that referenced this pull request Jul 9, 2026
…under Expo bridgeless
The iOS paste swizzle never installed because PasteInputModule captures
reactHost once in +setup:, but Expo creates the RCTHost lazily after that
call, leaving reactHost nil. Patch re-reads it from the rootViewFactory at
lookup time. Upstream: mattermost/react-native-paste-input#56.
@janicduplessis
janicduplessisforce-pushed the fix/ios-lazy-reacthost-bridgeless branch from a6e711c to 4d2b76bCompareJuly 9, 2026 17:57
@janicduplessis
janicduplessisforce-pushed the fix/ios-lazy-reacthost-bridgeless branch from 4d2b76b to 7344307CompareJuly 9, 2026 18:01
Sign up for freeto 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.

1 participant

@janicduplessis