Today I set out to the task of upgrading a set of views crafted with the React library. The upgrade was necessary because one part of a view using React stopped working in Chrome Version 48.0.2564.116 (64-bit). I don’t know why this happened, I didn’t find anything related to it and as it stands this was probably the only project on the Internet still running on this old version of React anyway. If not, you may or may not find this post useful. Or not. But at least it might be entertaining.

The post is divided in two major parts:

  • a rough description of my day in all of its beauty
  • a short reflection on this experience

Disclaimer

This is not a plain Javascript project. Had this been a plain Javascript project, things would have been a lot easier. I mean, still maybe not that easy, but you know, somewhat more agreeable. Keep this in mind while you read the first part.

The upgrade path

Element Factories

As of React 0.12 React components need a factories in order to be created. It’s all described here. Upon reading the article I couldn’t help but wonder how long it will take until React will have its own AbstractSingletonProxyFactoryBean but let’s just not get into this right now.

Along with a few deprecation warnings prompting me that a few methods had moved from React to ReactDOM, this mainly meant that code that looked like this:

1
2
3
4
React.unmountComponentAtNode(container);
React.renderComponent(
    <BookTeeTimeModal issuerId={issuerId} time={time} facets={facets} lat={lat} lng={lng} locationName={locationName} isotime={isotime} teeTimeId={teeTimeId} greenFeeIds={greenFeeIds} restrictedSearch={restrictedSearch} book={book} bookingInfo={bookingInfo} cancellationPolicy={cancellationPolicy} quote={quote} />, container
);

now had to look like this:

1
2
3
4
var element = React.createElement(BookTeeTimeModal, {issuerId: issuerId, time: time, facets: facets, lat: lat, lng: lng, locationName: locationName, isotime: isotime, teeTimeId: teeTimeId, greenFeeIds: greenFeeIds, restrictedSearch: restrictedSearch, book: book, bookingInfo: bookingInfo, cancellationPolicy: cancellationPolicy, quote: quote});

ReactDOM.unmountComponentAtNode(container);
ReactDOM.render(element, container);

That still wasn’t too hard, simply requiring react-dom, figuring out the correct paths to give to requirejs and off to the next step, which is where the real fun begins.

JSX transpilation

React uses JSX views to… well, to be React. So those need to be “transpiled” (that’s the hipster word for “compile”) to plain Javascript.

(at this point, if you come from Javascript-land, as you continue reading, you will start to slowly but surely raise your eyebrows and hear yourself mumble the words “what the hell” repeatedly)

So when you have an SBT project the way to go is to use a plugin for sbt-web which is the library that lets you set up pipelines for Javascript, CSS etc. And so for React you use the sbt-reactjs plugin which takes care of JSX transpilation.

Except that this project is no longer maintained. So then off you go looking at the forks of that GitHub project, hoping that anyone has taken over the project. While I am at it, it would be very nice if GitHub made it possible to switch over the ownership of a project to another fork instead of continuing to promote the initial (but no longer maintained) version of that project.

Looking at https://github.com/ddispaltro/sbt-reactjs/network it looks like lglossman had a fork that switched over to the babel compiler since the JSTransform tool initially provided by Facebook is, you guessed it, no longer maintained.

So full of hope and renewed energy you set out to use this fork, which means that since it’s not published you need to build it yourself, i.e. cloning the project, configuring build.sbt so that the project is compiled using “maven style” (publishMavenStyle := true) and publish the library locally using sbt publish. The target project having already a custom local repository for this kind of situation all that is left to do is to copy the locally published artifacts from the local ivy repository to that repository.

What happens next is fun:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    spray.json.DeserializationException: Expected Int as JsNumber, but got null
        spray.json.package$.deserializationError(package.scala:23)
        spray.json.ProductFormats$class.fromField(ProductFormats.scala:62)
        com.typesafe.sbt.jse.SbtJsTask$JsTaskProtocol$.fromField(SbtJsTask.scala:92)
        spray.json.ProductFormatsInstances$$anon$2.read(ProductFormatsInstances.scala:56)
        spray.json.ProductFormatsInstances$$anon$2.read(ProductFormatsInstances.scala:46)
        spray.json.JsValue.convertTo(JsValue.scala:31)
        com.typesafe.sbt.jse.SbtJsTask$$anonfun$com$typesafe$sbt$jse$SbtJsTask$$executeSourceFilesJs$1$$anonfun$7.apply(SbtJsTask.scala:225)
        com.typesafe.sbt.jse.SbtJsTask$$anonfun$com$typesafe$sbt$jse$SbtJsTask$$executeSourceFilesJs$1$$anonfun$7.apply(SbtJsTask.scala:224)
        scala.collection.LinearSeqOptimized$class.foldLeft(LinearSeqOptimized.scala:111)
        scala.collection.immutable.List.foldLeft(List.scala:84)
        com.typesafe.sbt.jse.SbtJsTask$$anonfun$com$typesafe$sbt$jse$SbtJsTask$$executeSourceFilesJs$1.apply(SbtJsTask.scala:223)
        com.typesafe.sbt.jse.SbtJsTask$$anonfun$com$typesafe$sbt$jse$SbtJsTask$$executeSourceFilesJs$1.apply(SbtJsTask.scala:221)
        scala.util.Success$$anonfun$map$1.apply(Try.scala:206)
        scala.util.Try$.apply(Try.scala:161)
        scala.util.Success.map(Try.scala:206)
        scala.concurrent.Future$$anonfun$map$1.apply(Future.scala:235)
        scala.concurrent.Future$$anonfun$map$1.apply(Future.scala:235)
        scala.concurrent.impl.CallbackRunnable.run(Promise.scala:32)
        scala.concurrent.impl.ExecutionContextImpl$$anon$3.exec(ExecutionContextImpl.scala:107)
        scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
        scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.pollAndExecAll(ForkJoinPool.java:1253)
        scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1346)
        scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
        scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

