lelandrichardson / react-primitives

Primitive React Interfaces Across Targets

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Use react-native-web

necolas opened this issue · comments

This package largely copy-pastes or reimplements react-native-web for the web implementation. You could simply depend on react-native-web and contribute any adjustments required. That project also has better test coverage and more correct implementations. Let me know if you'd like to discuss further

Hey Nicolas,

Thanks for creating this issue and starting the discussion. I certainly would like to discuss further, and think that any collaboration we can do would be a good thing. Please keep in mind that react-primitives is a personal project for me, is not currently used by Airbnb for web purposes, and is most interesting to me at an academic level (though I do believe has a lot of practical value in the medium to long term). No one (that I know of) is depending on react primitives in production, and right now I have some renewed interest in this project because of another platform I am working on as a backend for it (sketch).

Sometimes, "forking" is required to try out new things, and better understand problems. A lot of the iteration I've been doing on the StyleSheet implementation, performance improvements, and flexbox fallbacks could not be done as easily with a project that people depend on like react-native-web. Also, the goals of the project are arguably different (creating a web equivalent of all of react native versus creating a minimal common interface for creating UI on different platforms).

I view a project like react-primitives as needed for a couple of reasons. A very rushed explanation of the core philosophy is something like this, though I'd like to write more on the topic later:

JavaScript is becoming a very realistic option to write cross-platform code. JavaScript libraries that don't work with DOM APIs (like pure business logic, redux, math libraries, lodash, etc.) all work great in this realm, but UI libraries have always been a problem, and a good chunk of code for an application touches this in one way or another, so sharing entire applications becomes very difficult. React of course challenges this and makes it such that actually most of our UI related code may not need to touch DOM API's at all. React Native made this vision a lot more clear, and also came with one more gem: the handling of platform-specific file extensions. Platform specific file extensions allow for an application's dependency graph to split in small areas, and come back, defining common interfaces with different implementations.

I view react-primitives as the minimal set of components that most platform-inspecific UIs can be built from. This would go beyond just ios/android/web (see sketch as one such crazy example). My goal is for library authors to be able to depend on react-primitives instead of react-dom directly, and then be guaranteed to be building cross-platform things so long as their dependencies never go beyond that. For that reason, I think it's crucial that the naming of the project indicates that this isn't a "react native" thing, and this isn't a "web" thing. The fact that the interface is pretty similar to react native's core components is just a sane choice, but not a necessity.

As a result, I think there are three possible end states for the two projects:

  1. react-primitives depends on react-native-web.
  2. react-native-web depends on react-primitives.
  3. neither libraries depend on each other.

You are proposing option 1, which is pretty reasonable. RP already depends on react-native, and react-sketchapp, and RNW wouldn't be all that dissimilar. At that point, RP just becomes the "name" of the common interface for each platform. The concerns I would have for this approach are as follows:

Performance: Some rough benchmarks of RP vs RNW seemed to indicate that RP was ~30x faster for most benchmarks I tried. I don't doubt that some of this comes at the cost of correctness, and I'm working on cleaning up some of those edge cases (like deoptimizing when an inline style overrides a registered style). I think performance for RNW or RP will be critical in convincing people that it's a reasonable platform to build on, so I don't think there is anything as important as this right now.

Size: The web platform is unique in terms of how important bundle size will be. I eventually would want RP to be something that component library authors don't think twice about depending on (as opposed to explicitly or implicitly depending on react-dom, for instance). In order for this to happen, bundle size needs to be monitored, and this means not relying on all of react-native web. I think you've mentioned before that importing a core build of RNW for this purpose could be done. I think that's reasonable. The concern there might be that the RNW package is going through lots and lots of breaking changes to non-core APIs, and that in turn huts the compatibility matrix of libraries depending on RP.

I think option 2 also does make some sense. In many ways, RNW could be seen as a superset of RP, and having a big library depend on the smaller library in this case makes sense. This seems like a bit of a harder pill to swallow at this stage, considering RNW is a much more popular library than RP (which doesn't really have any consumers at this point). This is a good way to have minimal duplicated work, while keeping the intentions of each library in tact as well. If the "correctness" of the stylesheet implementation get's fixed, this could also result in a fairly significant performance improvement for RNW users.

At the end of the day, option 3 would be a bummer but is also a possible reality. The downside there is duplicated work and two people working on a very similar problem in isolation versus collaborating.

I would definitely like to talk more about this, and am going to reopen this issue. I apologize for taking a few days to reply to this issue, and hope you don't think I was just ignoring you or something.

With that said, I hope you don't mind me continuing to hack on this project during my holiday because it is currently a project I'm having fun working on, even if I'm just learning some of the same things that you learned months ago. I could make the project private and then no one would know, but I prefer to write code out in the open.

OK sgtm and I understand. No question about making this private, if anything I hope you don't mind if there is cross-pollination in the other direction so I can apply some optimizations you develop back to RNW while we figure out the relationship between the projects.

@necolas I most definitely DO NOT mind. If anything, I'd be happy to chat about different trade-offs and corner cases that have caused us both to end up where we are right now. Getting stylesheet working on the web is a tricky business, but I'm pretty happy with where this implementation has gotten. I have a feeling you've stumbled into more corner cases than I have, but I think I've tackled some of the hard performance problems reasonably well.

Also, i forgot to mention that the reason I depended on the fork of RNW instead of master is only because i needed the fork that uses react 15.4

After adding memorization and mapping styles to class names render performance is looking much better without altering the computed styles. 13-15 times faster than before.

Benchmarks on my machine (means)

  • react-native-web@master: ~3500ms / ~500ms
  • react-native-web@stylesheet-rewrite: ~220ms / 35ms
  • react-primitives: ~140ms / ~30ms

I initially tried using the react-primitives stylesheet. But the computed styles were incorrect (because it concatenates class names when resolving RN array-styles).

There are more memoization optimizations I think I could squeeze out for arrays of mixed style types. And I'd like to find out what happens when you hydrate the cache from a SSR stylesheet

@necolas very cool! Sounds like you got some big improvements.

I initially tried using the react-primitives stylesheet. But the computed styles were incorrect (because it concatenates class names when resolving RN array-styles).

Can you elaborate on this a little bit more? I wasn't aware of incorrect semantics with my current implementation, and would like to know what I am missing.

The current implementation implements higher specificity of class names by their position in the nested array:

ie, <View style={[styles.foo, [styles.bar, styles.baz]]} /> would result in the following HTML and CSS:

<div class="foo bar1 baz2" />
.foo { ... }
.bar1.bar1 { ... }
.baz2.baz2.baz2 { ... }

Is this the behavior that you were saying was incorrect?

Can you elaborate on this a little bit more? I wasn't aware of incorrect semantics with my current implementation, and would like to know what I am missing.

After reading your comment and code I realised I must have made a mistake. Tried integrating it again and it renders the RNW storybook examples correctly AFAICT! There was one small issue – TouchableHighlight not clearing the pressed state, but I didn't look into it. Quite a cool hack to use class chaining. I wonder what (if any) effect there is on generated CSS in a big app.

I ran RNW through the benchmarks and switched between the StyleSheet implementations:

  • react-native-web@stylesheet-rewrite: ~220ms / 35ms
  • react-native-web@stylesheet-rp: ~370ms / 55ms

At this stage it's encouraging to have 2 different approaches with similar results!

Nice. Do you have a link to a branch where you have these benchmarks? I'd love to take a look.

Thanks for working together on this :)

@lelandrichardson One of the greatest explanation of the motivation behind the library I have ever seen