🦥 :TLDR:
A bit of background to understand why the hell do we handle text editing this way on mobile...
For start, we are using React Native (RN) and there are not much solutions out there.. Does that settle it ? 😅 You can spot some around the web, like this, or this, or even this (made by Wix !), but as you can see, they are either unused (ranging from 14 to a good 800 starts...), and mostly unmaintained (Wix lib hasn't been touched since 5 years...). And even if they were actually maintained, we'd hit a next issue: making sure the lib is compatible with our current stack (collab, OT JSON system, etc.).
So a React Native lib doesn't seem to be an option..
!! "Why not making ours ?" You may ask. !! !! Well this actually crossed our mind, but this boils down to 2 options: !! - Create a full native solution, wrapping some editor SDK on Android and iOS, ensuring a full custom and performant solution → loads of resources + loads of complexity = 🙅 !! - React-native solution, basically reproducing the behaviour of an editor (selection, rich text rendering, keyboard interaction, etc..) → complexity + time + no guarantee about performance = 🚫 !! !! Let's move on..
Luckily editor libs and the necessity of writing on some React Native app isn't new.. The most common solution: a Webview "simply" embedding the code of the editor (Slatejs, Draftjs, etc.), and displayed in a React Native app. A Webview is common component in React Native, and the code of the editor is most probably existing if you already use it in you webapp, so you plug it together and..
Problem solved ?! Yes, although it brings it's share of tradeoff...
First and if it isn't clear yet, a Webview is basically the RN equivalent of an iframe
on the web. Give it a url or some html, and it will display it nicely in secure sandbox, having its one context.
It's not a component to which we could simply pass props, so what if we do need to pass data to this Webview ? Here comes the messaging system.
Since a Webview is basically an iframe
, it has the same messaging mechanism to interact with its container. The container and the Webview can both listen to string messages and post messages to each other.
The Webview needs a document id ? RN will postMessage
. RN needs to know the height of the content ? The Webview will postMessage
the height of its document
. More complex: the Webview needs to navigate to an other doc (when clicking on a doc link) ? It will postMessage
a message to instruct RN to use its navigation system and show a new screen displaying the clicked doc.
So RN and Webview needs to exchange data or action, with potentially some parameters. Let's use JSON, holding a type
and some data
, then parse into string and send it, then unparsed on the other side, and use a a gigantic switch/case
to finally trigger an action depending on the type
and data
passed...
Well, that sums it up ! And although it can seem overwhelming, it unlocks key advantages:
So even though this messaging system can seem a hack of a solution at first, it all makes sens, and still represent the best compromise so far. Now if this solution is to stay around for a while, it's worth investing time to make it more maintainable and readable on the long term !
This messaging system between RN and Webview is here to stay. But let's be honest, on the long term, relying on a full string exchange, and a such a loose format (type
, data
), is at best... insane. On each side, we can only guess what we'll receive, or what we can send, and each time we add/remove some new data, it's up to us to make sure the other side is ready to handle it..
How to make it more reliable ? Enters typing.
Although typing has exists for a while in other languages, JS got to fully harness its power since Typescript took off. For us it represents a very simple and interesting solution to our issues:
Time for an example ! Let's define a message interface to describe the communication in a basic use case.
interface DefaultMessages {
FromApp: {
setWebviewConfig: {
isAndroid: boolean
apiToken: string | undefined
}
}
FromWebview: {
webviewHeight: {
height: number
}
noteLinkPress: { noteId: string }
consoleLog: { logMessages: any[] }
reloadWebview: {}
}
}
A messages interface will always list the possible actions or messages than can be sent from the RN part (thus received by the Webview), and sent from the Webview (thus received by RN). An action is defined by its key
(e.g: setWebviewConfig
) and the parameters we can pass along (e.g: isAndroid
, apiToken
).
We can now improve the onMessage
and postMessage
functions used to exchange messages between the two entities, and type them using the previous interface ! I'll spare you some typing shenanigan used to fit this common interface into the two different functions, for the two different entities (it implies some hooks, generic types and type transformations... Have a look at the code here if you're interested !), but the result for the developer is something like this:
Sending a message from Webview
Handling a message in React Native
As long as RN and the Webview agree on the message interface to follow, both knows what they can exchange and what to expect !
Voila 🎉
A complex solution doesn't always mean a bad solution, and sometime it is the best compromise we can get out of a panel of solution. In our case, with React Native, limited ressource, and reusable code, this complex system definitely became the smartest choice.
Now, complex doesn't need to be hard to maintain or unreliable. Thanks to a strong typing, we manage to enforce interfaces and make sure the communication between the 2 parts, are both readable and predictable !
As always everything is subject to changes. If you have heard of other ways, seen better techs, or have ideas on how to improve this system, let us know 🙂