Hotwire is a new tech stack released by DHH and the folks at Basecamp. They created and used it when building Hey, their anti-Gmail email system. It consists of 3 components that turn a conventional Rails application into a super fast, reactive and responsive application.
Good tools are created when you build them for yourself to solve real problems you have and a project like Hotwire is no exception to that rule: it is a really nice technology stack that works amazingly well with Ruby on Rails.
Just the same way, our Rails deployment product was built out of our own need to deploy our Rails applications to any cloud and so I wanted to see how Hotwire and its different components deploy with Cloud 66. To do this, I picked a real project to test things out and learned a lot during the process.
Another cool tool I wanted to try was View Component, released by Github. I started really liking Component-based frontend development when I was playing around with React and Gatsby for our main website. Components, combined with a utility-first CSS framework like TailwindCSS, make the perfect mix for fast and yet maintainable development.
If you haven't checked out these projects, take some time and take them for a spin and share your thoughts with us.
Anyway, back to my project. I wanted to mix Hotwire (both Turbo and Stimulus) and View Components together but use them with my normal go-to tools like Devise and Sidekiq.
I don't want to repeat the great work of many awesome developers who helped my project progress. This, hopefully, is the first piece of a multi-piece post on these technologies. In this post I've focussed a solution for a specific issue when using Hotwire with Devise: the use of current_user
in Hotwire.
If you're looking for a solution to showing the form errors on Devise views, please check Chris Oliver's excellent piece.
One of the main components of Hotwire, is Turbo Streams. Turbo Streams are responsible for fetching a rendered view, following redirects, extracting the right Turbo Frame content from the rendered view and changing the page's DOM based on the response from the server. To understand this better, imagine the following scenario in posts/_post.html.erb
<%= turbo_frame_tag dom_id(post) do %>
<%= render 'posts/content', post: post %>
<%= render 'posts/comment', post: post %>
<% end %>
Without getting into the details of content
and comments
partials, this works with Turbo Streams without any issues. However, what if you need to show an Edit link next to a comment only when the author visits the page, and not others? Your posts/comments
partial might look like this (with Devise's current_user
)
<% post.comments.each do |comment| %>
<%= comment.content %>
<%= current_user == comment.author ? 'Edit' : '' %>
<% end %>
(in reality, Edit
would be a link).
While this will render fine with the page, it won't work when the request comes from Turbo Streams. The reason as explained here by DHH is that Turbo Stream renders partials without global references.
The results are no different if you link your current_user
to Rails' native CurrentAttributes
.
To be more precise, in the scenario above, if a Turbo Frame fetches the partial, the render will work as it is triggered by an AJAX request from the browser, which means the request will contain the required Warden session variables. But, and this is important, as soon as you start using Hotwire Broadcasts, the partial will stop rendering with an error like the one below, or even "current_user
is undefined".
Devise::MissingWarden in Home#show Devise could not find the
Warden::Proxy
instance on your request environment
The reason for this is that when a Turbo Stream is sent down the wire triggered by a Broadcast, Turbo Stream doesn't have any context of the request that started the broadcast and therefore the partials being rendered need to be completely self-sufficient (i.e. pass in anything they need as locals
and avoid using global references).
Here is what your post.rb
might look like:
class Post < ActiveRecord::Base
broadcasts
end
Here, broadcasts
triggers a Turbo Stream down the wire when the post is updated. By default it looks for _post.html.erb
to render and send to the client, but since _post.html.erb
contains use of current_user
(inside of comments
) the render fails. To fix this issue, we need to do the following:
- Remove
current_user
fromcomment
and replace it with a local variable, likeuser
- Change our
Post
model to include this variable
The new _post.html.erb
will look like this:
<%= turbo_frame_tag dom_id(post) do %>
<%= render 'posts/content', post: post, user: user %>
<%= render 'posts/comment', post: post, user: user %>
<% end %>
In your non-partial view (like show.html.erb
) you can render the page for the first time like this:
<%= render partial: 'post', locals: { post: @post, user: current_user } %>
Now let's change the model:
class Post < ActiveRecord::Base
after_update_commit -> { broadcast_replace_later_to(self, locals: { user: current_user, post: self }) }
end
Here we replaced a general and default broadcasts
call with a specific after_update_commit
so we have more control over the function that runs when an update is committed. You can repeat the same for delete and create commit hooks as well. In the hook, we are calling broadcast_replace_later_to
which sends a Turbo Stream to the client (using self
to target the dom_it(post)
of our frame) but we are also including user: current_user
as a partial so Turbo can render the partial correctly.
While this works in the browser, you will notice an error when you try to save a post in your console. This is because in the console there is no current_user
. The fix is a bit more tweaking of the above change:
class Post < ActiveRecord::Base
after_update_commit -> { current_user ? broadcast_replace_later_to(self, locals: { user: current_user, post: self }) : nil }
end
The current_user ?
check will skip the update if there is no current_user
which will then fix our console saves.
In the next post, I will dive into making View Components and Stimulus working nicely!