Quick Tip

Use caching to speed up data loading in Phoenix LiveView

cacheliveviewcachex

In this post, I want to show you how to leverage cache in Phoenix LiveView to avoid hitting the database twice when mounting a LiveView page. The reason is that the query might potentially be expensive and slow down the page with a bad user experience as a result.

I will do this by using Cachex, but the same principle applies to other libraries and also using pure ETS.

Install Cachex

Begin by adding Cachex to the dependencies.

# mix.exs
def deps do
  [
    {:cachex, "3.4.0"}
  ]
end

And run

mix deps.get

Next step is to start the Cache and can either start it manually where its directly called or like there, in the application.ex among the list of children. In this example, I use the name :data_cache

# lib/tutorial/application.ex
children = [
  # Other children
  {Cachex, name: :data_cache},
]

Load data with cache

With the latest version of LiveView, 0.17, you can now load data in an on_mount-hook. This is exactly what I want to do. Just because I can move som logic from the Live-component and also at a later stage, use the same cache logic for more than one resource.

# lib/tutorial_web/live/init_assigns.ex
defmodule TutorialWeb.Live.InitAssigns do
  import Phoenix.LiveView

  alias Tutorial.Customers

  def load_customers(_params, _session, socket) do
    {
      :cont,
      socket
      |> assign(:customers, Customers.list_customers())
    }
  end
end

And then I can call this in the on_mount hook in the Customers live view like

# lib/tutorial_web/live/customer_live/index.ex
defmodule TutorialWeb.CustomerLive.Index do
  use TutorialWeb, :live_view

  alias TutorialWeb.Live.InitAssigns

  on_mount {InitAssigns, :load_customers}

  def mount(_params, _session, socket) do
    {:ok, socket}
  end
  # other code
end

With this change, I have now moved the loading of customers out from the LiveView into the on mount hook.

I want to achieve 3 things with the caching function:

  1. It needs to be reusable
  2. It should have a find or fetch functionality
  3. I should be able to set TTL (time to leave) since that can vary depending on situation
  @cache_name :data_cache # same as in application.ex
  @default_ttl 1_000 * 60 * 10 # 10 mins
  
  def with_cache(key, fun, ttl \\ @default_ttl) do
    case Cachex.get(@cache_name, key) do
      {:ok, nil} ->
        result = fun.()
        Cachex.put(@cache_name, key, result, ttl: ttl)
        result
      {:ok, result} ->
        result
    end
  end

This means that if the data does not exist in cache, it will execute the passed in function and fetch the data from the database and then put the result in the cache with the given cache key and then return the result.

So, for my example, the final results look like this:

defmodule TutorialWeb.Live.InitAssigns do
  import Phoenix.LiveView

  alias Tutorial.Customers

  @cache_name :data_cache # same as in application.ex
  @default_ttl 1_000 * 60 * 10 # 10 mins

  def load_customers(_params, _session, socket) do
    customers = with_cache("list-customers-cache", (fn -> Customers.list_customers() end))
  
    {
      :cont,
      socket
      |> assign(:customers, customers)
    }
  end

  def with_cache(key, fun) do
    case Cachex.get(@cache_name, key) do
      {:ok, nil} ->
        result = fun.()
        Cachex.put(@cache_name, key, result, ttl: @default_ttl)
        result
      {:ok, result} ->
        result
    end
  end
end

NOTE that I wrap the list_customers in a function:

(fn -> Customers.list_customers() end)

Otherwise it would of course be executed right away.

Phoenix Bolerplate

Generate a Phoenix Boilerplate and save hours on your next project.

6715 downloads
Try now
OR

SAAS Starter Kit

Get started and save time and resources by using the SAAS Starter Kit built with Phoenix and LiveView.

Subscribe for $39/mo to geat ahead!

Learn More