Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Laminar Native for building mobile and desktop apps #138

Open
raquo opened this issue Mar 2, 2023 · 2 comments
Open

Laminar Native for building mobile and desktop apps #138

raquo opened this issue Mar 2, 2023 · 2 comments
Labels
design hard problem needs sponsor Pretty sure I can't do this unless someone wants to sponsor this work.

Comments

@raquo
Copy link
Owner

raquo commented Mar 2, 2023

What & Why

It would be nice to render Laminar components into platform-native UI widgets on iOS and Android platforms, similar to what you can do with React Native. Native UI elements feel better and perform better on mobile. I never thought that such advanced features could be on the table for Laminar, but I did some initial research on this, and it looks doable in principle.

That principle being – compiling Laminar components to React Native components. There is some prior art on this, for example Vue Native compiles to React Native. Some other libraries compile to NativeScript instead, e.g. NativeScript Vue and Svelte Native, but after quickly reviewing both React Native and Nativescript targets, I don't see any advantages to NativeScript that would outweight the massively larger community and ecosystem of React Native.

I think that React Native itself is the lowest level interface that we could target. That is, I think there is no alternative to JS DOM in React Native that we could target instead of targeting the higher level React Native itself. RN uses Yoga layout engine, but that does not seem usable directly from JS. Anyway, I think targeting React Native is overall the best choice due to good ecosystem support – all React Native plugins / addons / frameworks / etc. seem to require React Native rather than some lower level abstraction, as far as I can tell.

I was under the impression that React Native is only for building mobile apps, because it uses a special set of elements and properties, including style properties – it does not use HTML and CSS. However, it turns out you can render React Native apps into HTML+CSS+JS with react-native-web. Microsoft has also created native renderers for Windows and Mac, but perhaps predictably, the Mac one seems to be lagging behind in support. And there's more. Bottom line is, you can build native mobile + web + desktop apps all with one React Native codebase.

The most feasible alternative to React Native is using Cordova or Capacitor. Since these solutions render the HTML+CSS+JS UI in the app's built-in browser, Scala.js & Laminar already work fine there. However, these tools are limited by performance (example), and realistically I think such web-tech apps will be second-class on mobile for as long as Apple and Google hold their iron grip on their app stores. In practice, I saw quite a few companies switch from Cordova to React Native – always for performance reasons – yet I can't recall even a single notable case of a company switching away from React Native to Cordova / Capacitor, if they do switch away, it's towards platform-specific native development, which is simply too expensive for many small companies and individuals.

How

Laminar's direct DOM updates driven by observables are pretty much the opposite to the virtual DOM approach. I've written about that in more detail, and my Laminar video discusses the practical aspects of integrating Laminar with React.js web apps.

The very first version of Laminar actually used virtual dom (snabbdom), so I have first hand experience trying to wrangle Laminar's API into that paradigm. It is not a perfect fit, but it's certainly possible. Normally I would be against this, because there's nothing to gain from virtual dom when you have observables, however in this case, virtual dom is the only feasible target for building cross platform native UIs on mobile, so the tradeoffs might be worth it.

Specifically, I think I would need to create a new "bundle" in Laminar, api.N in addition to api.L, which would contain all of the React Native APIs. The model would basically be the same as regular Laminar – tags, attributes, events, style props.

Our ReactiveElement class it not suitable for virtual DOM, so I would need to implement a whole new NativeElement class. The API and usage patterns would need to change a bit, for example you might lose access to .ref, you will not need to call split, etc. – but the high level idea of using modifiers, and updating props and children via observables would remain. The new NativeElement would be doing all the heavy lifting, translating Laminar-style inputs into virtual dom updates for React Native.

Translating to virtual dom would be less efficient than updating the DOM directly, but that's not really an option for native mobile apps, so the comparison is moot. Moreover, the gains in efficiency from using platform-native components will probably outweight the extra work that NativeElement would need to do. Especially so because in React Native, all our JS code is running is a separate thread, it's not blocking the UI (whereas in the browser, JS execution is blocking UI updates).

Targeting React Native has another significant advantage – this potentially lets us use all of the React Native components and frameworks out there. Unlike the web, where you can use low-level dependencies that don't require React.js, on mobile all the cross platform libraries target React Native (or another toolkit like Cordova / Capacitor) – there are no libraries that target the "native mobile DOM" because there is no such thing – iOS has its own UI toolkit, and Android has its own, and they don't natively interop with JS the way React Native does.

I think Laminar's standard Web Components pattern will work well for using React Native components. Although, perhaps I can come up with a better pattern using Airstream Val-s to represent constant props.

So, what needs to be done:

  1. Add all the React Native types to Scala DOM Types (or make a separate project).

    This will also help other Scala.js UI libraries build React Native interfaces.

  2. Develop a NativeElement class that translates Laminar API to React Native virtual DOM updates.

    This is the hardest part. I've done this before for snabbdom, and it was not trivial. React Native virtual DOM has different hooks, but probably requires pretty similar calls. I have a lot more experience with UI library internals today than six years ago, so hopefully that will help too.

  3. Develop good patterns to integrate third party React Native components.

If you have experience with React Native or similar technologies, your feedback on this plan would be very much appreciated. My expertise is mostly in the web domain, and although I have built native mobile apps for iOS before, that was many years ago.

When & Whether

With this feature, I'm in a very frustrating position where I can see a reasonable way to build it, but I can also see that it would require several weeks of full time work. With the limited time that I have available for open source, I don't see myself being able to tackle this. If your company is interested in fully or partially funding this work, please get in touch: [email protected]

Any feedback or ideas for implementation are welcome, of course.

@raquo
Copy link
Owner Author

raquo commented Mar 2, 2023

Some relevant resources from Discord discussion with @armanbilge:

Fabric virtual dom api: https://github.com/facebook/react-native/blob/b044ece59475c251bcf13bac0652f72d901e1c88/Libraries/ReactNative/FabricUIManager.js#L27-L32

Direct "DOM" updates with setNativeProps: https://reactnative.dev/docs/direct-manipulation

"DOM" rendering architecture: https://reactnative.dev/architecture/render-pipeline

ReactNativeHostConfig: https://github.com/facebook/react/blob/d49e0e0be0941490fe709f80de137516ba4c0ee3/packages/react-native-renderer/src/ReactNativeHostConfig.js#L263-L267

  • It calls into low level UIManager.updateView / manageChildren methods, but it's a pretty thin wrapper over them, doing minor diffing calculations. I guess we don't need those calculations for Laminar API, but on the other hand as Arman said, if we target the HostConfig API, we could potentially target... other renderers written to that same API? I think https://github.com/necolas/react-native-web might be one of those, but not sure yet how it's implemented.

@raquo
Copy link
Owner Author

raquo commented Aug 21, 2023

Note to self – one other uncertainty is the ability of Hermes to run Scala.js code. Seems doable but has some limitations apparently. Haven't looked into it myself. Discussion in scala-js discord: https://discord.com/channels/632150470000902164/635668814956068864/1143118419479236719

@raquo raquo added needs sponsor Pretty sure I can't do this unless someone wants to sponsor this work. needs design The solution is not clear, or I am not very happy with it and removed needs design The solution is not clear, or I am not very happy with it labels Jan 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design hard problem needs sponsor Pretty sure I can't do this unless someone wants to sponsor this work.
Projects
None yet
Development

No branches or pull requests

1 participant