Using Turbo Streams with Kredis
Tags: rails, hotwire
Published: 2022-07-23 10:23:00

When using Turbo Streams, we usually want to trigger WebSockets updates from our domain models using the [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb) methods included in ActiveRecord. 

But what if we want to add a feature which only uses Redis to persist data? How can we broadcast Turbo streams when the value of a Redis key changes?

Let's anwser those questions by making a simple live counter with Turbo Streams and [Kredis](https://github.com/rails/kredis).

## Making a counter with Turbo Streams and Kredis
Let's generate a Rails 7 app without ActiveRecord (and with Tailwind just to make our counter look good):
```bash
$ rails new hotwire-counter --css tailwind --skip-active-record
$ cd hotwire-counter
```

To use Kredis, we need to uncomment `gem "kredis"` in our Gemfile and install it:
```bash
$ bundle
$ bin/rails kredis:install
```

Let's generate a controller:
```bash
$ rails g controller counter show
```

And define our actions:
```ruby
# app/controllers/counter_controller.rb
class CounterController < ApplicationController
  def show
    render :show, locals: {
      counter_count: counter_count.value
    }
  end

  def increment
    counter_count.increment

    respond_to do |format|
      format.turbo_stream { render_partial_update }

      format.html { redirect_to root_url }
    end
  end

  def decrement
    counter_count.decrement

    respond_to do |format|
      format.turbo_stream { render_partial_update }

      format.html { redirect_to root_url }
    end
  end

  private
    # We use Kredis to store and retrieve the counter's state
    def counter_count
      @counter_count ||= Kredis.counter "counter:count"
    end

  	def render_partial_update
      render turbo_stream: turbo_stream.update("counter-count",
        counter_count.value
      )
    end
end
```

Let's add our routes:
```bash
Rails.application.routes.draw do
  root to: "counter#show"

  post "/increment", to: "counter#increment", as: :increment
  post "/decrement", to: "counter#decrement", as: :decrement
end
```

Let's update `app/views/counter/show.html.erb`:
```erb
<div class="flex flex-col items-center mx-auto my-0 mb-4 text-lg gap-2">
  <div>
    <span id="counter-count">
      <%= counter_count %>
    </span>
  </div>

  <div class="flex gap-2">
    <%= button_to "+", increment_path, class: "px-8 py-4 text-white bg-black rounded" %>
    <%= button_to "-", decrement_path, class: "px-8 py-4 text-white bg-black rounded" %>
  </div>
</div>
```
Let's start the server:
```bash
$ bin/dev
```

Now we should be able to use our counter in the browser:
![Rendering Turbo streams](https://cdn.kapwing.com/final_62d98fa44f292f00e3b30c79_372958.gif)

## Broadcasting Turbo streams
Since we're not using ActiveRecord, we won't be able to use the `Turbo::Broadcastable` methods to broadcast updates.

But this won't be a problem because we can use the [`Turbo::StreamsChannel`](https://github.com/hotwired/turborails/blob/main/app/channels/turbo/streams_channel.rb) directly with methods like `Turbo::StreamsChannel.broadcast_update_to` (defined in the [`Turbo::Streams::Broadcasts`](https://github.com/hotwired/turborails/blob/50402466cd85f27a8515b3865797d12ac741c26d/app/channels/turbo/streams/broadcasts.rb) module).

It works with the [`turbo_stream_from`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb#L52) helper used in the views to subscribe to the `Turbo::StreamsChannel` and receive updates from specific [streams](https://api.rubyonrails.org/classes/ActionCable/Channel/Streams.html).

Let's try to use `Turbo::StreamsChannel.broadcast_update_to` in our controller to broadcast updates:
```ruby
# app/controllers/counter_controller.rb
class CounterController < ApplicationController
  ...
  def increment
    ...
    respond_to do |format|
      format.turbo_stream do 
        render_partial_update

        broadcast_update
      end
      ...
    end
  end

  def decrement
    ...
    respond_to do |format|
      format.turbo_stream do 
        render_partial_update

        broadcast_update
      end
      ...
    end
  end

  private
    ...
    # This will broadcast the following Turbo stream:
    # <turbo-stream action="update" target="counter-count" >
    #   <template>counter_count.value</template>
    # </turbo-stream>
    def broadcast_update
      Turbo::StreamsChannel.broadcast_update_to "counter",
        target: "counter-count"
        content: counter_count.value
    end
end
```

We also need to use `turbo_stream_from` in `app/views/counter/show.html.erb` to receive the updates:
```erb
<%= turbo_stream_from "counter" %>
...
```

Now let's open our page in two separate tabs:
![Broadcasting Turbo streams](https://cdn.kapwing.com/final_62d9920753c84d0093527ac1_719169.gif)

Nice, it worked!

But hold on! There's a problem with our approach. Can you figure out what it is? 

Our controller shouldn't be responsible for broadcasting page changes. That's because they can't be transmitted if we use the counter directly from the back end. It means we won't see the result on the page if we increment the count from the console, for example.

Can you think of a better solution?

A better approach would be to model our counter by including [`Kredis::Attributes`](https://github.com/rails/kredis/blob/main/lib/kredis/attributes.rb) and using the [`kredis_counter`](https://github.com/rails/kredis/blob/main/lib/kredis/attributes.rb#L65) macro with an `after_change` callback:
```ruby
# app/models/counter.rb
class Counter
  include Kredis::Attributes

  kredis_counter :count,
    key: "counter:count",
    after_change: :broadcast_update

  class << self
    def count
      new.count
    end
  end

  def broadcast_update
    Turbo::StreamsChannel.broadcast_update_to "counter",
      target: "counter-count",
      content: count.value
  end
end
```

Now that we moved the broadcasting logic in the `Counter` model, we can broadcast updates whenever the count changes. It means we can update the count from the console and see the result on the page.

Let's continue by refactoring the `#counter_count` method in our controller:
```ruby
# app/controllers/counter_controller.rb
class CounterController < ApplicationController
  ...
  private
    def counter_count
      @counter_count ||= Counter.count
    end
end
```

Now let's open the console and update the count:
![Broadcasting Turbo streams from Kredis](https://cdn.kapwing.com/final_62daa18ece24a80058e9df94_558862.gif)

Et voilà, there you have it!

## Wrap up
We can use the [`Turbo::StreamsChannel`](https://github.com/hotwired/turbo-rails/blob/main/app/channels/turbo/streams_channel.rb) with [`Kredis::Attributes`](https://github.com/rails/kredis/blob/main/lib/kredis/attributes.rb) to broadcast Turbo streams when the value of a Redis key changes.