Sending a single value might be ok, but imagine you need to also render Todo author avatar and you end up sending multiple values just to rebuild the Todo and User and Avatar schemas by hand. This quickly becames unmanagable and extremaly prone to errors. Not to mention that even with single inserted_at field it can be only sent as a binary so you need to convert it to proper DateTime which is just another layer of complexity. And if you render another field but forget to send it back you will only notice when you try to edit the Todo as the first render will work just fine.
I don’t agree the cache doesn’t make sense - with a long lists you can still benefit from not having to keep everything in memory but only to recently used items.
A proof of concept of a process-local cache could be something like this:
defmodule LiveCache do
@ttl :timer.seconds(60)
def fetch(key, ttl \\ @ttl, fun) do
case Process.get(key) do
nil ->
value = fun.()
put(key, value, ttl)
value
{value, timer_ref} ->
Process.cancel_timer(timer_ref)
put(key, value, ttl)
value
end
end
def put(key, value, ttl) do
ref = Process.send_after(self(), {__MODULE__, :clear, key}, ttl)
Process.put(key, {value, ref})
end
def clear(key), do: Process.delete(key)
end
## in LiveView process
def handle_info({LiveCache, :clear, key}, socket) do
LiveCache.clear(key)
{:noreply, socket}
end
## in (nested) LiveComponent
def handle_event("validate", %{"id" => id, "todo" => data}, socket) do
todo = LiveCache.fetch({Todo, id}, fn -> Todos.get_todo!(id) end)
{:noreply, stream_insert(socket, :todos, to_change_form(todo, data, :validate))}
end
While it’s possible to use socket.assigns as the cache storage it seems more complex to work with when using nested live components that have their own state. If the cache is to be shared between components it must be passed as attributes, potentially causing unnecessary rerendering.
A different way of solving this issue would be to have two zipped streams like this:
todos = Todos.list_todos()
forms = Enum.map(todos, &to_change_form(&1, %{}))
# ...
<%= for {todo, form} <- zip(@streams.todos, @streams.forms) do %>
<.avatar user={todo.user}/>
<.form for={form}> ... </.form>
<% end %>
and then allow updates to one stream only while keeping the other intact.