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

Consider decoupling Reagent from React #271

Open
skrat opened this Issue Oct 21, 2016 · 41 comments

Comments

Projects
None yet
@skrat

skrat commented Oct 21, 2016

I would like to open a discussion about this. Preact is much simpler, with easier licensing, and way smaller in size, with mostly compatible API.

https://preactjs.com/

As of the ES6 API, it should be possible to leverage Closure Compiler for cross-compiling to ES5.

@Deraen

This comment has been minimized.

Member

Deraen commented Oct 21, 2016

Preact doesn't work with React-native.

Lack of stateful components would require changes in Reagent and would break many projects using Reagent.

But maybe it would be possible provide an option to use Preact.

First thing that could be done separate to Reagent, would be to create package for Preact: https://github.com/cljsjs/packages

@Naomarik

This comment has been minimized.

Naomarik commented Oct 23, 2016

With all the fast, lightweight competing JS frameworks today, what basis would you choose preact over something like mithril, vidom, or inferno? If there's to be a discussion on being able to optionally wrap another framework, should we first discuss a way to evaluate the best alternative?

@developit

This comment has been minimized.

developit commented Oct 30, 2016

Aside from the React Native point (which is correct and can be a reason to use React itself), the reason one might choose preact is because it maintains nearly perfect API compatibility with React. This is very different from switching to an arbitrary frontend framework, since it does not require any changes to your code.

Stateful components are definitely supported by the way, let me know if there's an article or something out there seeming to imply otherwise. Also preact works fine without ES6 or anything, it supports IE9+. Cheers!

@Deraen

This comment has been minimized.

Member

Deraen commented Oct 30, 2016

Preact retains a large amount of compatibility with React, but only the stateless functional components and ES6 Classes interfaces.

I just skipped ES6 classes part as it is not something I'd be usually interested in.

As the API is nearly perfectly compatible it is possible that nothing needs to be changed in Reagent and users could just change the dependency. For this to be possible, someone should finish packaging Preact in Cljsjs: cljsjs/packages#719

@skrat

This comment has been minimized.

skrat commented Nov 14, 2016

With the cljsjs/packages#719 (comment) merged, should we try to do this and see what happens?

@developit

This comment has been minimized.

developit commented Nov 14, 2016

I'd be excited to see a demo of this if anyone gives it a go. Really neat stuff!

@KeeganMyers

This comment has been minimized.

KeeganMyers commented Nov 16, 2016

Just adding my two cents. I started working on a fork previously that used inferno and found converting from react to inferno to be very simple. However, I only used stateless components. Eventually I abandoned the fork due to lack of time and testing utilities for inferno. Of the virtual DOM libraries I have tried React is currently the only one that has its own testing utilities. I think rendering components to hiccup data structures would increase test-ability and allow an easier transition to other JS frameworks.

@developit

This comment has been minimized.

developit commented Nov 17, 2016

