Feliz: Make Feliz an abstract API

Created on 19 Dec 2020  ·  15Comments  ·  Source: Zaid-Ajaj/Feliz

I'm about to propose a breaking change, but hopefully one that only requires final users and library authors to update packages and no actual code changes.

  1. F# is a great language to declare UIs
  2. Every F# developer who does something web-related write their own DSL to declare UIs
  3. Feliz is a great API/DSL to declare React web UIs

What if we removed "React" from 3? This could solve many issues:

  • Feliz could become a standard and solve 2 in the list above. So most components written with Feliz could automatically be used with different rendering engines
  • This can also avoid the Fable.React dependency as discussed in #285 (Fable.React would implement IViewElement to keep compatibility)
  • This could also make it easier to write a server-side Feliz renderer without having to use #if FABLE_COMPILER everywhere as with Fable.React
  • It may happen that someone somewhere is writing a frontend renderer directly in Fable. It'd be great if it could just use Feliz

With the elevator pitch done, if you think this is a good move we can discuss the implementation. I haven't tried anything yet so not sure how it could work but what I have in mind is something like this:

type IViewElement = interface end
type IViewProp = interface end

// Later renderers can inherit these interfaces
type ReactElement = inherit IViewElement

// The basic renderer interface contains few methods
type IHTML =
    abstract renderNative: tagName: string * props: IViewProp seq

// Most of the helpers would be extensions to the interface
type IHTML with
    member this.div(props: IViewProp seq) = this.renderNative("div", props)
    ...

// Then we have different implementations
module Feliz.React

type ReactHTML() =
    interface IHTML with
        member _.RenderNative(tag, props, children) = ...

let Html = Feliz.ReactHTML()

// Consumer code
open Feliz.React

Html.div [...]

Not sure how intellisense would work, maybe we need to use an abstract class instead of an interface, but I hope you get the idea. What do you think?

Most helpful comment

@alfonsogarciacaro This looks amazing. As mentioned in #262, the only thing I really, really miss since switching from Fable.React to Feliz is the 'it just works' SSR. Both Feliz and Feliz.ViewEngine are amazing projects, but having two different projects makes code reuse difficult, as any piece of code that uses the ifdefs can only be used by other code that uses the ifdefs - creating silos for js-only, dotnet-only and both-worlds-code. Anything that closes that gap would certainly improve the quality of life when working with Feliz and .Net servers.

All 15 comments

Hi @alfonsogarciacaro, to be honest with you I don't _feel_ like this is a good idea. Theoretically it makes sense of course: let's build a standard API for HTML in Fable. In practice, that doesn't translate very well. Here are a couple of reasons:

  • The current API is anything but standard, in fact it very opinionated. Many people still don't like the whole single list and prop.children thing which is of course totally fine.
  • The current API is made "simple" because it of the assumption that it is React-only: abstracting it would require A LOT of work where the alternative of writing/duplicating a Feliz-style API would probably be the easier solution.
  • I would rather focus on improving the tooling around React rather than spread the resources thin: Feliz is far from "done", there is still a lot to do regarding documentation, sample applications, better testing story etc.

Thanks a lot for the reply @Zaid! I understand your reasoning, it's still a pity that we have so many competing DSLs but maybe there are other solutions as you say. If I find time I'll try to write a prototype anyways to see how it would work.

BTW, I've started some work here: https://github.com/alfonsogarciacaro/Feliz.Engine/blob/main/src/HtmlEngine.fs

I haven't tested it, but I've removed al the unbox and similar tricks, so in principle it "should" work with any renderer that implements this interface. It'd be nice, for example, to try to combine a React renderer with @dbrattli Feliz.ViewEngine and see if it makes SSR simpler with Feliz.

There's a couple things that would need to be figured out/considered:

Intellisense

I'm not super happy that my intellisense would look funky since Html would be defined in Feliz as:

let Html = HtmlEngine(renderer)

That's more of a minor issue though.

Bundle size

We'd obviously want this to be the same as it is currently (or better). Perhaps a plugin in Feliz.HtmlEngine that is wrapped in compiler directives? This solution would make the web servers have a dependency on Fable though.

Other libraries

One of the biggest issues with SSR as it's currently done is that other libraries in most cases simply won't work, and those that do (like Feliz.Bulma/Feliz.Bulma.ViewEngine) still need adjustments unless they too build an abstract API for both to reference similar to what this is.

Alternatives

