question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

RFC: React Cross-Platform Strategy

See original GitHub issue

to: @necolas

This is mostly in response to #31, but also just something I’ve been thinking about for some time, and is a proposal for a path to building a real production-quality ecosystem for building UI with React across multiple platforms in a single code base.

At the moment this isn’t very easy, and there’s no standard or agreed upon way to do it. React Native has been the closest thing to accomplishing this, with the two platforms being iOS and Android platforms (and others in the community sprouting up, such as windows, vr, etc.). Projects like react-primitives and react-native-web are attempting to solve this.

I think it’s important to come at this with the angle of it not being just “React Native, but running in a web browser”. Similarly, I think it’s important to come at this thinking more than just “web” and “native”, even if we don’t know what all of the additional platforms will be. (Though we can make some educated guesses… Windows, VR, desktop, etc.)

I believe there are three main tenets that are needed to accomplish this:

Three Main Tenets

Tenet 1: Platform Extensions

Platform extensions, such as the ones allowed by the React Native Packager, are crucial to creating cross-platform code-bases. It allows for people to create consistent interfaces with different implementations, and make that transparent to the end user.

I believe we should take this convention a step further by creating a webpack resolver plugin that allows for the exact same resolution rules. Additional plugins for other bundlers such as rollup and browserify would also be ideal.

In addition, I’d like to standardize on a module-scoped package.json directive: platformCascade

In an NPM module’s package.json, a library author would be able to describe the proper cascade of platform extensions, and it would be scoped to every file inside of the package, but not in any dependencies. An example platformCascade could be defined in this way:

"platformCascade": {
  "ios": ["ios", "native"],
  "android": ["android", "native"],
  "web": ["web"],
  "sketch": ["sketch"]
},

This allows library authors to define cascades that may not be mainstream (i.e., “sketch”), without it having to be standardized (and without causing unknown side-effects in other modules if that extension just happened to be there).

It is critical that this convention works outside of just the RN packager, as it will be required for this ecosystem to extend beyond just RN and the associated tooling.

Tenet 2: Primitives

In order for cross-platform to really work, we need to make a world where if someone wants to build a cross-platform UI component, they don’t have to implement it n times for n platforms. This requires at least a few building block components.

  1. StyleSheet: Layout is critical to building dynamic UIs. There are lots of options for layout algorithms, but flex box makes a compelling choice. Yoga is a cross-platform flex box implementation that will likely be applicable to many platforms, and the algorithm is already available in all major web browsers (though with some problems still remaining). React Native’s StyleSheet implementation is quite nice to work with, and works extremely well with React.
  2. View: Basic rectangular view. Important for this to take into account layout (through style) as well as accessibility attributes, and the proper event hooks. The API that RN has chosen for view seems to do a pretty good job, and I suspect this is in large part due to the fact that RN was built with cross-platform in mind.
  3. Text: Laying out text is vital to building UI, obviously.
  4. Image: Sometimes you need to display Images in your UI.
  5. Animated: It’s important to make UIs dynamic and interactive. Animation is a critical component to this. The Animated API from React Native is a perfect candidate for this. It is declarative, which makes it easier to decouple the API surface area from platform-specific implementation. It is also already has an implementation completely implemented in JavaScript that would work by default, while other “native” implementations could also be implemented with some effort w/ the same APIs.
  6. Touchable: Touching/Pressing ends up being a fairly basic and essential form of interaction.
  7. Platform: This is less of a primitive as much as it’s a utility. Pragmatically speaking, it’s often needed to do small statement-level switches between different platforms to configure things differently based on the underlying implementations.

We want to be careful in choosing these “primitives”. We need enough to be able to build bigger more complex things, but need to keep it minimal enough so that it’s reasonable to expect a new platform to be able to implement the full set of them. I believe things like TextInput may be really important in terms of some platforms, but not others, and so should maybe not be a primitive. This will have to be defined over time, but I think being conservative at first is ideal.

To be honest, I’m actually not sure if I’m even confident about the primitives listed above. For instance, if we wanted to add a platform like “console” and create something like react-curses, it’s not clear if Image or Touchable actually make sense. These components were mainly chosen because after creating a fairly complete Component Library for Airbnb in React Native, I noticed that almost 95% of it could be implemented with just these primitives. The four that seem really clear to me are StyleSheet, View, Text, and Platform, but it’s a balance.

Tenet 3: Platform built on Primitives