@KeeganMyers preact-jsx-chai is fairly capable for testing, though I'm not sure how these types of things work for ClojureScript. I am working on a way to use Enzyme with Preact, if that's what you were referring to (since React's TestUtilities option is deprecated).

I don't mean to imply that you should consider them equivalent though. Preact is an optimization for web, React is a reference implementation with all the necessary abstractions for multiplatform. I think the compelling thing for Reagent would just be a flag that would let people output preact if their use-case is a good fit for it.

@piecyk

This comment has been minimized.

piecyk commented Dec 7, 2016

Is someone working on this ? planing to hack on this over the weekend!

@skrat

This comment has been minimized.

skrat commented Dec 7, 2016

@piecyk I was planning to but having hard time finding time, go ahead.

@developit

This comment has been minimized.

developit commented Dec 7, 2016

@piecyk hack away! 😍

@piecyk

This comment has been minimized.

piecyk commented Dec 8, 2016

After quick look... just included preact, preact-compat, preact-render-to-string we got

Ran 101 tests containing 2286 assertions.
11 failures, 2 errors. 

and demo page is fully working 🎉 out of box, awesome work @developit 👏

I think i will create reagent.impl.provider that will have refs to react / preact or better export functions to don't have refs to it in ohter places just hide it here, i think we don't need all stuff from preact-compat etc...

@danielcompton

This comment has been minimized.

Contributor

danielcompton commented Dec 8, 2016

I can fully understand the desire for something like this, but I don't know if I'd like to see it as part of Reagent, at least not initially. Reagent was built around React and has close integration with it. While it would be possible to add another layer of abstraction to let people choose between React and Preact, what will happen if/when React provides features that we want to use but Preact doesn't support? I fear that we would run into a lowest common denominator situation.

An alternative option could be a friendly fork of Reagent (perhaps even within the reagent-project org) which swaps out React for Preact entirely. That project can then have room to breathe, experiment, and find what is the best way to write the code. If in (say) a year, there is continued interest in the project (and Preact is still being maintained(!)) then it could be possible to reintegrate Preagent with Reagent.

That option would also have the advantage of giving you a much faster feedback loop. It's quite likely a change like the one you are discussing will take a while for Dan to approve and review.

With all that being said, it sounds at the moment like this may be a pretty minimal change, so perhaps a separate project isn't necessary?

@skrat

This comment has been minimized.

skrat commented Dec 8, 2016

While it would be possible to add another layer of abstraction

It's not a layer of abstraction, it's a configuration variable + swappable backend for an API that Reagent is using

what will happen if/when React provides features that we want to use but Preact doesn't support

I don't think that's a valid reasoning. I think Reagent is built around using React's API, and if that API is implemented by different library, with different features (size) that users are after, then why not? Taking into account hypothetical future features of React is not a valid argument.

It is a very common pattern to have different backends and I don't see any downsides to it. The usual approach to provide some implementation specific is to expose the backend and let the users do anything they want to do with it.

@piecyk

This comment has been minimized.

piecyk commented Dec 8, 2016

@danielcompton you are so true! Also seeing how fiber can change a lot in near future regarding how react internal will work bring more doubts about this... preact on the other hand can win with some web worker integration

It looks like preact did great job and out of box 99% is working the same, just poking with this looks like it will be so small change, IMO we could just provide 3 files with api contracts that will dispatch correct implementation regarding what people want to use

(ns reagent.impl.provider)
(ns reagent.impl.dom-provider)
(ns reagent.impl.dom-server-provider)
@danielcompton

This comment has been minimized.

Contributor

danielcompton commented Dec 11, 2016

@skrat I'm not sure if what I was trying to communicate in my last comment came through very well, so I'll try again.

Introduction

In principle, I'm not opposed to the idea of Reagent using Preact. It has some cool features and I like that it is small (although in comparison to the total compiled size of a normal CLJS app it's probably a wash). If Preact worked 100% out of the box with Reagent with no code changes required then I would have no issues with someone swapping out the React dependency for a Preact one and calling it a day. If there are only a few minor tweaks to Reagent required to pass the tests, then again I don't really have any issues with that. I suspect that even if you have 100% of the tests passing, there will still be issues, as Reagent was built around React, and may not have tests that would cover the difference in behaviour between React and Preact.

Indirection

It looks like it may not be a pure lift and shift to support Preact. If that's the case then we run into a bigger issue: indirection. Reagent was written and built around the ideas and in the context of React. There are assumptions (probably tens or hundreds) built around React's API and possibly implementation details too.

Adding abstraction adds a large cost because you can no longer program against a concrete API and implementation, you now have to consider two. There are three numbers in computer science, 0, 1, and many. We would be moving from 1 to many, and that takes work.

An aside: recently at work, we were looking at moving a legacy system from only supporting dates in the past to also be able to support dates in the future. This should be straightforward right? We talked to the programmers responsible for it and they couldn't guarantee that it would work, nor whether supporting future dates would be easy or hard. In the building of that (or any) system, hundreds of simplifying assumptions are made around the context that the system is going to be built in.

It is a very common pattern to have different backends and I don't see any downsides to it.

I can't think of a single example of a system with multiple backends that didn't have any downsides to it, e.g. ORMs, HTML/CSS/JS, Java. There may be some, but they would be the exceptions that prove the rule. Everything has a cost, the question is whether there is a benefit that outweighs the cost. It is much harder to remove something from software than to add it, which is why we should be certain that the benefits outweigh the costs.

While Preact strives to be API-compatible with React, portions of the interface are intentionally not included. The most noteworthy of these is createClass() ...
https://preactjs.com/guide/switching-to-preact#3-update-any-legacy-code

Reagent currently uses createClass. There are workaround options provided, but this is an example of some of the API differences between React and Preact which you need extra compatibility layers to support. Do we know if the compatibility layer works 100% correctly?

A possible future if Preact support is merged now

As a thought experiment, let's assume that Preact is in Reagent with some kind of compatibility shim. Preact already has several performance optimisations that people can take advantage of:

customizable update batching, optional async rendering, DOM recycling and optimized event handling via Linked State. - (from Preact homepage)

