Not Preloading before rendering index page after changing referenced field when editing from index page

Greetings Everyone!!!

Once again I’m lost.

When rendering an index page that shows a referenced field for the first time, everything works fine. The list_trucks() function is preloading the association to the fuel’s type.

# index.ex
  @impl true
  def mount(_params, _session, socket) do
    {:ok, stream(socket, :trucks, Catalogs.list_trucks())}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

# index.html.heex
12: <.table
13:   id="trucks"
14:   rows={@streams.trucks}
15:   row_click={fn {_id, truck} -> JS.navigate(~p"/catalogs/trucks/#{truck}") end}
16: >
17:  <:col :let={{_id, truck}} label="Name"><%= truck.trk_descrip %></:col>
18:  <:col :let={{_id, truck}} label="Plate"><%= truck.trk_plate %></:col>
19:  <:col :let={{_id, truck}} label="Fuel Type"><%= truck.fuels.ful_type %></:col>
# ------------------------------------------------^^^^^^^^^^^^^^^^^^^^

When I click on any truck, it renders the show page for that truck just fine, and if I edit that truck from there and I change the fuel’s type (with a reference to another table), it returns just fine to the show page of that truck after saving that modification.

The problem is when I click on the Edit button from the index page, then I change the fuel type and save the modifications. When is rendering the index page again, it shows an error on line 19 of the index.html.heex file, where it already displayed the fuel_type for the reference the first time it displayed the index page.

Since the mount just happens once, but the handle_params several times, I thought the handle_params function would be the right place to reload the trucks list. First I used the assign function and then I realized the mount function is using the stream function.

  @impl true
  def handle_params(params, _url, socket) do
    socket = stream(socket, trucks: Catalogs.list_trucks())
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

But even then, I am getting the same error.
(The query that runs from form_component:ex:8 is the one that gathers the options for the select object)

  @impl true
  def render(assigns) do
    assigns =
      assigns
      |> assign(fuels_options: Catalogs.get_fuels_options())

  def get_fuels_options() do
    Repo.all(
      from f in Fuel,
      select: {f.ful_type, f.id},
      order_by: f.id
    )
  end
[debug] HANDLE EVENT "save" in MenuWeb.TruckLive.Index
  Component: MenuWeb.TruckLive.FormComponent
  Parameters: %{"truck" => %{"trk_active" => "true", "trk_descrip" => "SHOPPING", "trk_fuel" => "2", "trk_plate" => ""}}
[debug] QUERY OK source="trucks" db=2.2ms queue=0.2ms idle=1886.0ms
UPDATE "general"."trucks" SET "trk_fuel" = $1, "updated_at" = $2 WHERE "id" = $3 [2, ~U[2024-12-16 18:40:44Z], 1]
↳ MenuWeb.TruckLive.FormComponent.save_truck/3, at: lib/menu_web/live/truck_live/form_component.ex:60
[debug] Replied in 11ms
[debug] QUERY OK source="fuels" db=0.6ms idle=1889.8ms
SELECT f0."ful_type", f0."id" FROM "general"."fuels" AS f0 ORDER BY f0."id" []
↳ MenuWeb.TruckLive.FormComponent.render/1, at: lib/menu_web/live/truck_live/form_component.ex:8
[debug] HANDLE PARAMS in MenuWeb.TruckLive.Index
  Parameters: %{}
::: Listing all the trucks :::
[debug] QUERY OK source="trucks" db=0.7ms idle=1890.7ms
SELECT t0."id", t0."trk_descrip", t0."trk_plate", t0."trk_active", t0."trk_fuel", t0."inserted_at", t0."updated_at" FROM "general"."trucks" AS t0 ORDER BY t0."trk_descrip" []
↳ MenuWeb.TruckLive.Index.handle_params/3, at: lib/menu_web/live/truck_live/index.ex:14
[debug] QUERY OK source="fuels" db=0.3ms idle=1891.7ms
SELECT f0."id", f0."ful_type", f0."inserted_at", f0."updated_at", f0."id" FROM "general"."fuels" AS f0 WHERE (f0."id" = ANY($1)) [[1, 2]]
↳ MenuWeb.TruckLive.Index.handle_params/3, at: lib/menu_web/live/truck_live/index.ex:14
[debug] Replied in 1ms
[error] GenServer #PID<0.739.0> terminating
** (KeyError) key :ful_type not found in: #Ecto.Association.NotLoaded<association :fuels is not loaded>
    (menu 0.1.0) lib/menu_web/live/truck_live/index.html.heex:19: anonymous fn/3 in 