With the “primitives” now built, now it is our job to figure out what useful crap we can build with them! The idea behind the primitives is that they are the building blocks that other more useful things can be built. Perhaps mediocre versions of things that are general and purely implemented by primitives.

Some components will need to have custom platform-specific implementations that will ensure that they perform as good as they possibly can, but we must always have at least one implementation that only uses the primitives (or other higher-order components that themselves have a primitives-only implementation).

There’s some obvious candidates for these components. Since we want to be able to share code between React Native and Web, most of the non-platform-specific APIs exported by React Native make obvious choices, but there are obviously lots more.

Proposal

I haven’t really proposed anything yet, as much as I’ve just talked about what I think is important. Here comes the proposal. Please keep in mind that I’m hoping this just starts the conversation. More than anything I’m interested in feedback and seeing if my ideas align with yours.

At the end of the day, I think there’d be a lot of value in us working together, and this is one way I could see that happening while (i hope) serving each of our slightly different use-cases.

The first proposal would be to move/rename necolas/react-native-web to react-community/react-platform. Alternatively, we could deprecate it and start fresh.

react-platform would become a lerna monorepo. One of the packages in the repo would be react-primitives which would include the primitives mentioned above, with the optimal implementation (whether that’s RNW’s current implementation, or react-primitive’s, or some combination of both). The lelandrichardson/react-primitives repo would also be deprecated.

Various modules that have been written in react-native-web would be refactored to be their own package in the lerna monorepo. For instance, ScrollView and ListView would become react-platform-scrollview and react-platform-listview.

A convention I could see working for this monorepo is a folder structure like this:

- react-platform             // repo root directory
  - packages
    - {package-name}
      - {platform-name}
        {file}.js            // clear that files here are meant for {platform}
    {file}.{platform}.js     // optimized version for {platform}
    {file}.js                // fully cross-platform implementation

We could also export a “grab-bag” package which could just be react-platform, which essentially be what react-native-web currently exports (something that attempts to be API-compatible with RN). Of course, you could also keep react-native-web and just depend on all of the corresponding react-platform-* packages or whatever was needed and export them directly (which may actually make more sense?).

Questions & Answers

What packages belong in the react-platform monorepo?

Of course the whole point of this is for any library authors to also be able to publish cross-platform components/modules by only depending on react-primitives and other fully cross-plat dependencies. So packages don’t need to be part of the react-platform monorepo in order to be a primitive.

My thinking is that by something being in the monorepo, we are asserting that no matter what platform you are on (i.e., could be something obscure like “sketch”), we are guaranteeing that there will be at least one primitives-only implementation (functional, even if it’s not optimal). Of course we will also have more optimized implementations in the case of the more popular platforms (namely, “native” and “web”).

How would new platforms get introduced?

I’m still not 100% sure what the right answer here is. If we lived in a world where there were ~15 platforms and there was source code for each in this monorepo, the maintenance burden could start to get out of hand. On the other hand, the idea should be that a new platform need only implement the primitives, and everything else should “just work” (even if using the non-optimal cross-platform implementations). My feeling is that the criteria ought to be something like:

  1. An implementation for every primitive is reasonably complete
  2. Someone is willing to be a point of contact for maintenance / issues / etc.

Does it have to look like React Native’s API?

No, but it doesn’t hurt if we follow an already existing API, and React Native’s make the most sense, as most of them have already been built with cross-platform interfaces in mind.

Will this be maintainable?

Happo is a CI tool built to detect visual regressions by taking screenshots and diffing them. I’ve been working on getting happo working for iOS, Android, and Sketch as well.

I believe we should be able to create a bunch of minimal examples with the primitives stressing all of the corner cases of layout as well as styling (box shadows, borders, border radius, transforms, etc.).

We then get two things out of this:

  1. We are able to detect visual regressions of the primitives and other components of the platform.
  2. We are able to directly compare all of the platforms in a straightforward way.

This, in addition to standard stuff like tests, listing, etc. I think will give us a pretty good story in terms of maintenance.

Long Term

React Native

Although this wouldn’t have to be an initial goal, I could eventually see implementations of various react native APIs moved into the react-platform repo instead of the react-native one. We have already talked about making RN core more modular and moving it into a similar repo structure. It might make sense for some of these things (like ListView, for example) to live outside of ReactNative as a lot of their optimizations are JS-level optimizations and could benefit all platforms.

