Early in my career, I fancied myself a frontend engineer. Frontend engineering felt like it had more going on that backend or application engineering. The relatively young language of JavaScript was seeing more uptake after the fall of Flash. Seeing the fruits of your labor in a web browser rather than a commandline or GUI was more rewarding to me.

So, we were all writing JavaScript. I remember when the industry had its first consolidation around jQuery when it came to writing cross-browser capable code. MooTools, Prototype.js, et. al faded slowly into obscurity. Shortly after, a surge in new frameworks came about designed more toward writing full applications (single-page applications) in the browser. These were Angular, Backbone.js, Ember, and React (although it wouldn’t be open-sourced for a little while longer).

Taking a Bet

I hitched my wagon at the time to Ember, since I viewed it as the Rails of the frontend: fully-featured, not controlled by a large company, opinionated, and modern. Participating in the Ember community made me several friends and colleagues who I still keep in touch with. Ember saw some popularity for a few years, but was entirely eclipsed by React.

React’s success—in my mind—came from its ability to integrate with existing applications in a more sensible way. It played well with the “sprinkling of JavaScript” approach. You could turn just parts of the page interactive by mounting React components to the right element. It took me some time to realize that how minimal React was made it easier to adopt as a large organization. The onramp to it was smooth and linear. Compare that with frameworks like Angular and Ember, which had a more all-or-nothing approach. The adoption path looked a lot more like a step change than slow integration. This all-or-nothing design meant that the Stranger Fig pattern was much more difficult to apply. Being adaptable to this pattern seems to be an immutable requirement at any large organization.

So, the industry consolidated around React.

Parallel to this, I had joined Gusto in 2016. Gusto had started out on Backbone.js in ~2011 and had made the decision to move to React in 2015. Progress was slow-going, as we didn’t have a team dedicated to the effort. Our efforts to get us off of Backbone.js and onto React were ad-hoc and best effort. We liked the React mental model a bit better, as it prevents a certain class of error that might be easier to make in Backbone.js. React is at its best when it’s forcing the programmer to think about how data flows through the page. Gusto runs payroll, benefits, and HR for small businesses in the US, so the application can mostly be summarized as some of the most complicated forms you might see in a web application. The incidental complexity of payroll and filing taxes can only be reduced so far.

React served us well, but things that were once easy became harder. As we bought more into the ecosystem, simple things began feeling harder. It hit us when adding a single input to a form resulted in a 700-line pull request. Coming from a Rails background, this felt like 690 more lines than we should have needed to write. But, my duties pulled me elsewhere in the company and I began to focus and care less about the frontend of the application.

Starting Something New

My familiarity with React and its ecosystem peaked in 2020. I hadn’t kept up with its development beyond reading Twitter. I thought the push into server-side components was interesting but duplicative for most non-new companies. Nonetheless, React remained what I knew best.

So when I left Gusto mid-2023, we went with React as the means of adding interactivity to our application.1 After a few weeks of building an initial prototype, it was clear that React of 2023 was very different from the React of 2014. The small, discretely mountable components had been replaced by an all-or-nothing approach. It took me some time to figure out how to hack a sprinkling-based approach, since the library seems to really want to drive the entire page these days. Embracing React fully was a non-starter, since the backend was in Rails, the technology that I’ve spent most of my career with.

It was a near-daily occurrence to be fighting React because it was not the one generating the server-side HTML. I only wanted half of what the library had become. It was no longer a gradually-adoptable library, but an all-or-nothing framework. The seed was planted to make a change.

Enter StimulusJS

When we hired our first engineer, he brought new expertise to the company. When giving him the tour of our nascent application, I highlighted my struggles with React. He suggested we use StimulusJS, since it fits well to the business we’re building and gels well with Rails.

Normally, a framework change for a young company is a terrible distraction, but we were in a position where there would never be a cheaper time to make the change as today. Without making the change, we’d put ourselves in a progressively more painful situation as we grew. So we made the decision to cut over and were done within a week.

As a result of switching from React to StimulusJS, we deleted about 60% of our JavaScript or about 2k lines at the time. Much of that was due to needing to do the same thing client-side that we were already doing server-side or just the state juggling that comes with using a client-side library that wants to put the HTML on the page. Interestingly, the amount of JavaScript in our application has remained relatively flat since then.

StimulusJS lets you consolidate more application logic and state to the backend. Sure, you might not get the reactivity of client-side state but client-side state is a lie.

Where are we today?

Today, the amount of JavaScript in our application has remained incredibly small. We have 993 lines of JS and 12 npm packages. You can see where we migrated off of React in August 2023.

We go weeks without authoring any new JavaScript while still delivering great experiences to our customers. Our application never has to pay the penalty of two pageloads: one for the server-rendered HTML and one for the client-rendered HTML. Although we’ve made little to no optimizations, our app has mostly stayed in a “default fast” state.

We’ve discovered some of the sharp edges of StimulusJS, mostly around typos in action names. This first felt like a huge regression when coming from a TypeScript world.

StimulusJS takes a contrarian approach when it comes to JavaScript: it wants you to write as little of it as possible. If we take to heart the messages of The Goal and The Phoenix Project, it’s that lines of code (inventory) are a liability not an asset. We have no JavaScript test frameworks, since all interactivity is exercised through Rails system specs.

After a false start with React in 2023, we’re now on a tech stack that we’re not fighting against and that maps better to our customers’ domain. The result is that we’re able to be a lot more nimble than our competitors, which we think will result in better products for our customers and better business outcomes for ourselves.

Special thanks to Ngan Pham for chastising me years ago to give StimulusJS a try. I blew him off at the time, but he came with receipts when I started writing this post. Lesson: always listen to Ngan.

  1. Our company, Scholarly, is building a Faculty Information System (FIS) for higher ed institutions within the US. Our first product is most similar to performance review software. So, lots of forms and reporting while being light on the interactivity.