I added realtime likes in less than an hour with Phoenix's LiveView

Apr 26, 2020

I wanted to share just how wonderful LiveView is and why you need to go and try it right now!

If you don’t know what LiveView is, here’s the short of it. LiveView is an Elixir library created for use in the Phoenix Framework, that provides a wrapper around the view layer to provide convenient web sockets that enable serverside rendered realtime updates to the frontend with minimal overhead and without having to write JS like React, Vue or Angular.

While announcing the release of Phoenix 1.5.0 a video was posted about how to make a twitter clone in 15 minutes by Chris McCord. Using this as the primary example I was able to add likes to this blog in around 100 lines of code (not including setting up LiveView) and no JS!! 🤘

Before I go into how I did it, you should know I’m not going to cover how to set up LiveView in this topic since I feel there are probably other better examples anyways. Knowing that please be sure to have either generated a new 1.5.0+ project using the –live option or set up your project to support LiveView beforehand.

Here’s a quick break down of things you should also be aware of:

  • I don’t want to require a login to use this feature. I want the general public to engage in this site and I don’t think requiring a login is a good incentive.
  • I don’t care that you can click the like button more than once, frankly I think using Likes and Reposts as a metric is a horribl e idea anyhow. That said you can click this button as many times as you want.
  • I’m not an expert on LiveView, there are very likely better ways to do this. This is because this is the first time I’ve used LiveView. That in its self should be a testament to how great this is to get off the ground running.

Now with the formalities out of the way lets get started. The first thing I did was to create the PostLive module.

Let’s do that and implement our first callback the mount/3 function in our new PostLive module.

# lib/my_app_web/live/post_live.ex

defmodule MyAppWeb.PostLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(
        _params,
        %{
          "post_id" => post_id,
          "post_likes" => post_likes
        },
        socket
      ) do
    if connected?(socket),
      do: Phoenix.PubSub.subscribe(MyAppWeb.PubSub, "post:like:#{post_id}")

    {:ok, assign(socket, post_likes: post_likes, post_id: post_id)}
  end
end

So we start by using the use MyAppWeb, :live_view macro which assuming you have setup your LiveView correctly should be available.

Next, I used the @impl true This is a new feature of Elixir 1.5.0.

Elixir v1.5 introduces the @impl attribute, which allows us to mark that certain functions are implementation of callbacks: https://elixir-lang.org/blog/2017/07/25/elixir-v1-5-0-released/

After that, we create our callback taking 3 params. The first one I ignore since I’m not using a router for this. The next is our sessions param. In this, it will include a map with two “String” keys. Its been noted very well in the docs that:

the :session uses string keys as a reminder that session data is serialized and sent to the client. So you should always keep the data in the session to a minimum. I.e. instead of storing a User struct, you should store the “user_id” and load the User when the LiveView mounts.

In this case, we have two items in our map we care about. First, the Post id which we will look up and also use to subscribe to our pubsub with and our current likes count.

The 3rd param is the socket used.

Moving on to the body of the function we see I first check to see if the socket is connected via connected?. If so we then subscribe to our PubSub using our app’s Pubsub server MyAppWeb.PubSub and a topic that we build as a string providing our Post’s ID. post:like:#{post_id}

A word of caution: While I have not seen any issue yet with having multiple subscriptions like this you should be mindful of how many you create. In my use case of this on my Post’s index view, I may end up creating as many as 21 subscriptions for just one user looking at that page. This has the potential to become an issue and I will likely look to consolidate my subscriptions later. For the time this is how I got it to work the way I wanted it to.

I also did it this way because I wanted to gradually introduce LiveView into my current app and I didn’t want to have to completely rewrite the whole way I render my blog. This allows me to keep my current setup at the compromise of a few extra subscriptions.

After we subscribe we then return back an :ok status using a tuple providing our new state using the assigns function as the second arg passing in the post_likes and post_id values.

With that setup, we can go to a section in our view that we want to render our button.

# lib/my_app_web/templates/post/index.html.eex

<%= for post <- @posts do %>
  <%= link to: Routes.post_path(@conn, :show, post) do %>

  <%= live_render(@conn, MyAppWeb.PostLive, session: %{"post_id" => post.id, "post_likes" => post.likes_count}) %>
<% end %>

You will most likely have some comprehension in your template that enumerates over your posts like so.

