How to Extend React Native APIs for Windows
React Native can be a great option when wanting to start a cross-platform project, which is why at PSPDFKit, we wanted to ensure that our PDF framework supports React Native with little fuss. You may know that React Native can be used to develop for Android and iOS, but did you know that it also supports Windows? As a result, naturally we decided to extend PSPDFKit for React Native to also support Windows!
In our native SDKs, we expose a lot of APIs for full customization, but we only use a subset of those APIs in our React Native library. In this article, we’ll show you how to bring native Windows APIs to React Native to make it easier for everyone to extend our Windows SDK to React Native or to expose native Windows code to React Native in general.
ℹ️ Note: If you’re looking into extending our iOS or Android SDK APIs to React Native, please take a look at our How to Extend React Native APIs for iOS and Advanced Techniques for React Native UI Components for Android blog posts.
Native UI Components
When extending PSPDFKit for React Native, you’ll mostly be extending the UI component of PDFView
. The PDFView
will be responsible for showing a PDF and general tools to manipulate the PDF.
Props
Props are parameters that allow you to customize your UI components. The following example shows how to extend the library to open a document on a specific page index.
JavaScript Implementation
First, we define the prop in index.windows.js
, which is where PSPDFKitView
, our React Native UI component, is defined:
class PSPDFKitView extends React.Component { _nextRequestId = 1; _requestMap = new Map(); render() { return <RCTPSPDFKitView ref="pdfView" {...this.props} />; } } PSPDFKitView.propTypes = { /** * Page index of the document that will be shown. */ pageIndex: PropTypes.number, };
Native C# Implementation
We then expose a method in PSPDFKitViewManager
to handle the defined prop. Within the method, we can reference both the view that’s being manipulated (PDFViewPage
) and the prop that’s being passed in. We also have to ensure that the prop type matches the parameter type. For example, PropTypes.number
is an int
:
[ReactProp("pageIndex")] public async void SetPageIndexAsync(PDFViewPage view, int pageIndex) { await view.SetPageIndexAsync(pageIndex); }
Usage of the Exposed Prop
Once you’ve completed the above, this is how to use the newly added prop in your JavaScript code:
class ManualSave extends React.Component { render() { return ( <View style={{ flex: 1 }}> <PSPDFKitView ref="pdfView" style={styles.pdfView} document="ms-appx:///Assets/pdf/annualReport.pdf" pageIndex="1" /> </View> ); } }
Methods
It’s possible to add a method directly to the PSPDFKitView
, which will allow us to call directly through to C#. On top of this, we can also set up a system that handles a Promise
in JavaScript to make an intuitive interface in React Native. Implementing a Promise
is advisable in most cases, as doing so provides the writer with both an indication of whether or not the operation was successful and a more native way of handling errors in JavaScript.
Native C# Implementation
There are two steps to implementing the native side of a method call to the view.
First, we want to create a mapping in which the JavaScript index.windows.js
can call. This is done by overriding the ViewCommandsMap
of the PSPDFKitViewManager
:
private const int COMMAND_ENTER_ANNOTATION_CREATION_MODE = 1; public override JObject ViewCommandsMap => new JObject { { "enterAnnotationCreationMode", COMMAND_ENTER_ANNOTATION_CREATION_MODE } };
Now when we call enterAnnotationCreationMode
, this will map to a const int
, which we can use next.
Then, we need to use the mapping created above to decide which code path to take. When the JavaScript calls, it’ll call through to a single location, ReceiveCommand
, which we need to override in PSPDFKitViewManager
. Within this, we switch based upon the unique integer passed in where the mapping was created above:
public override async void ReceiveCommand(PDFViewPage view, int commandId, JArray args) { switch (commandId) { case COMMAND_ENTER_ANNOTATION_CREATION_MODE: var requestId = args[0].Value<int>(); try { await Pdfview.Controller.SetInteractionModeAsync(InteractionMode.Note); this.GetReactContext().GetNativeModule<UIManagerModule>() .EventDispatcher .DispatchEvent( new PdfViewOperationResult(this.GetTag(), requestId) ); } catch (Exception e) { this.GetReactContext().GetNativeModule<UIManagerModule>() .EventDispatcher .DispatchEvent( new PdfViewOperationResult(this.GetTag(), requestId, e.Message) ); } break; } }
Arguments are passed as a JSON array which can be parsed and used if necessary. The first argument in the example above is an integer that provides the mapping back to a Promise
in JavaScript. We use this ID to send an event to handle the Promise
. Please see the Events and Callbacks section below for more information.
JavaScript Implementation
We then need to create a method for the PSPDFKitView
class held in index.windows.js
. This method will call through to the native code with the exposed command from the previous step:
enterAnnotationCreationMode() { let requestId = this._nextRequestId++; let requestMap = this._requestMap; // We create a `Promise` here that will be resolved once `onDataReturned` is called. let promise = new Promise((resolve, reject) => { requestMap[requestId] = {resolve: resolve, reject: reject}; }); // This calls through to C# with the command exposed as `enterAnnotationCreationMode`. UIManager.dispatchViewManagerCommand( findNodeHandle(this.refs.pdfView), UIManager.RCTPSPDFKitView.Commands.enterAnnotationCreationMode, [requestId] ); return promise; }
In the example above, you can see that we’re setting up a Promise
to return. Within index.windows.js
, we’re keeping a mapping to ensure the correct Promise
is called from C# in the previous step. The ID for this Promise
is passed in as requestId
.
Usage of Exposed Method
class AnnotationMode extends React.Component { render() { return ( <View style={{ flex: 1 }}> <PSPDFKitView ref="pdfView" style={styles.pdfView} document="ms-appx:///Assets/pdf/annualReport.pdf"/> </View> <Button onPress={() => { this.refs.pdfView.enterAnnotationCreationMode().then(() => { alert("successful"); }).catch(error => { alert(error); }); }} title="Create Annotation" /> ); } }
Events and Callbacks
Events, or callbacks, allow you to be notified in JavaScript when something occurs in native C# code. Take a look at the official documentation for more information and the React Native Windows documentation for Windows-specific implementations.
Events should be prefixed with “on,” so in our case, we’ve implemented onAnnotationsChanged
, which is called when annotations change. In PSPDFKit for Windows, we have AnnotationsCreated
, AnnotationsUpdated
, and AnnotationsDeleted
, all of which we can use for mapping to JavaScript.
Again, we’ll start in index.windows.js
and define the events in JavaScript:
class PSPDFKitView extends React.Component { render() { return ( <RCTPSPDFKitView ref="pdfView" {...this.props} onAnnotationsChanged={this._onAnnotationsChanged} /> ); } _onAnnotationsChanged = (event) => { if (this.props.onAnnotationsChanged) { this.props.onAnnotationsChanged(event.nativeEvent); } }; }
Now we need to create a mapping to code. Similar to methods called from the PSPDFKitView
, we also have a mapping to events:
public override JObject CustomDirectEventTypeConstants => new JObject { { PdfViewAnnotationChangedEvent.EVENT_NAME, new JObject { {"registrationName", "onAnnotationsChanged"}, } } };
As you can see, there’s a new class here named PdfViewAnnotationChangedEvent
, which is extended from ReactNative.UIManager.Event
and describes the event in JSON:
class PdfViewAnnotationChangedEvent : Event { public const string EVENT_NAME = "pdfViewAnnotationChanged"; public const string EVENT_TYPE_CHANGED = "changed"; public const string EVENT_TYPE_ADDED = "added"; public const string EVENT_TYPE_REMOVED = "removed"; private readonly string _eventType; private readonly IAnnotation _annotation; public PdfViewAnnotationChangedEvent(int viewId, string eventType, IAnnotation annotation) : base(viewId) { this._eventType = eventType; this._annotation = annotation; } public override string EventName => EVENT_NAME; public override void Dispatch(RCTEventEmitter rctEventEmitter) { var eventData = new JObject { { "change", _eventType }, { "annotations", JObject.Parse(_annotation.ToJson().Stringify()) } }; rctEventEmitter.receiveEvent(ViewTag, EventName, eventData); } }
We can then use PSPDFKit for Windows events to drive React events:
PDFView.OnDocumentOpened += (pdfView, document) => { document.AnnotationsCreated += DocumentOnAnnotationsCreated; document.AnnotationsUpdated += DocumentOnAnnotationsUpdated; document.AnnotationsDeleted += DocumentOnAnnotationsDeleted; };
private void DocumentOnAnnotationsCreated(object sender, IList<IAnnotation> annotationList) { foreach (var annotation in annotationList) { this.GetReactContext().GetNativeModule<UIManagerModule>().EventDispatcher.DispatchEvent( new PdfViewAnnotationChangedEvent(this.GetTag(), PdfViewAnnotationChangedEvent.EVENT_TYPE_ADDED, annotation) ); } }
Usage of Exposed Events
Now that we have everything set up, we can use the event in JavaScript:
class PdfViewListeners extends Component { render() { return ( <View style={{ flex: 1 }}> <PSPDFKitView document="ms-appx:///Assets/pdf/annualReport.pdf" onAnnotationsChanged={(change) => { alert('Annotations changed\n' + JSON.stringify(change)); }} /> </View> ); } }
Native Modules
Native modules are often used when you want to interact with native code that isn’t related to a view. A great example of this in the PSPDFKit React Native library is library search. We can set up a library and perform a search over assets or a user-defined folder, all without having a view.
This post won’t go into native modules, but feel free to read up on the React Native Windows documentation and refer to our React Native Library example.
Customizing the UI Using Native Code
Sometimes, making native APIs available to React Native doesn’t really make sense for a specific use case — for example, rather than just setting the simple colors of the PdfView
, maybe you want to implement complex CSS customization. In such a case, it could be a great idea to have a static CSS file you set on your native PdfView
, thereby reducing development time for a simple non-dynamic code change.
The change would be a simple case of adding a CSS asset and then referencing it in the PdfViewPage
:
<ui:PdfView License="{StaticResource PSPDFKitLicense}" Name="PDFView" InitializationCompletedHandler="PDFView_InitializationCompletedHandlerAsync" Css="ms-appx-web:///Assets/css/my-pspdfkit.css"/>
Conclusion
In this blog post, you learned how to expose native PSPDFKit for Windows APIs to React Native. Exposing native APIs in this way will create a very powerful yet simple React Native plugin for many of your PDF needs. If you come across any missing APIs that you think would be useful to have or if you have any questions about PSPDFKit for React Native, feel free to reach out to us. We’re happy to help.
When Nick started tinkering with guitar effects pedals, he didn’t realize it’d take him all the way to a career in software. He has worked on products that communicate with space, blast Metallica to packed stadiums, and enable millions to use documents through Nutrient, but in his personal life, he enjoys the simplicity of running in the mountains.