8
.
04
.
2024
8
.
04
.
2024
Hotwire
Ruby on Rails
Backend
Frontend

Turbo Streams and current user

Mateusz Bilski
Ruby Developer

Rails Turbo Streams allow you to send updates directly from the server to the client, updating the client's view of the data without any need to reload the page.

Using Rails with Turbo Streams is an efficient way to manage the server → client communication. It reduces the load on the server and improves the user experience by eliminating the need for frequent page reloads. The changes in the server data are pushed directly to the client, ensuring that the user always views the most current state of the data. This real-time functionality can be instrumental in creating applications where immediate data updates are critical.

Turbo Streams could be particularly useful in applications like chat systems, live dashboards, stock updates, collaborative editing tools, notification systems, and order tracking platforms where real - time updates are crucial.

In this article, I aim to demonstrate the basic functionality of Turbo Streams, primarily based on a tutorial provided by the hotrails.dev website. I'll also place extra emphasis on enhancing the practices outlined in this tutorial and highlight what to avoid. In the second part of the article, I will focus on managing session context and highlight how to render user - personalized components when using streams.

Following Hotrails tutorial

As outlined in the hotrails tutorial, transforming your application to be live can be as simple as adding a few lines of code. Firstly, you need to add the turbo_stream_from tag to the view. In this example, I opted to use this on the index page to display tweets that users can create within the Tweet model. It’s important that the view shows recently added tweets on the top.

# app/views/tweets/index.html.haml
= turbo_stream_from 'tweets'

#tweets
  = render @tweets

Following the tutorial, you need to add an after_create_commit block to the desired model, in this case, Tweet. This block will broadcast the new tweet to the 'tweets' stream after it's successfully created and committed to the database. There are some other methods (append, replace, remove etc.), but to add newly created tweets, obviously I use broadcast_prepend_to.

class Tweet < ApplicationRecord
  belongs_to :user

  validates :content, presence: true, length: { maximum: 280 }

  after_create_commit { broadcast_prepend_to 'tweets' }
end

And voilà! It should work every time a new tweet is created.

Things I don’t follow from Hotrails tutorial

The solution provided by the Hotrails tutorial is indeed quick and gets the job done with sugar syntax of Rails. However, based on my production proofed experience, it can generate some problems in certain scenarios. In the following sections, I would like to draw attention to these issues and propose a more efficient solution. The proposed solution is suitable for commercial projects as it avoids common problems and facilitates easier debugging. Let me outline few points I realized working with turbo streams:

  • Using ActiveRecord callbacks can be sometimes bad idea, for many reasons, like hiding more complex logic, overloading models, testing purposes and unexpected complexity on bulk operations. That’s why it’s better to avoid placing broadcast inside after_create_commit callback (or any other ActiveRecord callback).
  • No errors. Mentioned callback above has one more weakness. If rendering broadcast partial fails for some reason, in my experience case it was lack of devise’scurrent_user method, it doesn’t leave almost ANY trace of the error. Turbo Streams context doesn’t have any information of session, especially who is the receiver of the broadcast. It just doesn’t broadcast, which is sometimes hard to notice. However, there will be some logs left, there are hard to notice in the jungle of other logs Rails produces. It doesn’t give you clear 500 error on the page and browser’s console, nor logging middleware like Sentry, New Relic etc.
  • Rendering views in a model - with Turbo it’s possible to render everywhere. This gives developers flexibility, but also responsibility. My suggestion is not to render directly from model, because it breaks the principal of MVC framework, as well as overloads models.

Avoid using turbo streams in ActiveRecord callbacks

Of course, here is my idea of implementation, which gets rid of all three issues presented above.

# app/controllers/tweets_controller.rb
def create
  create_service = Tweets::CreateService.new(current_user, tweet_params)

  if create_service.call
    redirect_to tweets_path, notice: 'Tweet was successfully created.'
  else
    render :new
  end
end

# app/services/tweets/create_service.rb
module Tweets
  class CreateService
    def initialize(user, params)
      @user = user
      @params = params
    end

    def call
      tweet = @user.tweets.new(@params)
      result = tweet.save
      broadcast_tweet(tweet) if result

      result
    end

    private

    def broadcast_tweet(tweet)
      Turbo::StreamsChannel.broadcast_prepend_to(
        'tweets',
        target: 'tweets',
        partial: 'tweets/tweet',
        locals: { tweet: tweet, from_stream: true }
      )
    end
  end
end

# app/views/tweets/index.html.haml
= turbo_stream_from 'tweets'

= button_to 'New Tweet', new_tweet_path, method: :get

#tweets
  = render @tweets, from_stream: false

# app/views/tweets/_tweet.html.haml
%article{ id: dom_id(tweet)}
  %nav
    %h6= tweet.user.email
    %time= time_ago_in_words(tweet.created_at)
  = tweet.content

Introducing User context

Let's say it's not enough and you want to use the user context. For this particular example, let's say an App is only available to signed-in users, and there are also admin users. If the user is the author, or has an admin role, display a 'Delete' button allowing to delete the tweeted post. Easy, let's add it!

%article{ id: dom_id(tweet)}
  %nav
    %h6= tweet.user.email
    %time= time_ago_in_words(tweet.created_at)
  = tweet.content
  .actions
    - if current_user.admin? || current_user == tweet.user
      = link_to 'Delete', tweet, data: { turbo_confirm: 'Are you sure?', turbo_method: :delete }

Should it work? Of course - there is nothing wrong, but unfortunately not with Turbo Stream. Turbo Stream doesn’t have any user nor controller context. Let’s see the error:

