Mind blown - part four

Apologies - I missed a week again.

This time my excuse was I was writing a piece on implementing authentication using SuperTokens in a Rails app, and the night before I was due to publish it, I made some change somewhere and broke it. I couldn't see what I'd done wrong, even looking through the git logs, so I figured it wouldn't be much of a how-to guide if it's that flakey. It seems to be working now, but I need to rewrite the article, so here's something I had stashed away for a later week.

Mind blown recap

Over the last few weeks, I've been writing about how truly responsive layouts can work in CSS, how Sveltekit organises server-side and client-side Javascript in a fantastic, logical, way. And how reactivity is just the Observable pattern reimplemented. Rails actually has all the pieces needed to do something similar to Sveltekit, but it just needs a bit of reorganisation.

Introducing reactiveresource

So I thought I better get started on it. Using TurboFrames, TurboStreams and ViewComponents, we can build something that:

  • organises your views into discrete components, with the server-side code, the client-side code, the visual structure and the styling all in one place
  • is internally reactive, so when the user does something on the front-end, the back-end responds and the front-end updates to show that response
  • is externally reactive, so when someone else makes a change to something that you are looking at, your view automatically updates
  • does not require you to track all the dependencies between your views and your models, nor manage lots and lots of client-side state.

However, I'm writing this the right way, by building an actual application, extracting the common stuff and putting it in a library. Which means that, at present, the library is completely empty!

But, in the spirit of "Design by ReadMe" (see also "Design by Press Release") I've written a description of how I would like the API to look. Currently, I have no idea if I can achieve that, but it's always good to start with the end in mind.

And, of course, as I extract functionality, I will release it into the repo, ready for everyone to use.

What's the big idea?

The basic idea, as mentioned above, is that everything - server code, client code, structure and styling, sits in one place. Similar to a React or Vue component but on both sides of the network divide. Of course, you may end up with huge component files - but that is just the code telling you that it needs breaking into pieces.

Show me the code

A component will look something like this.

class Person::Card < ReactiveResource::Component
  represents :person

  state :selected, :boolean, redraw: true

  template <<-HTML
    <div <%= classes :card, selected: :selected %>>
      <div <%= classes :details %>>
        <div><%= image_tag person.avatar.url %></div>
        <div>
          <p>First name: <%= person.first_name %></p>
          <p>Last name: <%= person.last_name %></p>
        </div>
        <div>
          <%= content %>
        </div>
      </div>
      <div <%= classes :footer %>>
        <input type="checkbox" <%= bind :checked, :selected %>></input>
        <% if person.can_be_poked_by?(user) %>
          <button <%= react_with :poke %>>Poke me</button>
        <% end %>
      </div>
    </div>
  HTML

  styles <<-CSS
    .card {
      background-color: #eee;
    }
    .card .selected {
      border: 1px solid #A00;
    }
    .card .details {
      display: flex;
      justify-content: space-between;
    }
    .card .footer {
      display: flex;
      justify-content: space-between;
    }
  CSS

  def poke
    person.has_been_poked_by! user
  end

  def toggle_selection
    selection = selection.blank? ? "selected" : nil
  end
end

And is used like this:

<%= component "person/card", person: some_person, user: current_user do %> 
  <p>Some additional person related stuff</p> 
<% end %>

We have a "card" component that both represents a "person" object and knows who the current user is. It has access to all the attributes of a person, plus it has its own state - a selected attribute - which is specific to this user and the display they can see on-screen.

The HTML for the card shows the first name and last name, a section for any extra content passed in by the caller, and a footer that has a checkbox, bound to our "selected" attribute. And then, depending on permissions, there might be a "Poke me" button that reacts_with :poke. What this means is that when the button is clicked, ReactiveResource intercepts that click and calls the component's server-side poke method. The poke method does whatever it does, the person object broadcasts that it has changed and anyone who is viewing that person gets an automatic refresh of any and all components that have registered an interest in that person.

Unlike standard Hotwire and Turbo-Rails, this is done automatically, so you do not need your models to keep track of which views (partials) require updating. And ReactiveResource also knows who is viewing each person and what their component's internal state is (for example, the selected attribute), so when it sends out the broadcasts, it can do so in full knowledge of the permissions available (are you allowed to click that "poke me" button?) and whether that particular user has selected the person.

Vapourware

Of course, none of this is written yet. Well, I've started, but it's not working in any meaningful way. So this is essentially vapourware.

But I am sure it can work; my only questions are around the scalability. But then again, I only need to deal with thousands of concurrent users, not millions, and I'm confident it will handle that easily. Background tasks are your friend.

So check out the repository and stay tuned, as I'm actively working on this for my side-project. Plus I suspect it will also make its way into my day-job (which is heavily used with many concurrent users) as well.

Rahoul Baruah

Rahoul Baruah

Rubyist since 1.8.6. Freelancer since 2007, dedicated to building incredible, low-cost, bespoke software for tiny businesses. Also CTO at Collabor8Online.
Leeds, England