Wouldn't you want to be able to take advantage of those in your application? I certainly would. Now to do so, you may run into issues because the compatibility shim layer that was written was encoded around default assumptions of React, and they may not apply to Preact. Do we have to rework the shim layer, or lower level Reagent API stuff? Who is going to do that work? Who is going to review it and merge it?

Let's consider the reverse. Perhaps in some new React version, Facebook comes out with a new API which is faster or better suited to Reagent's style of rendering, so we want to switch to that. However that new model may not work with Preact. Again, we're in a bit of a pickle: Preact users want to be carried along with Reagent and get the benefits of new Reagent work, but it may not be easy or possible to support the new API for them. Now what?

Consider everyday development on Reagent. Reagent's source code is built around a very detailed understanding of React and is highly optimised. If Preact was supported too, then developers would probably need to gain an understanding of Preact too.

At the moment, Preact has one main contributor, it has been around for 1.5 years. React has many contributors. I'd estimate there are 100+ people with very deep knowledge of React. It's been around (in public form) for 3.5 years. In general, the JavaScript community does not have a reputation for long-term support of projects. What happens if development slows/stops on Preact and the compatibility layer isn't kept up to date? It is much harder to remove something than it is to add it. Who decides when/if to remove Preact from Reagent at a future date?

These are all hypotheticals, but I hope this demonstrates that the extra abstraction provided by supporting two VDOM layers doesn't come for free. At the very least, it consumes extra brain cycles when testing and developing Reagent, extra support and documentation costs from users wanting to use one or the other, as well as extra indirection when running and debugging apps using Reagent.

The Innovators Dilemma

If you haven't already, I highly recommend reading "The Innovators Dilemma" by Clayton Christensen. One of the key points he makes in that book is the difference between integrated and modular products and when to develop each kind.

CHRISTENSEN: When the functionality of a product or service overshoots what customers can use, it changes the way companies have to compete. When the product isn’t yet good enough, the way you compete is by making better products. In order to make better products, the architecture of the product has to be interdependent and proprietary in character.

In the early years of the mainframe computer, for example, you could not have existed as an independent contract manufacturer of mainframe computers, because the way they were made depended upon the art that was employed in the design. The way you designed them depended upon the art that you would employ in manufacturing. There were no rules of design for manufacturing.

Similarly, you could not have existed as an independent maker of logic circuitry or operating systems or core memory because the design of those subsystems was interdependent. The reason for the interdependence was that the product wasn’t good enough. In every product generation, the engineers were compelled by competition to fit the pieces of the system together in a more efficient way to wring the maximum performance possible out of the technology that was available at the time. This meant that you had to do everything in order to do anything. When the way you compete is to make better products, there is a big competitive advantage to being integrated.
...
In order to compete in that way, to be fast and flexible and responsive, the architecture of the product has to evolve toward modularity. Then, because the functionality is more than good enough, you can afford to have standard interfaces; you can trade off performance to get the advantages of speed and flexibility. These standard interfaces then enable independent providers of pieces of the system to thrive, and the industry comes to be dominated by a population of specialized firms rather than integrated companies.

I would argue that we are still very much at the point where the current VDOM libraries aren't good enough yet. They aren't yet ready to be commoditised, and the best option is to tightly integrate.

Options from here (with some conjecture)

  1. Someone can make a PR to Reagent to add support for Preact. It will probably take a while to get merged because it is a significant change. Once it is merged and released, there will probably need to be several rounds of revisions before it is ready to go. Because Reagent moves relatively slowly, this will take a while.

    Reagent also has a large number of production users, so new releases need to be well tested and stable. Adding Preact to the mix is going to slow this down further.

  2. Someone can make a fork of Reagent (let's say it's called Preagent). You can run wild experimenting with what is the best way to use Preact in Preagent, take advantage of all of the great features Preact has, and have a much faster turnaround time for releasing and using it. You will be able to work out what is the right API and integration points for Preact because you have room to experiment with it, without the weight and responsibility of bringing the rest of the Reagent users along with you.

    At some point in the future, you could review merging Preagent back into Reagent, given all that you now know. You would also have the weight of evidence on your side where you can demonstrate the benefits of Preact and can show how many users want Preact. This would let you make a much better case for including Preact, give you what you want in the meantime, and likely provide a higher quality integration in the future.

    Alternatively, you may decide that Preagent is better served going its own way and integrating more closely with Preact. This is also a good option.

Abstraction is not free

