Real Time Notification System with Hotwire, in Rails 7
Tags: hotwire, rails, redis
Published: 2022-04-13 15:18:20

**DISCLAIMER**
Time ago i wrote an article here, on how to build a notifications system with Rails and Redis, from scratch. This new one it's a quick update of that one.

If you don't know what i'm talking about, and are curious on how to accomplish same thing without Hotwire, here you go:

{% embed https://dev.to/matiascarpintini/real-time-notification-system-with-sidekiq-redis-and-devise-in-rails-6-33l9 %}

---

Rails 7 was a huuuge update. I'm not going to talk about that in this post but Hotwire. [Hotwire](https://hotwire.dev/) became -officially- part of Rails 7. Now you can have real-time interactions, SPA looking Rails projects, without even write a single line of JS.

**TL;TR**
Hotwire can be understood as an umbrella/approach. Inside this thing you will find 2 little boys, Turbo, that replaces TurboLinks, and Stimulus, which is in their own words: *A modest JavaScript framework for the HTML you already have* or, the thing that we're going to use when we need to listen some button clicks :p

### Hands on 👊
I'm going to use a clean project, you can [skip this](#how-to-notifications) if you want. Therefore:
`$ rails new notifications_with_hotwire -d=postgresql -T`

???
`-d=postgresql` specify the DB that we're going to use. By default, Rails uses sqlite3.

`-T` skips test files.

To make it more real, let's include [Devise](https://github.com/heartcombo/devise), and [Tailwind](https://github.com/rails/tailwindcss-rails) with their icons library, [Heroicons](https://github.com/bharget/heroicon):
```$ bundle add devise tailwindcss-rails heroicon```

*Now we need to setup that gems...*
For tailwind and heroicons just run `$ rails tailwindcss:install && rails g heroicon:install`

For Devise, run `$ rails g devise:install && rails g devise User` and follow their notes.

I'm also going to create a resource that will dispatch notifications later on: `$ rails g scaffold Post title body:rich_text user:references`. Don't forget to assign user on create,

```ruby
before_action :authenticate_user!, except: [ :index, :show ]
def create
  @post = current_user.posts.new(post_params)
  ....
```

And of course, specify the relationship on the User model with: 
```ruby
has_many :posts
```

Finally, let's move to what you're looking for 😉

## How to notifications
I want to create notifications for different resources, such as posts, comments or likes. For this, i'm using a polymorphic reference. If you're not familiar with, read about that [here](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations).
```$ rails g model Notification item:references{polymorphic} user:references viewed:boolean```

Just add a default value for viewed field on the migration:
```ruby
t.boolean :viewed, null: false, default: false
```

Build a basic nav and place the notifications count badge on it with something like:
```ruby
<%= link_to notifications_path, class: "bg-gray-800 p-1 rounded-full text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" do %>
  <span class="sr-only">View notifications</span>
  <%= heroicon "bell", variant: :outline, options: { class: "h-6 w-6" } %>
  <%= tag.div id: :notifications_count do %>
    <%= render "notifications/count", count: current_user.unviewed_notifications_count %>
  <% end %>
<% end %>
```

You may noticed that unviewed_notifications_count doesn't exists on our User model so, let's define that as a class method
 on `user.rb`:
```ruby
has_many :notifications
def unviewed_notifications_count
  self.notifications.unviewed.count
end
```

And the `app/views/notifications/_count` partial is needed for Turbo. Going to explain this below.
```ruby
<%= tag.div id: :notifications_count do %>
  <% if count > 0 %>
    <span class="h-6 w-6 flex items-center justify-center bg-red-500 rounded-md text-sm">
      <%= count > 9 ? "+9" : count  %>
    </span>
  <% end %>
<% end %>
```

Here's the thing of the whole article (`notification.rb`). Turbo actions. Let's take the first callback to explain what's going on.
1. *broadcast*: now turbo handle the sttuff we built on the [previous article](https://dev.to/matiascarpintini/real-time-notification-system-with-sidekiq-redis-and-devise-in-rails-6-33l9). TD;TR: ApplicationCable to stablish connection between the user and the server, create and send jobs to the background to be performed async with Redis.
2. *prepend/replace/remove*: simple JS actions
3. *to*: the target (AKA [stream](https://turbo.hotwired.dev/handbook/streams)). In this case, i'm establishing connection with the specific signed user.
4. *target*: simple HTML (or [turbo_frame](https://turbo.hotwired.dev/handbook/frames)) element used to perform the JS action.

```ruby
scope :unviewed, ->{ where(viewed: false) }
default_scope { latest }

after_create_commit do 
  broadcast_prepend_to "broadcast_to_user_#{self.user_id}", 
    target: :notifications
end

after_update_commit do 
  broadcast_replace_to "broadcast_to_user_#{self.user_id}", 
    target: self
end

after_destroy_commit do 
  broadcast_remove_to "broadcast_to_user_#{self.user_id}", 
    target: :notifications
end

after_commit do
  broadcast_replace_to "broadcast_to_user_#{self.user_id}", 
    target: "notifications_count", 
    partial: "notifications/count", 
    locals: { count: self.user.unviewed_notifications_count }
  end
end
```

Yup, the _latest_ scope didn't exists either, i like to define that on `application_record.rb`.
```ruby
scope :latest, ->{ order("created_at DESC") }
```

In `application.html.erb` create the stream that allows the previous snippet "talk" with the signed user (without this everyone's getting everyone's notifications 😛)
```ruby
<% if user_signed_in? %>
  <%= turbo_stream_from dom_id(current_user, :broadcast_to) %>
<% end %>
```

Now let's put notifications-related stuff on `concerns/notificable.rb`.
1. _included_ will append where we import this concern. It sets a relationship and have a simple callback that runs once we _create_ a new resource (post in this case) object.
2. If that resource model have the user_ids method, it will create the notifications for that users.
```ruby
module Notificable
  extend ActiveSupport::Concern

  included do
    has_many :notifications, as: :item, dependent: :destroy
    after_create_commit :send_notifications_to_users
  end

  def send_notifications_to_users
    if self.respond_to? :user_ids
      self.user_ids&.each do |user_id|
        Notification.create user_id: user_id, item: self
      end
    end
  end
end
```

So then, when we want to create notifications for a new resource, we just need to include that concern on our resource model, in this case `post.rb`
```ruby
include Notificable

def user_ids
  User.where.not(id: self.user_id).ids
end
```

Finally, to show the notifications on our application UI, let's respond to /notifications path on `notifications_controller.rb`
```ruby
class NotificationsController < ApplicationController
  before_action :authenticate_user!

  def index
    @notifications = current_user.notifications
    @notifications.update(viewed: true)
  end
end
```

Don't forget to notify the controller about this new path on `config/routes.rb`
```ruby
resources :notifications, only: [ :index ]
```

And there you go. The notifications index :)
views/notifications/index.html.erb
```ruby
<div class="space-y-4">
  <p class="text-lg leading-6 font-medium text-gray-900 flex justify-between">
    Notifications
  </p>
  <ul class="border-t border-b border-gray-200 divide-y divide-gray-200" id="notifications">
    <%= render @notifications %>
  </ul>
</div>
```

The previous `render @notifications` will look for the `views/notifications/_notification.html.erb` partial.
And i like to render a new partial here, since we have a polymorphic relation.
```erb
<li class="py-4" id="<%= dom_id(notification) %>">
  <%= render "notifications/#{notification.item_type.downcase}", notification: notification %>
</li>
```

That previous `render` will look for `views/notifications/_post.html.erb` (in this case). If you create notifications for, let's say, likes then you need to create a new partial, called `_like`. And customize the notification for that new resource.
```erb
<p class="text-gray-900">
  <%= notification.item.user.email %> just posted:
  <%= link_to notification.item.title, notification.item, class: "underline font-medium" %>
</p>
```

Here's the [repo](https://github.com/matias-carpintini/notifications-with-hotwire) with everything :)

Oh, and i'm building a job board for devs that want to work remotely. There's already couple job offers for Rails devs, if you're looking for job, [check this out](https://workby.io).

Bye.