Yes, Turbo Stream uses empty dummy controller just in case of get basic rendering context of the application. Let’s check what’s inside request and controller:

Even request host is set to example.com.

Empowering user context - combining Streams and Frames

Thanks to Turbo being included in Rails, there is an option to use Turbo Frame to obtain data within the client's session. This is called decomposing (tutorial here). In the Turbo Stream partial, it’s just enough to render turbo_frame with an src attribute and provide the action that renders view with the context. Let’s see how it looks, I also placed the action in another partial.

# app/views/tweets/_tweet.html.haml
%article{ id: dom_id(tweet)}
  %nav
    %h6= tweet.user.email
    %time= time_ago_in_words(tweet.created_at)
  = tweet.content
  .actions
    - if from_stream
      = turbo_frame_tag dom_id(tweet, 'actions'), src: actions_tweet_path(tweet)
    - else
      = render 'actions', tweet: tweet

# app/views/tweets/_actions.html.haml
- if current_user.admin? || current_user == tweet.user
  = turbo_frame_tag dom_id(tweet, 'actions') do
    = link_to 'Delete', tweet, data: { turbo_confirm: 'Are you sure?', turbo_method: :delete }

# app/controllers/tweets_controller.rb
def actions
  @tweet = Tweet.find(params[:id])
  render partial: 'actions', locals: { tweet: @tweet }, layout: false
end

Let me explain what is happening in the code presented above. There is a variable called from_stream and each time I render tweet, I check its value. If the view is rendered by turbo stream, the variable is true and turbo_frame_tag with desired DOM id and src is rendered. Otherwise, it’s just standard partial render (in case of entering or reloading page). Let’s then see what turbo_frame_tagproduces to the HTML.

<turbo-frame id="actions_tweet_84" src="/tweets/84/actions"></turbo-frame>

What Turbo on client’s side does, it fetches desired URL - like a standard GET request, so server knows client’s session - including current_user. Complete solution works like on the animation below.

Summary

In conclusion, the mission to understand and implement Turbo in the Visuality project has been successfully completed. The experience provided a wealth of knowledge which I've shared in this article to help others navigating similar challenges. The complete source code for the application can be found on my GitHub here.

Mateusz Bilski
Ruby Developer

Check my Twitter

Check my Linkedin

Did you like it? 

Sign up To VIsuality newsletter

READ ALSO

Ula Sołogub - SQL Injection in Ruby on Rails

The Deadly Sins in RoR security - SQL Injection

14
.
11
.
2023
Urszula Sołogub
Backend
Ruby on Rails
Software
Michal - Highlights from Ruby Unconf 2024

Highlights from Ruby Unconf 2024

14
.
11
.
2023
Michał Łęcicki
Conferences
Visuality
Cezary Kłos - Optimizing Cloud Infrastructure by $40 000 Annually

Optimizing Cloud Infrastructure by $40 000 Annually

14
.
11
.
2023
Cezary Kłos
Backend
Ruby on Rails

Smooth Concurrent Updates with Hotwire Stimulus

14
.
11
.
2023
Michał Łęcicki
Hotwire
Ruby on Rails
Software
Tutorial

Freelancers vs Software house

14
.
11
.
2023
Michał Krochecki
Visuality
Business

Table partitioning in Rails, part 2 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
Ruby on Rails

N+1 in Ruby on Rails

14
.
11
.
2023
Katarzyna Melon-Markowska
Ruby on Rails
Ruby
Backend

Turbo Streams and current user

29
.
11
.
2023
Mateusz Bilski
Hotwire
Ruby on Rails
Backend
Frontend

Showing progress of background jobs with Turbo

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Ruby
Hotwire
Frontend
Backend

Table partitioning in Rails, part 1 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend
Ruby on Rails

Table partitioning types - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend

Indexing partitioned table - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
SQL Views in Ruby on Rails

SQL views in Ruby on Rails

14
.
11
.
2023
Jan Grela
Backend
Ruby
Ruby on Rails
Postgresql
Design your bathroom in React

Design your bathroom in React

14
.
11
.
2023
Bartosz Bazański
Frontend
React
Lazy Attributes in Ruby - Krzysztof Wawer

Lazy attributes in Ruby

14
.
11
.
2023
Krzysztof Wawer
Ruby
Software

Exporting CSV files using COPY - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Ruby
Ruby on Rails
Michał Łęcicki - From Celluloid to Concurrent Ruby

From Celluloid to Concurrent Ruby: Practical Examples Of Multithreading Calls

14
.
11
.
2023
Michał Łęcicki
Backend
Ruby
Ruby on Rails
Software

Super Slide Me - Game Written in React

14
.
11
.
2023
Antoni Smoliński
Frontend
React
Jarek Kowalewski - ILIKE vs LIKE/LOWER - Postgres Stories

ILIKE vs LIKE/LOWER - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Ruby
Ruby on Rails
Postgresql

A look back at Friendly.rb 2023

14
.
11
.
2023
Cezary Kłos
Conferences
Ruby

Debugging Rails - Ruby Junior Chronicles

14
.
11
.
2023
Piotr Witek
Ruby on Rails
Backend
Tutorial

GraphQL in Ruby on Rails: How to Extend Connections

14
.
11
.
2023
Cezary Kłos
Ruby on Rails
GraphQL
Backend
Tutorial

Tetris on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Backend
Frontend
Hotwire

EURUKO 2023 - here's what you've missed

14
.
11
.
2023
Michał Łęcicki
Ruby
Conferences