Flexbox / Yoga

I know that Sebastian Markbage has been thinking about integrating layout into react for a long time. I think this would bode very well for the concept of “primitives” and also ensure that different platforms would be more consistent. The main thing each platform would then have to provide would be a text measurement API. I’d like to add an emscripten pipeline to Yogo so that there is an optimized and consistent JS implementation of flex box that matches the native implementations. You could also see Web Assembly as a target for Yoga in the long term as well.

Adoption

Imagine if today’s most popular react-* and react-native-* libraries depended on react-primitives or react-platform-* packages instead of react-native or “host” DOM components like <div> and <span>. Entirely new platforms could be born overnight by just implementing the primitives, and entire code bases could be made to target those platforms with just the changing the “platform” target of your JS bundler, and potentially implementing a couple of platform-specific interfaces where needed.


I look forward to your thoughts, and hope that we can find a time to talk about this more seriously if you are interested (perhaps next React Wednesday?)

Issue Analytics

  • State:open
  • Created 7 years ago
  • Reactions:106
  • Comments:24 (3 by maintainers)

github_iconTop GitHub Comments

12reactions
necolascommented, Jan 5, 2017

I first read ReactNative.createDOMElement feels weird to me

It would probably feel less weird if React Native had been called something like React Platform: ReactPlatform.createDOMElement / ReactPlatform.requireNativeModule. But I’m open to createDOMElement being called something else or the end result being achieved with a different API.

So for a component like Checkbox, the generic version would be built from View but the web version would use createDOMElement?

Well, there would only be one Checkbox and each platform might have a custom implementation. If some platforms can get away with a pure JS implementation using View, then they should. But if the implementation needs access to platform-specific APIs or components (for performance, ease, accessibility, etc.) then we use the escape hatches to do that.

The long-term objective / hypothesis is that we’d use the various escape hatches as infrequently as possible, for low-level building blocks, so that the rest of the ecosystem above is built on a unified, platform-agnostic, JavaScript-only API. But at the moment even RN has a lot of iOS/Android-specific deviations exposed in the APIs of its core components and features 😃

11reactions
ericvicenticommented, Jan 4, 2017

I’m not sure I fully understand how these principals apply to things like Touch ID which aren’t web APIs. I know you just picked it as an example, but I’m not sure what the right way to share that would be.

I specifically picked that example because it isn’t a web API yet. Basically I’m suggesting that the JS app should check for presence of native features and opts in to them when available. Instead of specifying behavior for each platform, an app would specify behavior for cases where different native features are available.

Another practical example may be the camera. If I adopt a camera library in my app, I should check with the library to determine if there is a camera on the device, rather than assuming certain platforms have cameras. And I should check with the library to see if the camera has a LED, to determine if the flash buttons should appear. The standards web API may not support flash settings, but if my app checks with the library, then the library can later add LED support for new environments, even though the standards support lags behind.

How do you envision “encourage feature detection” being pulled into this proposal? Are you envisioning this as something instead of the Platform API?

I think the platform API should be available as an escape hatch for when you absolutely need to override based on the platform. This should be used when the product requirements are different, designs are different, or in cases where probing the native modules for feature detection causes instability.

Maybe the three tenets can be reworked as such:

  1. Build a lowest-common-denominator implementation with primitives
  2. Use feature detection where possible to utilize native features, regardless of platform
  3. Fork based on platform (and use platform extensions) as a fallback, or for product reasons
Read more comments on GitHub >

github_iconTop Results From Across the Web

Flutter vs React Native – What to choose in 2023?
Flutter enables cross-platform app development. It gives developers an easy way to build and deploy visually attractive, natively-compiled ...
Read more >
Introducing the React RFC Process – React Blog
Inspired by Yarn, Ember, and Rust, the goal is to allow React core team members and community members to collaborate on the design...
Read more >
RFC: File System-Based Native Routing with Expo and React ...
Since April 2021, I've been working on a new cross-platform router that generates nested navigation and deep links based entirely on the ...
Read more >
How To Build Cross Platform Apps Using React, React Native ...
This allows users to access the same application using different devices. In this tutorial you will learn how to create cross platform apps...
Read more >
This Week In React #103: Contentlayer, Remotion 3, Stale Closure ...
This Week In React #103: Contentlayer, Remotion 3, Stale Closure, Cross-platform, Jest 28, Hydration, Netlify Edge Functions.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found