Making Hotwire and Devise play nicely

Making Hotwire and Devise play nicely for Rails applications.

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::Proxyinstance 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:

  1. Remove current_user from comment and replace it with a local variable, like user
  2. 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!

More Articles:

Try Cloud 66 for Free, No credit card required