Up until now, I have three forms where I am displaying a referenced field in the index page, and all three of them have the exact same behaviour: They show just fine the first time, and then, when I edit a record from the index page and change a referenced field, the association is not loaded when rendering the index page again. If I remove that “line 19”, where I try to show referenced field, in all my index.html.heex files everything works fine.

What can I do to have that association loaded again when rendering the page after that edit?

Thanks in advance,

Greg.

Streams have a very specific rule set with regards to both set up and updates. They are prone to misbehaving if you do not follow the rules, and are generally awkward to learn and work with initially. It doesn’t help that finding info on them is difficult because Stream in Elixir and streams in LiveView have basically the same name but Stream always returns as the top result on search egnines. But info on streams can be found below.

Phoenix.LiveView — Phoenix LiveView v1.0.1

Firstly, is stream_configure elsewhere in your code? You don’t seem to have this in the code you provided. In order to “update” your stream, you will need use stream_insert/4 which means you will likely need to configure the stream. I don’t often use streams so I could be wrong, but I always include it.

From the hexdocs:

A stream must be configured before items are inserted, and once configured, a stream may not be re-configured. To ensure a stream is only configured a single time in a LiveComponent, use the mount/1 callback. For example:

def mount(socket) do
  {:ok, stream_configure(socket, :songs, dom_id: &("songs-#{&1.id}"))}
end

def update(assigns, socket) do
  {:ok, stream(socket, :songs, ...)}
end

In order to update an item in a stream you need to use stream_insert/4 like below, but also preload the item prior to insertion.

Hexdocs:

Or update an existing song (in this case the :at option has no effect):

stream_insert(socket, :songs, %Song{id: 1, title: "Song 1 updated"}, at: 0)

As a general note on streams:

Streams are typically used for handling large datasets or real-time updates on the client, where you want to optimize memory usage or keep the data synced with frequent changes. Streams allow for incremental loading of data, sending only the necessary items to the client, which is particularly useful when dealing with large lists of records. For real-time data, streams can efficiently add, remove, or update items without reloading the entire dataset.

For a chat app, where hundreds or thousands of messages are constantly being added to the list, streams are ideal. You can simply stream new messages and append them to the end of the list as they come in. However, for something like a catalog, which is likely fixed and already paginated to a manageable size, using regular assigns is a much easier and more straightforward option to implement in my opinion.

Streams are unaffected by regular assigns updates / DOM patching like push_patch as far as I’m aware, and require you to follow a specific process in order to update them correctly using things like stream_insert/4.

In your case, the initial preload works because the first load of the stream simply sends the queried dataset to the client. However, the reason the update is failing — (making an assumption as you haven’t shared the edit code) — is likely because the updated item isn’t being preloaded when it’s reintegrated into the stream.

1 Like

You don’t need to include it. I used streams all the time and have used this once (and think I reverted it). :dom_id has a default.

YOU ARE A GENIUS!!!

I started trying to use the stream_insert function in my handle_params function, as in my old approach, but I noticed it was already being used in the index.ex:handle_info() function, just ahead.

Right then, and after reading explanation, I understood why the form_component.ex module is sending a notification to its parent process with the updated/new record. Only that record is being sent back into the stream, that already has the list of trucks:

# .../truck_live/index.ex
  @impl true
  def handle_info({MenuWeb.TruckLive.FormComponent, {:saved, truck}}, socket) do
    {:noreply, stream_insert(socket, :trucks, truck)}
  end

So, I preloaded the updated truck record before sending the notification…

# .../truck_live/form_component.ex
  defp save_truck(socket, :edit, truck_params) do
    case Catalogs.update_truck(socket.assigns.truck, truck_params) do
      {:ok, truck} ->
        # notify_parent({:saved, truck})   <------------------------- Old Line
        notify_parent({:saved, Catalogs.preload(truck, :fuels)} # <-- New Line

        {:noreply,
         socket
         |> put_flash(:info, "Truck updated successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

# .../lib/menu/catalogs.ex
  def preload(struct, associations) do
    struct
    |> Repo.preload(associations)
  end

…and that was it!!!

Thank you very much and best regards,

Greg.