On Stimulus Reflex: What and Why


For those who don't already know, StimulusReflex (SR) is a Ruby on Rails gem to empower developers to build more interactive web applications. Although I have been a member of the excellent Discord server, it has taken me a long time to start to wrap my head around how to understand the world with a "SR mindset." In this post, I want to share my current thoughts on how SR fits into the landscape of web development tools and paradigms and why it is a powerful and exciting addition to this landscape for Rails developers.

Apps and Interactivity

While "Web 3.0" is the topic of our times it seems, I still find myself thinking about "Web 1.0" and "Web 2.0" far more often. What were "Web 1.0" and "Web 2.0" and why do people even use these labels? And what does any of this have to do with StimulusReflex?

In simple terms, "Web 1.0" is a label used to describe the an era in the life of the internet where developers and users primarily conceived of the internet and websites as "electronic documents". In this era, stateless REST came to the forefront as the primary paradigm for structuring sites. Users interact with nouns (resources) and each page represents a static and distinct snapshot of the state of the system at that time. Some pages provided forms to allow users themselves to change the state of the system, and they could see the results of those changes by visiting a relevant page after making submitting the form. Web 1.0 is built on links, buttons, and forms and centered on the page as a representation of a document/resource. In this world, pages were the atomic unit (so you could compose pages into “flows” at best) and stateless HTTP meant no client-side continuity. And with a single server, speed was limited by distance and the response handling layers.

"Web 2.0" is meant to capture the era of the internet where developers and users began to push for more interactivity; the era where some web sites became web apps. In the "Web 2.0" era, internet denizens all pushed to make web sites look and feel more like native applications. We pushed beyond the document and began exploring the vast landscape of possibilities that the Web 1.0 tools (links, buttons, forms, REST) provided. This expansion into the frontiers of possibilities aimed to improve 2 key aspects of a user's experience on a web app:

  1. speed, and
  2. continuity

Native applications have long provided users with a sense of near immediate reaction to their inputs, and the flow of state changes to the system felt continuous and integrated. For example, in Excel when you update a cell used in a formula, the spreadsheet immediately updates that formula cell with the newly calculated value. There is no "page reload" to demarcate "state 1" from "state 2" and likewise no delay for such a "page reload." Thus, this state change feels fast and a part of a continuous stream of changes.

We are still, whatever the crypto branding fans might say, squarely in the "Web 2.0" era. Users expect interactive applications, whether on the web or native, and developers are expected to be able to provide such interactivity. So, what does Web 2.0 look like in the Rails world?

Web 2.0 and Rails

To my mind, the single most important Rails feature that begins to provide Web 2.0 functionality to Rails application is Turbo (nee Turbolinks). Originally, Turbolinks provided a simple and straightforward way to developers to take their Web 1.0 sites and push toward Web 2.0 speed and continuity. However, the continuity improvements were less significant than the speed improvements. Yes, you don’t have to refetch assets, but client-side state of components is reset. Moreover, the speed gains are naturally constrained by the Rails request handling flow.

The new Hotwire bundle of tools is the next big step in Rails embracing and empower Web 2.0 applications. Turbolinks becomes Turbo Drive, bringing the "make a request, but in the background" approach to forms in addition to links. Turbo Frames allow you to take slices of your Web 1.0 pages (e.g. User#show) and mix them into other pages (like Post#show). Turbo Streams then provide a mechanism for the server to push targeted page updates to clients. And finally Stimulus provides a tool set and framework for sprinkling client-side interactivity into your application.

The unifying theme to these tools is that they all aim to build on top of and extend the Web 1.0 paradigm. You work with links, buttons, and forms that make HTTP requests to RESTful endpoints that return HTML responses. This is, in my opinion, a powerful and positive set of constraints to help developers build robust and stable applications. And, to be honest, I think in many cases these are appropriate constraints for many problems. I am a big fan of the Hotwire bundle of tools, and I am very excited for the future of Rails as it provides this solid toolset for building Web 2.0 applications.

Turbo(links) gave us some notable speed and continuity improvements but no composition improvements (still stuck at the level of the page). Turbo Frames brought composition improvements by allowing us to compose “pages”. However, there are trade-offs and limits.

Building on top of the Web 1.0 request/response RESTful model means that the Hotwire toolset is deeply bound up in the Rails routing and request handling stack as well as the completely noun-centric perspective of REST. Even Turbo Frames need a standardly defined route and controller action, which means a Frame needs to be able to standalone as a page. This kind of approach has 3 primary consequences:

  1. speed improvements have a cap, as the entire Rails routing and request handling layers must be executed for every interaction,
  2. server-side state updates cannot be kept continuous with client-side state updates, as even Turbo Drive requests will "nuke and pave" client-side state for any page sections updated, and
  3. composition is restricted to nouns, as every interaction must be shaped to fit the RESTful mold.

Now, these are not terrible limitations, but there are certainly applications and features where their pain will be felt more acutely, and this is where StimulusReflex steps up.

Web 2.0 and StimulusReflex

SR provides two related but distinct approaches for building web apps that feel "alive". The first can be seen as a continuation of the Turbo approach (page morphs), the second is more a server-centric reinterpretation of something like React (selector/nothing morphs).

Like Turbo, page morphs maintain the power of a single mental model—the page is a response to a request. Unlike Turbo though, you get additional speed gains (avoiding the routing and middleware layers) as well as major continuity gains (morphdom keeping client-side state alive and continuous). The cost of these gains is introducing a new layer to your server app code (reflexes) that quack like a controller and hand off to controllers but are not controllers. You therefore need to learn what properly belongs in that layer and what in the controller layer. But aside from this, you can continue to work and think in the request->response mindset, and treat SR as Turbo++.

Like React, selector and/or nothing morphs allow you to build and consider “components” as the atomic unit, not the page they are contained within. The page is the atomic unit of REST, and the limitations of such a “large” atomic component go hand in hand with the simplicity of the mental model. In contrast, React has shown that allowing pages to become scaffolding that hold “alive” and “reactive” components brings an exiting new level of interactivity to your users. And this shift can bring newfound developer happiness as well. This happiness is often tampered by the pain of state syncing tho. The second “mode” of SR strikes out to find a middle ground: the aliveness of components but the simplicity of a single source of truth. When you keep REST bound to the page-level, RPC fits naturally at the component level, and riding the same communication channels in both directions for client->server and server->client interactions helps you to shape and build your components in a clean and consistent manner.

For me, the key point is not that Railsy REST can't handle it; it is that it need not. This is where my growing focus on continuity and composition when thinking thru problems comes to the fore. REST is explicitly meant to be discontinuous. For many contexts, this is a useful constraint; for some, however, it is an unnecessary burden. And when you provide a mechanism to build server<->client interactions centered on Verbs (RPC) and not Nouns (REST), you unlock a new kind of composability. This is a key moment in using Stimulus—embracing the power of a controller mapping to a Verb/Behavior and not a Noun. In this way, thinking of Reflexes as Stimulus controllers on the server provides, indeed, a wonderful frame. They provide a structure for encoding Behaviors that span both server and client as first class citizens in your app architecture. And when you can build both Resources and Behaviors with much the same ease, you now have a powerful toolbelt to bring to bear on quite literally any problem space.