The point I have been trying to drive through this post is that abstraction is not free. Over-abstraction is a common anti-pattern and it saps productivity. I had a friend who recently left a Clojure job and started a Java one. He quipped to me about how he'd forgotten what it was like to trace code through five layers of abstraction to get to the concrete implementation. As programmers, we're trained to solve problems by adding layers of abstraction, but that isn't always the best way to solve the problem.

Lastly, this isn't a personal attack on you or your ideas. I'm all for innovation in ClojureScript webapps and I think that it is worth investigating Preact and how it could work in the ClojureScript ecosystem 😄 . I'm not against Preact. I would consider using it if there was a measurable benefit. I'm just suggesting that the best way to go about this is probably not to integrate it into Reagent as the first step. ✌️

Update 2018-11-23: I published this at https://danielcompton.net/2018/11/23/on-abstraction.

@gadfly361

This comment has been minimized.

Member

gadfly361 commented Dec 11, 2016

Well said @danielcompton :)

+1 regarding idea of "preagent" (i.e., reagent fork focused on preact)

@Deraen

This comment has been minimized.

Member

Deraen commented Dec 11, 2016

Someone should experiment if there is even need for any changes in Reagent, in theory Preact (and for that matter Inferno) both provide the same API as React so Reagent doesn't even need to care which one is used. Though this will require using https://git.io/preact-compat or https://github.com/trueadm/inferno/tree/master/packages/inferno-compat, to provide React and ReactDOM objects.

@developit

This comment has been minimized.

developit commented Dec 11, 2016

Just wanted to drop a note to say that was a really excellent post @danielcompton. Should be a blog post! 😉

I totally agree regarding abstraction, even if it is something as theoretically simple as an alias - it's still a means of indirection. It's funny to see the case for reduced abstraction being made here, since that was essentially the original experiment that turned into Preact.

I think a "Preagent" (I should apologise for starting that naming trend) project seems good - it'd let the people interested in experimenting with Preact as a renderer iterate, without needing to work within the release cycle of Reagent itself.

@Deraen

This comment has been minimized.

Member

Deraen commented Dec 11, 2016

I packaged react-compat (and proptypes) for cljsjs and used exclusions to replace React with Preact: https://github.com/Deraen/problems/tree/reagent-preact

At least a simple demo is working without any changes to Reagent. With master at least. #276 is required because other Reagent will try to load react-dom-server which is I did not package yet, but seems like it also available.

Testing this requires locally installing two packages from cljsjs/packages#875 for now. Will finish this later today.

@Deraen

This comment has been minimized.

Member

Deraen commented Dec 11, 2016

@developit what React version is Preact-compat targetting? preactCompat.DOM doesn't seem to contain render, findDOMNode or unmountComponentAtNode which where moved from React to ReactDOM in React 15.

I worked around this with a small wrapper: https://github.com/cljsjs/packages/blob/bcb88731505f2168500446b1be24980fa3c24c45/preact-compat/resources/cljsjs/preact-compat/common/react.inc.js

Btw. demo project 286KB with React and 173KB with Preact. Non gzipped.

@developit

This comment has been minimized.

developit commented Dec 11, 2016

preact-compat is both the React and ReactDOM module exports in one package (no .DOM needed). It targets 0.14.x and 15.x.

You can simplify your file by doing:

window.React = window.ReactDOM = preactCompat;
@Deraen

This comment has been minimized.

Member

Deraen commented Dec 11, 2016

Thanks. Just realized that myself.

@developit

This comment has been minimized.

developit commented Dec 11, 2016

Nice. Would be curious to see the gzipped difference too (I use npm.im/gzip-size-cli to check).

@Deraen

This comment has been minimized.

Member

Deraen commented Dec 11, 2016

77.8KB with React and 42.8KB with Preact.

@developit

This comment has been minimized.

developit commented Dec 11, 2016

Thanks, good to know!

@kanwei

This comment has been minimized.

kanwei commented Feb 1, 2017

@Deraen Tried your preact branch and while it works for the simplest case, adding a nested component to the view gives me Using native React classes directly in Hiccup forms is not supported. Use create-element or adapt-react-class instead: ReagentInput

Can you reproduce? It seems to happen with something as simple as:

(defn view1 []
  [:input])
(defn view2 []
  [:div
    [view1]])
@developit

This comment has been minimized.

developit commented Feb 2, 2017

I can't read this code to save my life, but the error is thrown here. Seems like view1 in your example is pre-transformed to a class instead of a function? Not sure where that process happens.

@yogthos

This comment has been minimized.

Member

yogthos commented Jul 3, 2017

It might be an interesting idea to have a pluggable backend API in Reagent. If there was a protocol, then it could be implemented by any implementation that provides a VDOM, and open a possibility of having a native ClojureScript implementation.