Can you spot the problem? Yeah, me neither.

Google is misleading on this one, it points to e.g. [this closed issue])(https://github.com/sbt/sbt-less/issues/38) which is about a less module, but has nothing to do with the root cause in this case.

So really the only way to make sense out of this error is to run SBT with the debugger turned on:

1
sbt -jvm-debug 5005

and to connect to it with your favourite IDE, and to download the source of the sbt-js-engine which blows up and to put a breakpoint here, to see what it is that we get back from the evaluation of the Javascript script that takes care of JSX transpilation

And that gives you the following insight:

1
{"results":[{"source":"/Users/manu/workspace/foo/bar/app/assets/agentAgentBooksTeeTimeModal.jsx","result":null}],"problems":[{"message":"unknown: The @jsx React.DOM pragma has been deprecated as of React 0.12\n> 1 | /** @jsx React.DOM */\n    | ^\n  2 | \n  3 | // https://github.com/umdjs/umd/blob/master/templates/returnExports.js\n  4 | (function (root, factory) {","severity":"error","characterOffset":null,"source":"/Users/manu/workspace/foo/bar/app/assets/agent/AgentBooksTeeTimeModal.jsx"}]}

(so there probably is a problem in the way the sbt-reactjs plugin reports errors, or something).

This time Google is your friend. It turns out that the /** @jsx ... */ annotation that was previously required for JSX transpilation to work [https://github.com/skratchdot/react-bootstrap-multiselect/issues/5](crashes babel). Alright then, removing it indeed fixes the issue. Let’s go to the next step.

Stricted DOM nesting validation

At first, a warning:

[Error] Warning: validateDOMNesting(...): <p> cannot appear as a descendant of <p>. See BookTeeTimeModal > p > ... > p.

So I can’t have a paragraph somewhere inside of a paragraph, which makes sense. And that’s good and all, but where does this nesting happen in the 425 lines of this component I didn’t write on my own? It would be nice if the stacktrace would give me a clue as to where to look for this nesting… at this point I have no other choice than to search for p’s in the component and evaluate its execution in my head.

There were a few more instances of invalid nesting in the project, namely related to tables, all related to invalid HTML, such as

Warning: validateDOMNesting(...): <tr> cannot appear as a child of <table>. See Buddies > table > tr. Add a <tbody> to your code to match the DOM tree generated by the browser

But now at least the DOM is clean.

transferPropsTo is deprecated

As described here, transferPropsTo is no longer. As a result the next error message is:

1
    [Error] TypeError: this.transferPropsTo is not a function. (In 'this.transferPropsTo', 'this.transferPropsTo' is undefined)

This is a fairly easy one to fix. Namely, this:

1
2
3
4
5
{this.transferPropsTo(
    <select id={this.props.id} className="form-control">
        {this.props.children}
    </select>
)}

turns into that:

1
2
3
<select {...this.props} id={this.props.id} className="form-control">
     {this.props.children}
</select>

After this step, things seem to work.

And that’s it

Finally, things start to work again, and lo and behold, the project works again in Chrome Version 48.0.2564.116 (64-bit).

Reflection

Everyone who has been in the IT business for a while knows that upgrades are not fun. Today was not really fun, even if I may have made it sound so. Everything keeps on breaking and while at it you don’t really know when the flow of stacktraces will quiet down.

In one way, today’s experience reminded me of Eclipse (the IDE). Jetbrains won the race mainly because they made one consistent product that didn’t break at each upgrade. I switched to that IDE in 2008 and never looked back because back then at least upgrading Eclipse meant spending half a day trying to get all of your plugins to work again.

But perhaps it’s not so much the upgrade which is painful here. In fact, React itself is farily well documented, with changelog and all, and with small articles detailing breaking changes. That’s not the case for most libraries out there. The real painful part of the process is caused by everything around React, including the build tools.

Would things have been easier if the build tool was directly a Javascript tool and not an SBT wrapper around a Javascript tool? Maybe so, but I can’t answer this question because I don’t work on a 100% pure Javascript project at the moment. Though I would suspect similar problems to arrise - after all, the root of the complexity lies in the interaction of all the moving parts.

So I think there are two things at play here: the high speed at which Javascript libraries evolve and a culture that does not favour backwards compatibility nor maintenance (see Generation Javascript) coupled with React’s “library” approach that leads to the “moving parts” problem (see Javascript fatigue).

After all, this was just one not so fun day, and I’m ok with this - it isn’t the first time and certainly won’t be the last. But I’m lucky enough to have the expertise required to get through such an upgrade without loosing my mind and literally flipping the table, although my distinct impression is that the “upgrade pain” gets worse as I get older, despite the fact that I know more year by year. In my previous life as a Java developer, things tended to get easier with time, because I had unconsciously memorized the stacktraces and knew fairly well that some arcane error message meant that I needed to add or remove a cryptic annotation here or there.

These days things seem to have changed. I’ve been more or less in constant touch with Javascript and its ecosystem for a solid 8 years now and yet things don’t seem to get easier, to the contrary. So I don’t know, maybe it’s just me getting more conservative / impatient / you name it or maybe, just maybe, it’s the ecosystem that gets harder to work with, especially in the recent 2-3 years.

I think that for someone who just gets started on the job an upgrade task of this kind of task might feel a lot more frustrating. Perhaps so frustrating that they might flip the table, throw everything away and rewrite it from scratch.

One thing seems increasingly clear to me: this way of building software is not sustainable. What can we do?