Conditional Rendering With Turbo Stream Broadcasts
14 Aug 2021A very common pattern in Rails development is for a view to contain checks for things like current_user.can?(:take_some_action)
. These types of checks are common, especially in B2B applications that implement role-based permissions powered by a solution like Pundit.
So, naturally, when a Rails developer begins working with Turbo Stream broadcasts, they wonder how to access the current_user
or other session-level variables. The short answer? You can’t.
Partials rendered via a stream broadcast have to be free of global variables or they’ll throw an error because they are not rendered within the context of a specific request. Without that request context variables like current_user
will always be undefined.
While we can’t access global variables from a broadcast, we can, with creative use of Turbo Frames, still deliver real-time broadcasts that retain access to key variables like current_user
.
To demonstrate the concept today we’re going to build a simple application that displays a list of Spies.
Each spy will have a mission; however, not everyone will be able to see the spy’s mission. Only visitors with secret clearance can see the mission field. Everyone else will see a “Classified” string in place of the mission.
When a new spy is created, we’ll broadcast an update to any viewers of the spy list. Viewers with clearance will see the mission for the newly created spy, viewers without won’t.
The end result will work like this:
This article will be most useful for folks who are already comfortable with Ruby on Rails and who have a little experience with Turbo already. If you’re comfortable with Rails but you’ve never used Turbo before, an article like this might be a better introduction.
As usual, you’ll find the full code for this article on GitHub.
Ready? Let’s get started.
Application Setup
Let’s get our application setup first, from your terminal:
Setup Frames
We’re going to make some updates to the Spy index view that our scaffold generated to incorporate a few Turbo Frames and prepare to use broadcasts to append new records.
Open spies/index.html.erb
and update it:
Here we’re subscribing to the spies
turbo stream channel, setting an id on the container div for our rendered spies, and then looping through and rendering the spy partial for each spy.
The default scaffold generator doesn’t create a spy
partial for us, so we need to create that:
And fill it in with:
We’re also rendering a new spy_link
partial, which doesn’t exist yet. Go ahead and create it now:
And fill that it in:
Wrapping the new spy link in a dedicated frame means that when the link is clicked, we can replace the link on the page with the content returned from the call to /spies/new
.
To make that work, we’ll need to update new.html.erb
next to render its content in a matching <turbo-frame>
tag:
In your browser, head to http://localhost:3000/spies and you should be able to click on the New Spy link and see the spy form render. Fill in a name and give your spy a mission, submit the form, and see that the form disappears but the spy doesn’t render on the page and the new spy link doesn’t come back.
Our spy creation form POST succeeds, but we aren’t sending back a useful response from the controller or broadcast any updates on the Spies channel.
Let’s finish up spy creation by tackling those issues next.
Handling spy creation
When our new spy form is submitted, a turbo stream request is sent to the server, and right now our server isn’t responding back with anything useful. Let’s fix that first by heading to the SpiesController
and updating the create method:
Now our create method will respond to turbo_stream
requests by replacing the new_spy
frame with the content in the spy_link
partial.
Try creating a new spy now and, after a successful POST, the form should be removed from the page and the New Spy link should appear in its place.
Finally, we need to append the newly created spy to the list of spies without the user needing to refresh the page.
We’ll do that with a broadcast from the Spy
model:
This callback runs after a new spy is created and sends a broadcast on the Spies channel containing a Turbo Stream element that looks like this <turbo-stream action="append" target="spies">
. Nested within the stream element is the with the content of the spy
partial
Now when we create a new spy, the new record should instantly be added to the list of spies, no refresh required.
With all of this in place, we can move to the really fun part of this article, conditionally rendering content for different users from a Turbo Stream broadcast.
Let’s see how it works.
Conditional rendering from a broadcast
First, let’s remember that our original goal was that each spy’s mission would only be displayed to users with secret clearance. Everyone else should just see “Classified” displayed in place of the mission text.
This is simple to do on the initial page load, but since we’re broadcasting new agent creation via a WebSocket connection, things are a little more complicated. Without access to the user’s session, we’ll have to get creative.
To meet our secret clearance requirement, we’re going to use a technique that I first saw described on the Hotwire discussion forums that allows us to bypass the need to access session-level variables in partials rendered by a broadcast.
We’ll implement this requirement by adding a check for a session variable into the spy
partial that our broadcast renders. When the session variable exists, the user will see mission text, otherwise the user will not.
This session variable is a rough mock of what, in a real application, might be accessed through something like Current.user
To start, we’ll add a helper method to our ApplicationController
to make it easy to access this session variable:
Then we’ll reference that helper method in spies/_spy.html.erb
like this:
We’ll need a way to set the value of clearance
for different users. To do that, we’ll add a new endpoint to the SpiesController
.
First, update our routes file:
Then in the controller, add the new clearance
method:
Then, we’ll add a way to the set the clearance session variable from the index page in our extremely secure spy application.
Before we move on, create a new spy from the Spies index page and see that secret clearance is always false when broadcasting through a channel, even when the current session has secret clearance set to true.
This is happening because the value of session
when rendering from a broadcast is an empty hash. The session isn’t read from the user’s current session, and so our secret_clearance
check will always fail.
To fix this issue and ensure users always see what they’re supposed to see, we’ll start by creating a new partial:
Fill that new partial in with the following content:
Here we’re taking advantage of the fact that Turbo Frames can be given an src
attribute which causes the frame load its content from a URL when it is inserted onto the page.
In this case, the frame will retrieve the content from spies#show
. We’ll update that method in the SpiesController
next:
Our show method checks the request headers to see if the request includes a turbo-frame
header. When the frame header is present, the existing spy
partial is rendered.
Finally, update our model broadcast to render the new spy_frame
partial:
With these changes in place, everything should work. To test it out, first make sure you’ve got two sessions active (open one in an incognito window). In one browser window, give the user clearance, in the other don’t.
Create a new spy in either session, and see that the broadcast triggers an update in both windows. In the window with clearance, the mission should display, in the window without clearance, Classified displays instead, like this:
So what’s going on here?
- The broadcast sends the empty,
src
powered frame from thespy_frame
partial to all subscribed users and that frame is rendered safely since it does not request any session variables - When the frame is inserted onto each user’s page, the
src
attribute on the frame immediately triggers an HTTP request tospies/:id
- Since the request is sent from the browser to an endpoint on the server, we have access to all the session data we expect in the rendered response
- The server responds to the request with the content of the
spy
partial which includes a<turbo-frame>
with an id that matches the frame id of the empty frame in step 1. - Since the frame ids match, Turbo replaces the content of the frame with the response from the server, and the new spy appears in the list.
With this trick, we can now make use of Turbo Stream broadcasts in situations where the rendered content changes based on non-local variables. Magical.
Wrapping up
Today we looked at a potential solution to a very common blocker for folks building more complex applications with Hotwire.
Real-world use cases for this approach include conditionally rendering edit/delete controls based on user permissions, or not displaying certain information to users without appropriate permissions inside of a multi-user account.
Other recommended approaches to these types of problems typically rely on using inline styles to hide/show content but that is not an acceptable solution for all use cases, particularly in more sensitive applications where the data being included in the markup but hidden on the page isn’t acceptable.
Although this method works well for some use cases, it certainly isn’t without downsides or limitations, which include:
- Each broadcast now requires an additional round trip to the server. We’re gaining a lot of flexibility in return for additional load on our server but we are adding load and potentially slowing down our application. Be sure you need the extra flexibility gained by this approach.
- The complexity of your code is increased. Following what’s happening in a broadcast takes a little more effort, even for the simplest possible use case outlined in this article.
- If you’re using this method to update content that already exists on the page, you might see flickering issues. There are ways to address this, including one described in the discussion on this approach on the Hotwire forum. Depending on your UX needs, you could also include an “updating content” loading state in the frame broadcast.
When considering taking this approach in a production application, think about the tradeoff you’re making. We learned about a hammer today, but that doesn’t mean every problem related to session variables and broadcasts is a nail.
Sometimes, you might be better off with another approach like:
- Broadcasting a generic “refresh to see changes” message
- Using inline styles to conditionally show/hide information
- Adjusting the content rendered in a broadcast to eliminate the need to access session variables
- Scoping your streams appropriately to ensure that every recipient of a particular broadcast should see what is being broadcast
- Not broadcasting updates at all. Sometimes, real-time visibility into changes isn’t helpful or necessary and folks will be comfortable seeing the updated information on the next page load.
While not without tradeoffs and potential drawbacks, this technique demonstrates some of the power that can be unlocked with creative applications of Turbo Streams and Frames in Rails applications. With Turbo still in early days, we can expect to see even more powerful techniques developed in the coming years to continue to push Turbo and Rails applications powered by Turbo ahead.
As always, thanks for reading!