@Deraen

This comment has been minimized.

Member

Deraen commented Jul 11, 2017

I'm open to protocol or something similar. It means we'd have to break things by removing requires on React and user would need to require namespace providing React implementation and probably provide that to render as option. Next version will probably break things anyway (:require changes coming in next Cljs release) so maybe it makes sense to do this at the same time.

@Deraen

This comment has been minimized.

Member

Deraen commented Jul 11, 2017

Providing adapter to only render is probably not enough as some things that need to call React happen outside of render loop. If we just have global adapter value and function to change that, it could be possible to implement this without breakage.

@danielcompton

This comment has been minimized.

Contributor

danielcompton commented Jul 12, 2017

I'm still of the opinion that abstracting this out in Reagent at this stage is too early. Making it pluggable means that we need to find the right abstraction. This is difficult to do without implementation experience, and once the protocol is created it will be hard to change. I would suggest forking Reagent to use a different rendering backend, then making a PR to merge it back in after you've learnt the implementation lessons and have a good idea of what a good protocol for this would look like.

OTOH I haven't thought too much about what a protocol would look like here, so maybe the protocol would be obvious and simple?

@yogthos

This comment has been minimized.

Member

yogthos commented Jul 12, 2017

I think doing a proof of concept fork would be a good idea. I think creating a protocol will result in cleaner code as well, as it would require untangling anything that's React specific from Reagent.

It would be good to work back from the API that Reagent exposes to the user and inform the protocol based on that. The good part is that Ragent API is fairly small, so I don't expect the protocol to be terribly complex, but I could be wrong here. :)

If nothing else, there would be value in the exercise just to figure out what constitutes core functionality in Reagent.

@yogthos yogthos added the enhancement label Aug 19, 2017

@kraf

This comment has been minimized.

kraf commented Aug 21, 2017

I stumbled upon this today: https://medium.com/@raulk/if-youre-a-startup-you-should-not-use-react-reflecting-on-the-bsd-patents-license-b049d4a67dd2

I think the licensing issue is a huge deal.

@mike-thompson-day8

This comment has been minimized.

Member

mike-thompson-day8 commented Aug 21, 2017

And there are contrary opinions on the patent clause which say the fuss is much ado about nothing.
https://medium.com/@dwalsh.sdlr/react-facebook-and-the-revokable-patent-license-why-its-a-paper-25c40c50b562

I'm not a lawyer, so this is hard for me to judge legally but, empirically, React has been out for 3.5 years, and I've not heard of a single startup getting sued.

But if you are sufficiently worried and motivated, I think the previous advice still stands: you should fork Reagent and create a proof of concept based on Preact. I'm fairly sure the current maintainers are not about to change Reagent itself in this regard.

@yogthos

This comment has been minimized.

Member

yogthos commented Aug 21, 2017

I agree that this is unlikely to be an issue for most people. However, the fact that Facebook can arbitrarily decide to sue you is a valid concern. The clause is intentionally vague, and Apache Foundation seems to think this is reason enough to black list React from their projects.

I also think there are other advantages aside from the license issue in abstracting the React layer. If somebody is motivated to explore decoupling React, it should be done in a way that's backend agnostic. As @Deraen notes, the team would be open to having a protocol. This would also help isolate what constitutes core functionality of Reagent.

@skrat

This comment has been minimized.

skrat commented Aug 21, 2017

Facebook will sue you when it sees your app (using React) as a threat to their business.

@yogthos yogthos changed the title from Consider replacing React with Preact to Consider decoupling Reagent from React Sep 17, 2017

@ggeoffrey

This comment has been minimized.

ggeoffrey commented Sep 23, 2017

@yogthos

This comment has been minimized.

Member

yogthos commented Sep 23, 2017

I think this allays any fears over the React license. I do think that decoupling from React might be worth exploring though, so I'd keep this open in case somebody wants to look into it.

@tomconnors

This comment has been minimized.

tomconnors commented Feb 21, 2018

I updated @Deraen's project to use the latest cljs and reagent: https://github.com/tomconnors/problems/tree/reagent-preact. For anyone who just wants a smaller build size, this seems like a pretty good option right now. All that was required to get this simple project to run was to provide the lein :exclusions and the :foreign-libs and :externs options to the compiler. I'd hoped to use the :npm-deps option, but that doesn't appear to work because we need to use the "preact" package to produce the "react" namespaces.

@ashnur

This comment has been minimized.

ashnur commented Nov 20, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment