Rodrigo Rosenfeld Rosas

Adopting React.js seems risky for long-term projects

Fri, 16 Jun 2017 18:10:00 +0000 (Updated at Sat, 17 Jun 2017 09:10:00 +0000)

Important Update

Feel free to completely skip this article as it's no longer relevant. I was confused by this part of the React documentation:

It is important to remember that the reconciliation algorithm is an implementation detail. React could rerender the whole app on every action; the end result would be the same.

It turns out "rerender", as explained in the ticket I created on the React project, means calling render in all components, it doesn't mean it could unmount and remount all components. If it remounted everything as I interpreted initially, it wouldn't be possible to integrate to any third-party library, which was my main concern.

That gives me enough confidence to adopt React or some of its alternative lightweight implementations. I'm keeping the old content just in case you're curious about it...

Old content

I've been working with long-term Single Page Applications (SPA) since 2009. When you know an application has to be maintained for many years you have to approach technology adoption very carefully. React.js introduced a very interesting approach based on virtual DOM and reconciliation algorithms, which seems to work great, but should it be considered safe to adopt React.js these days?

At a quick glance, the answer seems to be an obvious yes, right? React.js is used by Facebook, one of the largest company in the world, and maintained by its team with open-source contributions. It was largely adopted by many companies and there are some newsletters dedicated to React.js related technologies. There are even quite some compatible implementations such as Preact.js, Inferno.js, react-lite as well as other similar solutions such as Dio.js, MithrilJS and Maquette. All of them taking advantage of the virtual DOM concept. That means that even if React took a different route, or if Facebook moves to something else and stopped its maintenance, it should be easy to move to some of its alternatives provided we use some basic set of features that should be enough for most applications.

I was really excited by the VDOM moment and all those related technologies and I understand how they would help me to improve our current code base by not having to worry about manually managing the DOM, which gets more bug prone as you have to update an existing DOM. We adopted Knockout.js some years ago for parts of the application and it gave me about the same sense of making the code easier to maintain. However embedding HTML in JavaScript components with JSX feels much simpler to me than creating Knockout.js components (or Angular components, as they have a higher hype these days). Also, we are very concerned about the initial load time and it seems like VDOM based solutions can perform the initial rendering much quicker than MVVM alternatives such as Knockout.js and Angular.js.

My excitement quickly turned into fear after further reading the React official documentation, which is great, by the way.

Third-party components support

When you have to maintain a long-term large code base, one of your main concerns will be interoperability with third-party components. You can certainly find many articles and videos showing how easy it is for React to use third-party components. Almost all of them will mention returning false from the shouldComponentUpdate hook, or they will suggest an empty container.

It turns out it currently works pretty well, but is this really supported by React? I found React's official documentation to be quite confusing regarding third-party components as it's not consistent. Here's why I'm concerned the current approach to integrate with stateful third-party components may no longer apply with future versions of React.js and I couldn't find any official recommendation that would be more future proof.

I've submitted an issue a few days ago with my concerns, but got no response so far. Let me reproduce the issue content here.

So, here's what the documentation says:

To prevent React from touching the DOM after mounting, we will return an empty <div/> from the render() method. The element has no properties or children, so React has no reason to update it, leaving the jQuery plugin free to manage that part of the DOM

So, it suggests using the mount/unmount hooks in order to initialize and destroy the third-party components, however this is not enough to guarantee that the integration will succeed. I'll get more into that later.

It is important to remember that the reconciliation algorithm is an implementation detail. React could rerender the whole app on every action; the end result would be the same.

Currently, if shouldComponentUpdate() returns false, then componentWillUpdate(), render(), and componentDidUpdate() will not be invoked. Note that in the future React may treat shouldComponentUpdate() as a hint rather than a strict directive, and returning false may still result in a re-rendering of the component.

Can you see the problem with that? If I can't really rely on the reconciliation algorithm to not touch the elements React is not supposed to manage, then I have no guarantees that it would be possible to integrate React with stateful third-party components in the future.

Suppose I want to integrate with a very lightweight multi-options autocomplete component that only provides 3 public APIs, a constructor, a desctructor and some onChange hook. It's an stateful component but we don't have direct access to its state so that we can restore it after destroying and recreating it. It opens a menu with several items containing a checkbox and the item label. As you click on the items, checking its checkbox, onChange would be triggered, which we could use to change the state of some ancestor component managed by React.

While responding to the state change event, if React simply decides to re-render the ancestor component, without respecting shouldComponentUpdate, or if the reconciliation algorithm is not smart enough to only perform the required changes, it means it would probably call componentWillUnmount in the autocomplete component wrapper, which would only be able to destroy that component. Then, after componentDidMount we would only be able to initialize the component again, but we would have lost all of its state, like the scroll position and currently selected item and so on. In other words, that means React wouldn't be able to play nice with stateful third-party components. In order to have such a guarantee, we need to have more guarantees from React itself.

The reconciliation algorithm shouldn't be just an implementation detail without any guarantees. shouldComponentUpdate shouldn't be considered just a hint. Otherwise, how are we supposed to wrap third-library components in a reliable way?

Even though I'm pretty excited about VDOM based view components I'm not willing to give up on existing third-party JavaScript components that require direct access to the DOM. React expects all of your components to act like pure functions in the sense they should be able to restore its current state by re-rendering it at any given time, even if they completely removed the mount node's contents. That basically means most UI JS components would simply break when wrapped by a React component since they don't provide such a complete API that would allow us to completely restore its current state.

At this point I'm not really sure I'm ready to give up on third-party components in order to adopt React. But it gets worse. What if I decide I want to move to another software stack some years from now. It's important that I can draw the boundaries so that I can move one small component at a time to the new stack. But if React is not happy with setting such strict boundaries then in that case I'd have to move all at once, which doesn't really work for huge code bases.

If you know of any VDOM based library that provides hard boundaries and a precise diff algorithm, please let me know in the comments below. I'm very interested in using VDOM, but interoperability is so much important for me to give up from it. It allows one to incrementally change its software stack, by mixing different stacks, replacing one component at a time for a while, without having to rewrite the whole application which is not really feasible in most cases.

comments powered byDisqus