reason-reactify icon indicating copy to clipboard operation
reason-reactify copied to clipboard

Expose a minimal, shared reconciler interface to encourage component reuse

Open jchavarri opened this issue 5 years ago • 6 comments

While working on #34 I updated the DOM reconciler example to have its primitives look more like the ones in revery:

https://github.com/bryphe/reason-reactify/blob/60e270060db3980a833d4a62b5750c7b5e540855/examples/dom/WebReconciler.re#L17-L20

revery is already exposing CSS-like styles, and colors.

This made me think that, if there was a common interface that exposed the type for a minimal, shared set of primitives, it could be possible to have 100% reusable components across different reconcilers, as long as those primitives remain the same and the components themselves don't use environment-dependent APIs. Another upside would be the familiarity with these primitives so moving from one platform to another would have less friction.

The downsides of having this common interface would be that specific environments could put back-pressure onto these primitives, to promote new controversial primitives to get included, or some attributes / types are modified to satisfy one specific reconciler, but that might be unavailable in other platforms.

In any case, this "universal interface" would be optional, so reconcilers authors could always decide if they want their reconciler to matche this interface or not.

I'm also not sure if the interface should live in reason-reactify, or better in revery. I think the "lower" it is defined on the dependency chain, the more chances to increase the reuse of platform-agnostic components.

References:

jchavarri avatar Dec 16 '18 23:12 jchavarri

This made me think that, if there was a common interface that exposed the type for a minimal, shared set of primitives, it could be possible to have 100% reusable components across different reconcilers, as long as those primitives remain the same and the components themselves don't use environment-dependent APIs.

This is a really neat idea! It means that, if we have those consistent underlying primitives, you could have a whole ecosystem of components that work independently of the underlying infrastructure.

It would be pretty magical if you could build a component that works across Web, Desktop, Mobile, Native Widgets, GL Widgets, etc 🔥

The downsides of having this common interface would be that specific environments could put back-pressure onto these primitives, to promote new controversial primitives to get included, or some attributes / types are modified to satisfy one specific reconciler, but that might be unavailable in other platforms.

Yes, I think this is the challenge. How do we balance exposing enough capabilities of the primitives to make them usable, while not exposing so many that it becomes cumbersome to use in general? Those references you posted are really helpful - seems like a lot of thought has been put into those.

In any case, this "universal interface" would be optional, so reconcilers authors could always decide if they want their reconciler to matche this interface or not.

I like this - it also might mean a simpler starting point - someone who just wants to play with reason-reactify or incorporate it could skip steps 1, 2, & 5 in our README, and just focus on hooking up to their API (3&4). They could always 'eject' and implement the full model using the functor. Simply having a more convenient starting point would be valuable by itself!

I'm also not sure if the interface should live in reason-reactify, or better in revery. I think the "lower" it is defined on the dependency chain, the more chances to increase the reuse of platform-agnostic components.

I think revery would itself be the wrong place, in that it is already opinionated - it's using the hardware GL/WebGL rendering strategy, freetype/harfbuzz for text rendering, GLFW for managing app state, etc. I could see these primitives being applicable to other projects, too, like ones using native primitives (ie, briskml). Would be so cool if we could reuse third-party components between projects like brisk and revery!

I could see these primitives living either reason-reactify or perhaps we could refactor those pieces out of revery into a library like revery-core (where revery-core just exposes these primitives + Style, Color, whatever else is needed). revery-core could be the less-opinionated, more reusable, renderer-agnostic implementation.

You're definitely right - the lower in the dependency chain it is, the easier it is to apply to projects in general.

So I guess the next steps here are:

  • Decide if we should put this reconciler here in reason-reactify, or split out a revery-core project?
  • Decide on the core set of primitives we'll start with

Lots of potential here... thanks for thinking about and proposing this @jchavarri !

bryphe avatar Dec 17 '18 20:12 bryphe

I definitely like the idea and started thinking about it for mostly 'utility' stuff like color and style.

With reconcilers, I'm not sure there's something that could be factored out to an extent that would make it meaningfully reusable. I'd love to be proved wrong and see a concept built for native elements, because ultimately html already is a unifying interface.

rauanmayemir avatar Dec 19 '18 13:12 rauanmayemir

So I guess the next steps here are:

  • Decide if we should put this reconciler here in reason-reactify, or split out a revery-core project?
  • Decide on the core set of primitives we'll start with

@bryphe I've been thinking a bit more about what can be done to maximize component reuse between different environments. These are the different parts that I identified that would be involved in the pursuit of that goal:

  1. The types of the primitives (i.e. the ones that currently are defined in revery):

  2. The components API (as defined in reason-reactify): useReducer, useState etc.

  3. Companion types and utils used by primitives: for example, Style.t and NodeEvents.t in revery's View.

  4. Other types and utils that are not needed by the primitives, e.g. Animation.

The first two seem to be a hard requirement to get any component reuse. The third one consists on dependencies of the primitives in revery, and it might make sense to include them too as part of that minimum contract. The 4th would be a nice to have, but maybe it doesn't need to be included as part of the shared interface. Once there is an API + primitives, large part of the ecosystem (like hooks) could be reused across environments.

I could see these primitives living either reason-reactify or perhaps we could refactor those pieces out of revery into a library like revery-core (where revery-core just exposes these primitives + Style, Color, whatever else is needed).

@bryphe Keeping the points above in mind, I think the reach of this interface would escape the intent of reason-reactify, and it seems imo to fit better in a potential revery-core as you mentioned, that would include this set of interfaces and utils. (side note: once we extricated that revery-core, I'd prob start working on a revery-dom based on it, which is something I've been wanting to play with for a while. That exploration would hopefully help refine these boundaries and keep finding common ground between environments.)

ultimately html already is a unifying interface.

@rauanmayemir If the goal is to maximize component reuse across environments, anything browser specific would need to remain behind this interface, be it CSS or DOM related concerns. That means leaving behind a ton of upsides, but HTML and CSS are so pervasive (and different to anything else) that including them in any way in this interface would probably kill any chance of reuse. I think the fact that revery is being built with a "native first" strategy is the right approach to avoid the leaks in the API that generally come with the "browser first" strategy.

jchavarri avatar Dec 23 '18 00:12 jchavarri

@jchavarri I think you misunderstood, what you wrote is my thinking, too. :)

I meant to say that I wasn't able to envision truly shareable primitives I could use on native side, so tabled the idea for now. But I'd be happy to follow revery-core's lead should there be an adoptable prototype for those interfaces.

rauanmayemir avatar Dec 23 '18 11:12 rauanmayemir

Ah, sorry for misunderstanding! 🙂 I insisted on that point because I've thought many times in the past that one of the missed opportunities from React was precisely this: because it started as a browser based library, the "host components" were all DOM element tags based. Later on, they were able to split the library into react and react-dom, but it was already very hard to get rid of that dependency on the platform (without breaking tons of already existing components).

I agree with you that the path to component reusability across platforms is not a trivial one (react-primitives for example has barely 70 dependents after more than 2 years of existence), but this is a great chance to at least do everything we can to set the minimum foundation to make it happen.

jchavarri avatar Dec 23 '18 17:12 jchavarri

Cross posting from https://gist.github.com/lpalmes/8f771d2a700bd2a1172d73286ec75b19#gistcomment-2798693:

Some thoughts:

Supporting two models (class-based and hooks-based model) or just one

I think that creating a shared interface between reconcilers will be very challenging on its own, let alone if we try to bake the two models React supports today: the class components, and the functional components + hooks. Both models are pretty much equivalent in their expressiveness, but very different on their API.

It seems that functional+hooks will get more momentum over time so I think I would prefer to bet just on that model for simplicity. I could be wrong though about that momentum.

Interface vs implementation

The Reason type system provides a way to define types interfaces, and we could leave to each reconciler the details on how they want to implement it. For example, the let%reve ppx is a purely implementation detail because Revery decided to use first-class modules to define components. But Brisk or Pure could settle in a totally different approach, if the "component definition" was only limited to the specification of the render function (i.e. one reconciler could wrap the function with a module, but another could wrap it with a record for example).

I tried to start defining in https://github.com/revery-ui/reason-reactify/issues/35#issuecomment-449605494 the different parts of this interface. What would be the minimal interface that we could define so allow individual exploration in the different reconcilers, but still while allowing for a meaningful % of user-created components to be reused across the reconcilers? Does that interface even exist?

Find a balance between sharing too much vs too few

These are some of the areas where I think we could find a shared interface definition, but it's challenging to do so without restricting too much each reconciler evolution:

  • Dual models (class vs function+hooks), as mentioned above
  • Shared hooks APIs: supposing we include the function+hooks into the shared interface, maybe we could just follow ReactJS on thisto play it safe
  • Shared "fixed" props: like style or children: for any meaningful % of components being reused, we probably want to define these
  • Minimal set of shared component primitives (View, Text, Image,... what else?)
  • Minimal Layout API (part of fixed prop style): should it be included?
  • Minimal Event API
  • Fiber vs non-fiber: Does a fiber implementation have any impact on the minimal shared interface?

jchavarri avatar Jan 01 '19 14:01 jchavarri