Summary
On iOS, every JS→native command method on <CPDFReaderView> hangs forever (or silently no-ops) under React Native New Architecture interop (RCT_NEW_ARCH_ENABLED=1, Fabric on, Bridgeless off). pdfReaderRef.current.save() is the most visible failure mode — its Promise never resolves and never rejects, leaving any UI that awaits it stuck. The same broken code path also affects _pdfDocument.getDocumentPath(), _pdfDocument.getPageSize(), _pdfDocument.renderPage(), and ~60 other view-manager methods. Android is not affected.
Environment
@compdfkit_pdf_sdk/react_native@2.5.0 (also reviewed 2.6.6 source — see "Note on 2.6.6" below)
- iOS pods:
ComPDFKit (2.5.0), ComPDFKit_Tools (2.5.0)
- React Native: 0.77.3
- iOS deployment target: 15.1
- Reproduced on iPhone 17 Pro Max simulator
- New Architecture: enabled
- Bridgeless: disabled
- Under these flags,
RCTCPDFReaderView (a legacy RCTViewManager) is hosted inside RCTLegacyViewManagerInteropComponentView.
Steps to reproduce
- Build an app with
RCT_NEW_ARCH_ENABLED=1 and bridgelessEnabled=false on iOS.
- Render
<CPDFReaderView ref={pdfReaderRef} ... />.
- Load a PDF, optionally add an annotation.
- Call
await pdfReaderRef.current.save().
Expected: Promise resolves with true.
Actual: Promise never resolves and never rejects. JS hangs.
Root cause
RCTCPDFReaderView.cpdfViewAttached(_:) writes to a side-channel dictionary using cpdfView.reactTag.intValue as the key. Under iOS New Arch interop, the RCTCPDFView instance returned by view() is hosted inside an interop wrapper that owns the React tag JS sees via findNodeHandle(). The inner view's reactTag property is nil/0 at the point cpdfViewAttached fires, so the dictionary write is meaningless — the dictionary stays empty (or holds only a 0 key that JS will never query).
When JS calls e.g. CPDFViewManager.save(tag):
RCTDocumentManager.save(tag:resolve:reject:) calls reader.saveDocument(forCPDFViewTag: tag).
RCTCPDFViewManager.saveDocument(forCPDFViewTag:completionHandler:) reads cpdfViews[tag] — gets nil.
rtcCPDFView?.saveDocument(...) short-circuits via optional chaining. The completion handler is silently discarded.
resolve / reject are never called. The JS promise hangs.
Diagnostic logs (with temporary NSLog additions to confirm)
Added NSLog statements to ios/RCTDocumentManager.swift::save and ios/RCTCPDFViewManager.swift::saveDocument(forCPDFViewTag:). After loading a PDF, adding an annotation, and tapping save:
CPDFViewManager.save called with tag=3032
readerView resolved, cpdfViews keys=[]
saveDocument(forCPDFViewTag: 3032) — cpdfViews[tag] is NIL (all keys: [])
⚠ view lookup returned NIL — completion handler will NEVER fire, promise will hang
saveDocument call returned synchronously (waiting for completion)
Tag 3032 is also the value on every onChange event payload received by JS — confirming findNodeHandle() and event-bubbling targets are consistent. The only thing out of sync is the iOS-side cpdfViews registry.
Files involved
ios/RCTCPDFViewManager.swift:34-45 — saveDocument(forCPDFViewTag:) reads cpdfViews[tag]
ios/RCTCPDFViewManager.swift:820-822 — cpdfViewAttached(_:) writes cpdfViews[cpdfView.reactTag.intValue]
ios/RCTDocumentManager.swift::save(tag:resolve:reject:) — promise resolver that gets silently dropped
ios/RCTCPDFView.swift::createCPDFView() — call site for delegate?.cpdfViewAttached(self)
Why Android works
Android's CPDFViewModule (and analogous Java code path) dispatches commands through standard view-by-reactTag lookup in UIManagerModule rather than a parallel side-channel dictionary. We've confirmed Android saves complete in ~30ms on the same build with the same JS code.
Note on 2.6.6
I checked the v2.6.6 source. The new CPDFViewRegistry singleton with weak refs, guard let reactTag = cpdfView.reactTag early-return, cpdfViewDetached cleanup, and explicit "view_not_found" rejection are all improvements — but they don't change the underlying mechanism. There's still no Fabric component spec / codegenConfig, the registration still depends on cpdfView.reactTag, and under iOS New Arch interop that value will still be nil/0 when cpdfViewAttached fires.
The 2.6.6 behaviour under iOS New Arch would be a clean Promise rejection instead of a hang — better UX, but the annotations still aren't saved.
Suggested fix directions
Any of the following would resolve it:
- Defer registration until the inner
RCTCPDFView has received its reactTag from interop (e.g. register on didMoveToWindow rather than during createCPDFView).
- Look up the view via the bridge rather than a local registry:
bridge.uiManager.view(forReactTag:) returns the outer interop wrapper, walk its subview tree to find the inner RCTCPDFView. This is the lowest-risk patch — we've validated it locally and can share the diff.
- Add a Fabric component spec (
codegenConfig in package.json + matching component source) so the library participates in Fabric natively rather than via the interop layer.
(1) and (2) are tactical. (3) is the strategically right fix and aligns with React Native's stated direction for the New Architecture.
Test plan once fixed
- Build a sample app with
RCT_NEW_ARCH_ENABLED=1, bridgelessEnabled=false.
- Render
<CPDFReaderView>, load a PDF.
- Verify the view manager's registry is populated with a non-zero, non-nil tag that matches what
findNodeHandle(viewRef) returns to JS.
- Verify
await pdfReaderRef.current.save() resolves with true after annotations are added.
- Verify
_pdfDocument.getDocumentPath(), _pdfDocument.getPageSize(...), _pdfDocument.renderPage(...) all resolve correctly.
Happy to share our minimal repro app, full Xcode console capture, and Podfile if useful. Will also be raising this via paid support — wanted to file here too so other users hitting this can find it.
Summary
On iOS, every JS→native command method on
<CPDFReaderView>hangs forever (or silently no-ops) under React Native New Architecture interop (RCT_NEW_ARCH_ENABLED=1, Fabric on, Bridgeless off).pdfReaderRef.current.save()is the most visible failure mode — its Promise never resolves and never rejects, leaving any UI that awaits it stuck. The same broken code path also affects_pdfDocument.getDocumentPath(),_pdfDocument.getPageSize(),_pdfDocument.renderPage(), and ~60 other view-manager methods. Android is not affected.Environment
@compdfkit_pdf_sdk/react_native@2.5.0(also reviewed 2.6.6 source — see "Note on 2.6.6" below)ComPDFKit (2.5.0),ComPDFKit_Tools (2.5.0)RCTCPDFReaderView(a legacyRCTViewManager) is hosted insideRCTLegacyViewManagerInteropComponentView.Steps to reproduce
RCT_NEW_ARCH_ENABLED=1andbridgelessEnabled=falseon iOS.<CPDFReaderView ref={pdfReaderRef} ... />.await pdfReaderRef.current.save().Expected: Promise resolves with
true.Actual: Promise never resolves and never rejects. JS hangs.
Root cause
RCTCPDFReaderView.cpdfViewAttached(_:)writes to a side-channel dictionary usingcpdfView.reactTag.intValueas the key. Under iOS New Arch interop, theRCTCPDFViewinstance returned byview()is hosted inside an interop wrapper that owns the React tag JS sees viafindNodeHandle(). The inner view'sreactTagproperty is nil/0 at the pointcpdfViewAttachedfires, so the dictionary write is meaningless — the dictionary stays empty (or holds only a0key that JS will never query).When JS calls e.g.
CPDFViewManager.save(tag):RCTDocumentManager.save(tag:resolve:reject:)callsreader.saveDocument(forCPDFViewTag: tag).RCTCPDFViewManager.saveDocument(forCPDFViewTag:completionHandler:)readscpdfViews[tag]— getsnil.rtcCPDFView?.saveDocument(...)short-circuits via optional chaining. The completion handler is silently discarded.resolve/rejectare never called. The JS promise hangs.Diagnostic logs (with temporary
NSLogadditions to confirm)Added
NSLogstatements toios/RCTDocumentManager.swift::saveandios/RCTCPDFViewManager.swift::saveDocument(forCPDFViewTag:). After loading a PDF, adding an annotation, and tapping save:Tag
3032is also the value on everyonChangeevent payload received by JS — confirmingfindNodeHandle()and event-bubbling targets are consistent. The only thing out of sync is the iOS-sidecpdfViewsregistry.Files involved
ios/RCTCPDFViewManager.swift:34-45—saveDocument(forCPDFViewTag:)readscpdfViews[tag]ios/RCTCPDFViewManager.swift:820-822—cpdfViewAttached(_:)writescpdfViews[cpdfView.reactTag.intValue]ios/RCTDocumentManager.swift::save(tag:resolve:reject:)— promise resolver that gets silently droppedios/RCTCPDFView.swift::createCPDFView()— call site fordelegate?.cpdfViewAttached(self)Why Android works
Android's
CPDFViewModule(and analogous Java code path) dispatches commands through standard view-by-reactTag lookup inUIManagerModulerather than a parallel side-channel dictionary. We've confirmed Android saves complete in ~30ms on the same build with the same JS code.Note on 2.6.6
I checked the v2.6.6 source. The new
CPDFViewRegistrysingleton with weak refs,guard let reactTag = cpdfView.reactTagearly-return,cpdfViewDetachedcleanup, and explicit"view_not_found"rejection are all improvements — but they don't change the underlying mechanism. There's still no Fabric component spec /codegenConfig, the registration still depends oncpdfView.reactTag, and under iOS New Arch interop that value will still be nil/0 whencpdfViewAttachedfires.The 2.6.6 behaviour under iOS New Arch would be a clean Promise rejection instead of a hang — better UX, but the annotations still aren't saved.
Suggested fix directions
Any of the following would resolve it:
RCTCPDFViewhas received its reactTag from interop (e.g. register ondidMoveToWindowrather than duringcreateCPDFView).bridge.uiManager.view(forReactTag:)returns the outer interop wrapper, walk its subview tree to find the innerRCTCPDFView. This is the lowest-risk patch — we've validated it locally and can share the diff.codegenConfiginpackage.json+ matching component source) so the library participates in Fabric natively rather than via the interop layer.(1) and (2) are tactical. (3) is the strategically right fix and aligns with React Native's stated direction for the New Architecture.
Test plan once fixed
RCT_NEW_ARCH_ENABLED=1,bridgelessEnabled=false.<CPDFReaderView>, load a PDF.findNodeHandle(viewRef)returns to JS.await pdfReaderRef.current.save()resolves withtrueafter annotations are added._pdfDocument.getDocumentPath(),_pdfDocument.getPageSize(...),_pdfDocument.renderPage(...)all resolve correctly.Happy to share our minimal repro app, full Xcode console capture, and
Podfileif useful. Will also be raising this via paid support — wanted to file here too so other users hitting this can find it.