I think the root problem here is that trying to do SSR ourselves isn't super feasible when we want to fully emulate the intended first paint. What we really need is something like ReactDOMServer for .NET. I haven't really dug too deep into this, but would React.NET (maybe with a F# wrapper) be a better way forward?

Thanks a lot for the comments @Shmew and sorry for replying late:

  • Intellisense: Hmm, I only wrote a simple example with the abstract API but didn't see any issue with autocompletion. I'll try to check again. Instead of accessing a static type users access a value. I guess it should be fine and in many cases code won't need to change. The main drawback would probably be that open type cannot be used.
  • Bundle size: I also need to check this. My hope is it doesn't change very much because the class members are compiled by Fable as detached functions that can be tree shaken and also minify well. Although unfortunately the generated code won't look as nice as it's now after #284
  • Other libraries: Yes, other libraries would have to be adapted to get the benefits from an abstract API. Hopefully with minimal changes.
  • Alternatives: TBH, I don't really know how SSR with React works, although looking at the (contributed) code in Fable.React it doesn't look very complicated (basically just adding some metadata to the generated html tags) so not sure if it's easier to use something like React.NET or not. I guess that this won't be usable ootb with Feliz either, as Feliz contains Fable tricks (mainly unsafe casts) that will throw in .NET.

Actually, for this I'm thinking not only in SSR but also _beyond React_ :wink:, for example for html generators in F# servers that don't necessarily need to be compatible with React and in the case we want to use other renderers than React for the frontend.

Thanks a lot for the comments @Shmew and sorry for replying late

You're welcome, and no worries!

Intellisense: Hmm, I only wrote a simple example with the abstract API but didn't see any issue with autocompletion. I'll try to check again. Instead of accessing a static type users access a value. I guess it should be fine and in many cases code won't need to change. The main drawback would probably be that open type cannot be used.

Sorry, I wasn't super clear. Intellisense is fine, but rather the fact that Html would be a different color. Like I said, really minor 😅. The fact we lose being able to open type is actually a bit of a bigger deal. I know some really prefer that over having to namespace everything.

Bundle size: I also need to check this. My hope is it doesn't change very much because the class members are compiled by Fable as detached functions that can be tree shaken and also minify well. Although unfortunately the generated code won't look as nice as it's now after #284

I didn't even realize that the interop functions weren't already inlined, I inline them in my libraries already. I was more talking about the fact that the entire Html module has no bundle size increase compared to if it was done natively in JS because it's just all an inlined wrapper. With a concrete class that no longer becomes the case.

Other libraries: Yes, other libraries would have to be adapted to get the benefits from an abstract API. Hopefully with minimal changes.

The issue with this is that libraries that use native react code can't function properly because all the internals are JS only.

Alternatives: TBH, I don't really know how SSR with React works, although looking at the (contributed) code in Fable.React it doesn't look very complicated (basically just adding some metadata to the generated html tags) so not sure if it's easier to use something like React.NET or not. I guess that this won't be usable ootb with Feliz either, as Feliz contains Fable tricks (mainly unsafe casts) that will throw in .NET.

Yeah that works for simple cases, but if you try to use things that are more complex like some of the components in Feliz.MaterialUI it just isn't going to work. From what I understand, React SSR is pretty simple when done on node.js because they can just run the ReactDOMServer to actually spit out a full string that is the first paint of the page. My understanding is that React.NET is just wrapping a node.js process to do exactly that.

I suspect this would actually be pretty easy with Feliz code as it already supports things like that. The process would be something like:

Fable compiles F# React 
-> node.js process imports and returns html string 
-> web server caches output 
-> web server serves page

beyond React

That is a great point, and definitely something I'd want if I was looking to render pure HTML without React or anything.

I've experimenting with this idea, you can see my progress here: https://github.com/alfonsogarciacaro/Feliz.Engine

There's still work to do and I have some doubts about a couple of things, but it's already working and covers most of Feliz API. However, I had to make a few changes so it's true that it's going to be difficult to replace Feliz with something like this. But there's still value in having an abstract version of Feliz, as it opens many possibilities. Some examples:

  1. Use it with other frontend frameworks, like Sutil: https://github.com/davedawkins/Sutil/pull/15
  2. Use it to generate CSS instead of SASS: https://github.com/alfonsogarciacaro/Feliz.Engine/blob/main/samples/Feliz.Css/Feliz.Css.fs
  3. Use it for generating static html: https://github.com/alfonsogarciacaro/Feliz.Engine/blob/main/samples/Feliz.StaticHtml/Feliz.StaticHtml.fs
  4. Use it with another virtual-dom implementation, like Snabbdom: https://github.com/alfonsogarciacaro/Feliz.Engine/blob/main/samples/Feliz.Snabbdom/Feliz.Snabbdom.fs

These are all drafts but they're working and you can see they require very little code. More importantly they all offer a documented and familiar API to users, which is also extensible: e.g. you can easily create a BulmaEngine that is compatible with all these (and future) apps. In the case of 2. and 3. I'm using Fable because incremental compilation is much faster than with .NET but Feliz.Engine is compatible with .NET (I removed all the unbox) so it should be trivial to make them compatible with F# .NET servers, and hopefully it shouldn't require much work to make the HTML generator React-compatible too.

What do you think @Zaid-Ajaj? I've already given up on the idea of replacing the current Feliz React API 😅 But do you think something like this should belong to the "Feliz family" (maybe in this repo)? Are you ok with the name, or would you rather use something else?

Hi @alfonsogarciacaro, I will have a deep dive into these samples sometime this week and come back to it for sure 😄

Hello,

I took a really quick look at the code and it seems like this abstract version will have some impact on the bundle size. Just wanted to point it out as it was one of the feature of Feliz in it's current form.

Yes, an impact is expected, the question is how big it is :) I'm hoping that for medium-big apps is not very noticeable but of course it'd be nice to have something like fulma-demo in both styles so we can compare and use to test optimization opportunities. It'd be also interesting just to check how inlining or not Feliz helpers affects the bundle size. Right now, the code generated by Feliz is quite nice and looks a lot like compiled JSX, but functions can be minified so it could be a way to reduce duplication of many calls like .createElement("div") (not sure if gzip can compress these calls though).

Yeah I can't imagine anyone would be opposed to being able to use this wonderful API/style in other projects. My only real concerns were if it would impact the quality of this library, and some of the ergonomic issues that I outlined above.

Hi @alfonsogarciacaro, I have had a good look at the example code. I was surprised to see that the css engine and static html engine are both nodejs-only applications since they use node.js streams. Although the idea is interesting, I find it hard to see myself actually using any of the above:

  • The CssEngine generates CSS, what is the benefit of that as opposed to using just CSS/SASS toolchains or CSS-in-JS like the one built in Feliz or using other CSS-in-JS libs like FSS or Emotion. Generating CSS from F# would work on it's own but it is not something I would use with Feliz since relying on existing toolchains would be a better choice (also faster compile times if you don't want Fable/F# compiler to be slowed down by having to compile huge stylesheets) Currently I am more in favor of using CSS modules for Feliz/React components
  • The static html engine: Feliz already generates static html using ReactDOM in node and in the browser. Feliz.ViewEngine does the same for generating static HTML in dotnet. I know the idea is to share the API but that is made difficult by other challenges as @Shmew mentioned: 3rd-partly libraries having to implement both, impacts the generated code which is now really clean. It also mixes attributes, styles and children into one list which breaks the current API of Feliz.
  • Snabbdom engine: interesting example but doesn't have the ecosystem of libraries like React does.

All in all, an interesting exercise. To be honest with you, I am not a huge fan of the new API and personally would rather spend time improving the current experience with more docs, examples and bindings in React instead of trying to standardize the libraries. Sorry if I am coming off a bit negative about it, you probably worked a lot of it. I don't mind having the same name as Feliz since it was inspired by the library.

Thanks a lot for checking the samples @Zaid-Ajaj! I appreciate your honesty. The css and html printer were just quick examples to check how easy/difficult was to adapt Feliz.Engine to potential uses (the code to do the printing is just 50 lines). I used Fable & node because incremental compilation is much faster, but "in principle" it should be easy to adapt it to .net. The Snabbdom adapter was mean to be also another quick example but I'm actually starting to like, though that's another matter :)

Anyways, I've finally realized it's not going to be possible to abstract current Feliz React apps without breaking changes (just as you were saying from the beginning 😅) so let's keep it as a parallel project for users who want to try other renderers with a Feliz-like API 👍

@alfonsogarciacaro This looks amazing. As mentioned in #262, the only thing I really, really miss since switching from Fable.React to Feliz is the 'it just works' SSR. Both Feliz and Feliz.ViewEngine are amazing projects, but having two different projects makes code reuse difficult, as any piece of code that uses the ifdefs can only be used by other code that uses the ifdefs - creating silos for js-only, dotnet-only and both-worlds-code. Anything that closes that gap would certainly improve the quality of life when working with Feliz and .Net servers.

Yes. that would be a great use case. It's tricky though because at the end Feliz.Engine has some differences with Feliz, which means we would have to write a Feliz.Engine React implementation that would compete with Feliz itself, something I would like to avoid ;) But hopefully we can find a solution.

Was this page helpful?
0 / 5 - 0 ratings