GrammAcc

GrammAcc

CacheMeIfYouCan - Small Library for Computed Assigns in LiveView

I created a hex package to provide a reusable abstraction for computed properties in LiveViews.

The specific use-case that was giving me a lot of trouble was a complex tabular data view with pagination, dynamic sort on any column, and multi-select filters.

The streamed table rows need to be refetched based on the page no, page size, sort, and the filters, and the total page count also needs to be computed from a db query based on the page size and filters.

The README and moduledoc for the package have a simplified example showing the same use-case.

The idea behind this library is to provide something that can achieve the same kind of automagical recomputation of memoized properties as React’s useMemo, but in a more structured way that will fit into the LiveView server-side state model.

Cached/computed assigns are defined with a @reactive_cache module attribute, which takes a keyword list of four keys:

use CacheMeIfYouCan.LiveViewCache

@reactive_cache [
  key: :user_list,
  deps: [:page_no, :page_size, :filters, :sort],
  default_value: [],
  cb: &get_users/1,
]

def get_users(socket) do
  stream_async(socket, :user_list, fn ->
    users =
      from(User)
      ...
      |> Repo.all()
    {:ok, users, reset: true}
  end)
end

Recomputation is triggered by the use of wrapper functions assign_cached/3 and assign_new_cached/3. These functions pass their arguments through to the corresponding functions in the Phoenix.Component module but they also bust the cache for any computed properties that depend on the assigned key.

For example, if we have the @reactive_cache defined above, and we use

def handle_params(%{"page_no" => page_no}, _uri, socket) do
  {:noreply, assign_cached(socket, :page_no, page_no)}
end

Then the :page_no will be assigned like normal, but the get_users/1 function will also be called and the :user_list stream will be recomputed in the background.

We also still have complete control over when we trigger a recomputation because we can use the regular Phoenix.Component.assign/3 to assign the :page_no without fetching users if we want to. This is useful for example if we had a polling event or something that updated an assign, but we only wanted to recompute during a specific lifecycle stage or based on a user interaction event.

The assign_new_cached/3 function has a bit more going on. This function wraps the Phoenix.Component.assign_new/3 call and passes on its arguments, but it also automatically sets the default value for any computed properties that depend on the key being assigned. So, if we used

def mount(_, _, socket) do
  socket
  |> assign_new_cached(:page_no, fn -> 1 end)
  |> assign_new_cached(:page_size, fn -> 25 end)
  |> assign_new_cached(:sort, fn -> :asc end)
  |> assign_new_cached(:filters, fn -> [] end)
end

Then the :user_list assign would be initialized to the default value in the @reactive_cache of [] and the get_users/1 function would be called to load the initial stream.

The assign_new_cached/3 function also checks to make sure that all of the deps are in the assigns before it triggers the computation, so it won’t run four times because we call it on four deps, and it also only runs the computation for the connected render. This works well for the common use-case of initializing state with async tasks on mount. And we still have the flexibility to initialize things however we need to by simply using the Phoenix.Component.assign_new instead and setting up everything manually.

To clarify the behavior of the assign_new_cached/3 function, the following are roughly equivalent:

Without assign_new_cached/3

  def mount(_, _, socket) do
    if connected?(socket) do
      socket
      |> assign_new(:page_no, fn -> 1 end)
      |> assign_new(:page_size, fn -> 25 end)
      |> assign_new(:filters, fn -> [] end)
      |> assign_new(:sort, fn -> :asc end)
      |> assign_new(:users_list, fn -> [] end)
      |> get_users()
    else
      socket
      |> assign_new(:page_no, fn -> 1 end)
      |> assign_new(:page_size, fn -> 25 end)
      |> assign_new(:filters, fn -> [] end)
      |> assign_new(:sort, fn -> :asc end)
      |> assign_new(:users_list, fn -> [] end)
    end
  end

With assign_new_cached/3

  def mount(_, _, socket) do
    socket
    |> assign_new_cached(:page_no, fn -> 1 end)
    |> assign_new_cached(:page_size, fn -> 25 end)
    |> assign_new_cached(:filters, fn -> [] end)
    |> assign_new_cached(:sort, fn -> :asc end)
  end

I tried to design the api in a way that would work with existing constructs, so if you’re already fetching data with async assigns/background tasks and you have some helper functions to wrap up the recompute task and run it during different event handlers, then it should be pretty easy to update the code to use this library and reduce the boilerplate.

I’m still in the process of refactoring my site with these patterns, so I haven’t pushed this into production yet. You can try it out if you’re interested, but keep in mind this hasn’t been battle-tested yet. I’m also still trying to figure out how to write tests for a LiveView that doesn’t have an endpoint and plug router set up. :upside_down_face:

This is my first Elixir lib on Hex, so feedback and criticism is very welcome and appreciated. Cheers!

Where Next?

Popular in Announcing Top

hauleth
PhoenixBakery is library for Phoenix 1.6 (and later) that provides modules implementing Phoenix.Digester.Compressor. There are currently ...
New
ananthakumaran
An ExUnit formatter to visualize test execution and find bottlenecks in your test suite. I created a small library called ex_unit_spa...
New
phcurado
Zoi is a new schema validation library for Elixir. It’s inspired by Zod from the JavaScript ecosystem, bringing a similar functional API...
New
rahultumpala
Hello Elixir community, I work primarily on Java and other object oriented programming languages and often found it difficult to read an...
New
hauleth
It is library created by me and @abc3. It is simple client for DuckDB, but with small twist when compared with other libraries out there ...
New
zteln
Hello Elixir community, I’ve written a small embedded key-value database in Elixir called Goblin. It’s based on the Log-Structured Merge...
New
rbino
Hey folks! I’m happy to introduce TigerBeetlex, an Elixir client for TigerBeetle, the financial transactions database. I’ve been working...
New
munksgaard
flakify is an igniter installer that allows you to quickly get a Nix flake-based development shell up and running for your Elixir/Phoenix...
New
victor23k
LiveViewPortal is a small library that allows third-party sites to embed LiveView apps. The library is distributed as an NPM package. On...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement