We estimate that 80% of the pain points in web development are the direct result of maintaining state on the client. Even without considering the complexity of frameworks like React, how much time have you lost to fretting about model validation, stale data, and DOM readiness over your career?
* This is at least 98% true.
Imagine if you could focus almost all of your time and attention on the fun parts of web development again. Exploring the best way to implement features instead of worrying about data serialization and forgotten user flows. Smaller teams working smarter and faster, then going home on time.
Designing applications in the StimulusReflex mindset is far simpler than what we're used to, and we don't have to give up responsive client functionality to see our productivity shoot through the roof. It does, however, require some unlearning of old habits. You're about to rethink how you approach persisting the state of your application. This can be jarring at first! Even positive changes feel like work.
When you access a page in a StimulusReflex application, you see the current state of your user interface for that URL. There is no mounting process and no fetching of JSON from an API. Your request goes through the Rails router to Action Pack where your controller renders your view template and sends HTML to the browser. This is Rails in all its server-rendered glory.
Only once the HTML page is displayed in your browser, StimulusReflex wakes up. First, it opens a websocket connection and waits for messages. Then it scans your DOM for elements with
data-reflex attributes. Those attributes become event handlers that map to methods in Stimulus controllers. The controllers connect events in your browser to methods in your Reflex classes on the server.
In a Reflex method you can call ActiveRecord, access data from Redis or the Rails cache, and set instance variables that get picked up in your view. After the Reflex method is complete, the Rails controller's action method is called and any instance variables set are passed on to the Rails controller's action method. The controller then passes its instance variables to the view template render engine itself. In this way, a Reflex is sort of like a
before_action callback that fires before the controller even kicks in.
We find that people learn StimulusReflex quickly when they are pushed in the right direction. The order of operations can seem fuzzy until the light bulb flicks on.
This document is here to get you to the light bulb moment quickly.
One of the most common patterns in StimulusReflex is to pass instance variables from the Reflex method through the controller and into the view template. Ruby's
||= (pronounced "or equals") operator helps us manage this hand-off:
def update_value@value = element[:value]end
def index@value ||= 0end
<div data-controller="example"><input type="text" data-reflex-permanentdata-reflex="input->Example#update_value"><p>The value is: <%= @value %>.</p></div>
When you access the index page, the value will initially be set to 0. If the user changes the value of the text input, the value is updated to reflect whatever has been typed. This is possible because the
||= will only set the instance variable to be 0 if there hasn't already been a value set in the Reflex method.
Of course, instance variables are aptly named: they only exist for the duration of a single request, regardless of whether that request is initiated by accessing a URL or clicking a button managed by StimulusReflex.
When StimulusReflex calls your Rails controller's action method, it passes any active instance variables along with a special instance variable called
@stimulus_reflex which is set to
true. You can use this variable to create an if/else block in your controller that behaves differently depending on whether it's being called within the context of a Reflex update or not.
def indexunless @stimulus_reflexsession[:balls_left] = 3endend
In this example, the user is given 3 new balls every time they refresh the page in their browser, effectively restarting the game. If the page state is updated via StimulusReflex, no new balls are allocated.
This also means that
session[:balls_left] will be set to 3 before the initial HTML page has been rendered and transmitted.
session object will persist across multiple requests; indeed, you can open multiple browser tabs and they will all share the same
session.id value on the server. See for yourself: you can create a new session using Incognito Mode or using a 2nd web browser.
We can update our earlier example to use the session object, and it will now persist across multiple browser tabs and refreshes:
def update_valuesession[:value] = element[:value]end
def indexsession[:value] ||= 0end
<div data-controller="example"><input type="text" data-reflex-permanentdata-reflex="input->Example#update_value"><p>The value is: <%= session[:value] %>.</p></div>
In general, you should be careful not to abuse the session object in a production app. First, sometimes sessions get lost or reset when people move between devices. It's also possible to accidentally reuse the same session variable key in multiple places, resulting in confusion and a frustrating bug hunt. Don't underestimate your ability to sabotage yourself in the future!
The Rails session object is perfect for prototyping during development, before potentially moving to the Rails cache, Redis or your database to store anything important. You have the power and flexibility to decide which data is ephemeral and which needs to survive the loss of a data centre, coast or continent. 🦖 👾 🌪
One of the most under-appreciated modules in Rails is
ActiveSupport::Cache which provides the underlying infrastructure for Russian doll caching. It can be called directly and that it has a solid offering of utility methods such as
decrement for numeric values.
The Rails cache provides a consistent interface to a key/value storage container that behind the scenes can be anything from an in-memory database or temporary files to Redis or Memcached hosted on a different machine.
You can access the Rails cache easily from anywhere in your application:
Behold the sexy: we're using the user's
session.id to help us build a unique key string to look up the current value. The structure of the key is free-form, but the convention is to use colon characters to build up a namespace, terminating in a unique identifier. For example:
If no key exists, it will evaluate the block, store the value for that key and return the value in one convenient, atomic action. Bam!
If you're planning to do more than set an initial simple value for the fetch default, it's good idiomatic Ruby to move to the
do..end form of block declaration:
<pre><%=Rails.cache.fetch("fortune") do`fortune | cowsay`end%></pre>
The most common and powerful persistence mechanism you'll call from a Reflex method is also the most familiar.
The Reflex class makes use of the
data-id attributes from individual Todo model instances, and the new Rails
& safe navigation operator (available since Ruby 2.3) to make short work of mapping events on the client to your permanent data store.
todos_controller.rb only makes a single ActiveRecord query to render the current state of the view template. Well-designed StimulusReflex applications leave the heavy-lifting associated with state changes to the Reflex class.
If Redis is your Rails cache store, you're already one step ahead!
Depending on your application and the kind of data you're working with, calling the Redis engine directly (through the
redis gem, in tandem with the
hiredis gem for optimal performance) from your Reflex methods allows you to work with the full suite of data structure manipulation tools that are available in response to the state change operations your users initiate.
Using Redis is beyond the scope of this document, but an excellent starting point is Jesus Castello's excellent "How to Use the Redis Database in Ruby".
Kredis is an exciting new addition to the Rails family. It provides a higher-level abstraction over the low-level Redis commands, allowing developers to "interact with them as coherent objects rather than isolated procedural commands."
Kredis also adds new methods to your ActiveRecord models, allowing you to treat Redis data structures as attributes on your model.
If you're running Ruby 2.7 or later, Kredis is likely a superior option to using the Rails cache or calling the Redis gem directly.
Since Kredis can optionally use its own separate Redis database instance, you might decide to use a different key expiration strategy from a cache. Caches can safely evict the least recently used keys, whereas model attributes and ActiveJob queues should be configured to scream as loudly as possible if they run out of room for more data.