In there we add our live_render/3 call which takes our @conn, the LiveView module in question MyAppWeb.PostLive and our session map %{“post_id” => post.id, “post_likes” => post.likes_count}

At this point, we are ready to mount our LiveView. If you reload the page though you will likely see we are missing our render function. Let’s add that next to our LiveView module.

# lib/my_app_web/live/post_live.ex

@impl true
def render(assigns) do
  ~L"""
  <button class="" phx-throttle="500" phx-click="inc_post_likes">
    <i class="fas fa-heart"></i> <%= @post_likes %>
  </button>
  """
end

Again we use the @impl true, then we create our render callback which takes just the assigns param. It may look like at first that we are not using the assigns but let me assure we are in our new LiveView sigil ~L.

From there we see just plain old HTML markup and an Embedded elixir (Eex) tag. Closer inspection of our button tag, you will notice I’m using two attributes the phx-throttle and the phx-click.

Adding the phx-throttle we are limiting the push to our server per click so that we don’t get some rogue a hole trying to spam the server. I mean they still can spam but it won’t be a very efficient bot

Next adding the phx-click we add a binding to which event we want to emit upon clicking the button.

Now our button needs a way to respond to the clicks. Lets add that next.

# lib/my_app_web/live/post_live.ex
alias MyApp.Blog
alias MyApp.Blog.Post

@impl true
def handle_event("inc_post_likes", _value, %{assigns: %{post_id: post_id}} = socket) do
  {:ok, post_likes} = Blog.inc_likes(%Post{id: post_id})
  {:noreply, assign(socket, :post_likes, post_likes)}
end

For that we add a handle_event callback which pattern matches on the event emitted by the click “inc_post_likes”.
We ignore the values arg because, honestly I don’t know. I think this comes from say a form post or somewhere else. Anyhow its not the data we are looking for so we ignore it.

Next, we pattern match on our socket to pluck out the post_id.

From there we call a function Blog.inc_likes/1 in our Blog context which we have not yet implemented which we will do next so bare with me. It will. Also note that we alias in both our Blog and Post modules, don’t forget that.

From there we take our new post_likes value and respond with a :noreply tuple and our new state via our assign call passing in the socket and our updated post_likes. Again this may not be the best way to do this but hey it works now so whatever.

So at this point, we need to add our inc_likes function to our context. Let’s do that.

def inc_likes(%Post{id: id}) do
  {1, [post]} =
    from(p in Post, where: p.id == ^id, select: p)
    |> Repo.update_all(inc: [likes_count: 1])

  Phoenix.PubSub.broadcast(
    MyApp.PubSub,
    "post:like:#{id}",
    {:post_liked, post.likes_count}
  )

  {:ok, post.likes_count}
end

Here using the Repo.update_all we ensure our atomic update (“to be honest I’m not sure how this ensures it’s atomic I’m just taking Chris McCord’s word for it. Maybe I’d do a blog post later on this”)

From there we broadcast our update on our pubsub. Phoenix.PubSub.broadcast passing in our string for that given topic using the post’s id like we seen when we subscribed.

Then we return our :ok tuple with our new updated post’s likes_count.

We may need to make sure our Post Schema has a likes_count field on it. If you have not done so let’s add that really quick.

# some ecto migration
defmodule MyApp.Repo.Migrations.AddLikes do
  use Ecto.Migration

  def change do
    alter table("posts") do
      add :likes_count, :integer, default: 0
    end
  end
end

# lib/my_app/blog/post.ex

defmodule MyApp.Blog.Post do
  schema "posts" do
    ...
    field :likes_count, :integer, default: 0
  end
end

Everything should be working now, but there is one last thing missing. Notifying other subscribers there has been a change to our likes count.

To update the view for other subscribers we add this last callback.

# lib/my_app_web/live/post_live.ex

@impl true
def handle_info({:post_liked, post_likes}, socket) do
  {:noreply, assign(socket, :post_likes, post_likes)}
end

Here we pattern match on the PubSubs broadcast :post_liked tuple from our Blog’s inc_likes call. it includes the post_likes, next we take in the socket. From there we just reply with our updated state again using the assign function

And there you have it the like button will update in (“soft”) realtime If you were to open up another tab of this page and click the likes button or if someone else were to like this post you would see it update on the fly.

And that’s it! Now I have likes on the blog and snapshots section of my site without writing any JS.

photo by European Southern Observatory

Go for it, show this post as little or as much love as you want