Phoenix Live Component with preloads error

hi

I made a derivative of the Chirp TImeline example video and have a post schema with a belongs_to User

post.ex schema

  schema "post" do
    field :body, :string
    field :image, :string
    field :likes_count, :integer, default: 0
    field :repost_count, :integer, default: 0
    belongs_to :user, User
    timestamps()
  end

form_component.html.heex

<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="post-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

    <%= hidden_input f, :user_id, value: (@user_id) %>
    <%= hidden_input f, :website_id, value: (@website_id) %>
    <%= hidden_input f, :channel_id, value: (@user_id) %>
  
    <%= textarea f, :body, class: "textarea h-32 w-full textarea-primary" %>

    <div class="p-4">
      <%= error_tag f, :body %>
    </div>


    <div>
      <%= submit "Save", phx_disable_with: "Saving...", class: "p-4 btn btn-sm"  %>
    </div>
  </.form>
</div>

on edit and new, when I try to render the preloaded user in the live_component, the edits and new complete, but they also crash the Genserver

	def render(assigns) do
		~H"""
		<div class="Media">


     		<div id={"post-#{@post.id}"} class="Media">
				<div class="Media-body">
					<p><%= @post.body %><%= inspect @post.user.id %></p>
						  <a href="#" phx-click="like" phx-target={@myself}>đź‘Ť<%= @post.likes_count %></a>
					      <a href="#" phx-click="retale" phx-target={@myself}>♻️<%= @post.retales_count %></a>
				          <span><%= live_patch "Edit", to: Routes.tale_index_path(@socket, :edit, @post) %></span>
				          <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: @post.id, data: [confirm: "Are you sure?"] %></span>
				</div>
			</div>
		</div>	
		"""
	end 
<div class="container mx-auto p-8">
  <%= if @live_action in [:new, :edit] do %>
    <.modal return_to={Routes.post_index_path(@socket, :index)}>
      <.live_component
        module={AppWeb.PostLive.FormComponent}
        id={@post.id || :new}
        title={@page_title}
        action={@live_action}
        post={@post}
        website_id={@website_id}
        user_id={@user_id}
        channel_id={@channel_id}
        return_to={Routes.post_index_path(@socket, :index)}
      />
    </.modal>
  <% end %>

  <div id="post" phx-update="prepend">
      <%= for post <- @posts do %>
    		<%= live_component @socket, FolkbotWeb.TaleLive.TaleComponent, id: tale.id, post: post %>
      <% end %>
  </div>

  <span><%= live_patch "New Tale", to: Routes.tale_index_path(@socket, :new) %></span>
</div>

</div>

post_component.ex


<div class="container mx-auto p-8">
  <%= if @live_action in [:new, :edit] do %>
    <.modal return_to={Routes.post_index_path(@socket, :index)}>
      <.live_component
        module={AppWeb.postLive.FormComponent}
        id={@post.id || :new}
        title={@page_title}
        action={@live_action}
        post={@post}
        website_id={@website_id}
        user_id={@user_id}
        channel_id={@channel_id}
        return_to={Routes.post_index_path(@socket, :index)}
      />
    </.modal>
  <% end %>

  <div id="posts" phx-update="prepend">
      <%= for post <- @posts do %>
    		<%= live_component @socket, AppWeb.postLive.postComponent, id: post.id, post: post %>
      <% end %>
  </div>

  <span><%= live_patch "New post", to: Routes.post_index_path(@socket, :new) %></span>
</div>

index.ex

defmodule AppWeb.postLive.Index do
  use AppWeb, :live_view

  alias App.Channels
  alias App.Channels.post
  alias AppWeb.MountHelpers
  require Logger

  @impl true
  def mount(params, session, socket) do
    if connected?(socket), do: Channels.subscribe()
    socket =
      socket
      |> MountHelpers.assign_defaults(params, session, [:post, :read])
      |> assign(:page_title, "posts")
      |> assign_posts()
#      |> assign_blank_banner()

     {:ok, socket, temporary_assigns: [posts: []]}
  end

  defp assign_posts(socket) do
    posts = list_posts
    assign(socket, :posts, posts)
  end

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

  defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    |> assign(:page_title, "Edit post")
    |> assign(:post, Channels.get_post!(id))
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New post")
    |> assign(:post, %post{})
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "posts")
    |> assign(:post, nil)
  end

  def handle_event("delete", %{"id" => id}, socket) do
    Logger.info("Delete: #{inspect(id)}")
    post = Channels.get_post!(id)
    # Logger.info("Delete: #{inspect(post)}")
     # IO.puts("DEBUG: broadcast")
     # IO.inspect(post)
    case Channels.delete_post(post) do
      {:ok, post} ->
        send(self(), {:post_deleted, post})
       # Logger.info("Delete: #{inspect(post)}")
       # IO.puts("DEBUG: Delete")
       # IO.inspect(post)
        {:noreply,
         socket
         |> put_flash(:info, "post deleted successfully")
         |> assign(:posts, list_posts())
         |> push_redirect(to: "/posts")
        }

      {:error, changeset} ->
        {:noreply, assign(socket, :changeset, changeset)}
    end
  end

  def handle_info({:post_created, post}, socket) do
    {:noreply, update(socket, :posts, fn posts -> [post | posts] end)}
  end

  def handle_info({:post_updated, post}, socket) do
    {:noreply, update(socket, :posts, fn posts -> [post | posts] end)}
  end

  # def handle_info({:post_deleted, post}, socket) do
  #   {:noreply, update(socket, :posts, fn posts -> posts -- [post] end)}
  # end

  def handle_info({:post_deleted, post}, socket) do
    socket =
      socket
      |> update(:posts, fn posts -> posts -- [post] end)

    {:noreply, socket}
  end

  def list_posts do
    Channels.list_preloaded_posts_order_by_id()
  end

end

channels.ex Context

def get_post!(id), do: Repo.get!(post, id)

  def get_preloaded_post!(id) do
    post_id = id
    Repo.one(from t in post, 
      where: t.id == ^post_id, preload: :user)
      # |> Repo.preload(:users)
  end    


  def create_post(attrs \\ %{}) do
    %post{}
    |> post.changeset(attrs)
    |> Repo.insert()
    |> broadcast(:post_created)
  end

  def update_post(%post{} = post, attrs) do
    post
    |> post.changeset(attrs)
    |> Repo.update()
    |> broadcast(:post_updated)
  end


  def delete_post(%post{} = post) do
    Repo.delete(post)

    broadcast({:ok, post}, :post_deleted)
  end

  def change_post(%post{} = post, attrs \\ %{}) do
    post.changeset(post, attrs)
  end

  def subscribe do
    Phoenix.PubSub.subscribe(App.PubSub, "posts")
  end

  def broadcast({:error, _reason} = error, _event), do: error
  def broadcast({:ok, post}, event)  do
    Phoenix.PubSub.broadcast(App.PubSub, "posts", {event, post})
    {:ok, post}
  end  

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

    broadcast({:ok, post}, :post_updated)
  end

  def inc_reposts(%post{id: id}) do
    {1, [post]} = 
    from(t in post, where: t.id == ^id, select: t, preload: :user)
    |> Repo.update_all(inc: [reposts_count: 1])

    broadcast({:ok, post}, :post_updated)
  end

form_component.ex

defmodule AppWeb.postLive.FormComponent do
  use AppWeb, :live_component
  alias AppWeb.postLive.Index

  alias App.Channels


  @impl true
  def update(%{post: post} = assigns, socket) do
    post = Channels.get_preloaded_post!(assigns.id)
    changeset = Channels.change_post(post)

    {:ok,
     socket
     |> assign(assigns)
     |> assign(:changeset, changeset)}
  end

  # @impl true
  # def handle_event("delete", %{"id" => id}, socket) do
  #   post = Channels.get_post!(id)
  #   {:ok, _} = Channels.delete_post(post)

  #   {:noreply, assign(socket, :posts, Index.list_posts())}
  # end



  @impl true
  def handle_event("validate", %{"post" => post_params}, socket) do
    changeset =
      socket.assigns.post
      |> Channels.change_post(post_params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, :changeset, changeset)}
  end

  def handle_event("save", %{"post" => post_params}, socket) do
    save_post(socket, socket.assigns.action, post_params)
  end

  defp save_post(socket, :edit, post_params) do
    case Channels.update_post(socket.assigns.post, post_params) do
      {:ok, _post} ->
        {:noreply,
         socket
         # |> put_flash(:info, "post updated successfully")
         |> push_redirect(to: socket.assigns.return_to)}

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

  defp save_post(socket, :new, post_params) do
    case Channels.create_post(post_params) do
      {:ok, _post} ->
        {:noreply,
         socket
         # |> put_flash(:info, "post created successfully")
         |> push_redirect(to: socket.assigns.return_to)}

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

end

trying to understand where I should be preloading user in the Live Component lifecycle for new and edit

the new and edit complete, but they crash the genserver in the process

[debug] QUERY OK db=0.7ms queue=0.2ms idle=1637.0ms
UPDATE "posts" SET "body" = $1, "updated_at" = $2 WHERE "id" = $3 ["new ggggjjj ddd", ~N[2022-01-29 18:51:37], 156]
[debug] QUERY OK source="users_tokens" db=0.4ms idle=1643.1ms
SELECT u1."id", u1."admin", u1."editor", u1."host", u1."seller", u1."banner_image", u1."profile_image", u1."bio", u1."email", u1."first_name", u1."last_name", u1."location", u1."slug", u1."snowflake", u1."tagline", u1."username", u1."website", u1."hashed_password", u1."confirmed_at", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-(60)::numeric * interval '1 day')) [<<247, 30, 135, 246, 22, 78, 171, 35, 35, 74, 184, 30, 39, 109, 117, 32, 63, 93, 78, 105, 211, 196, 51, 160, 68, 20, 222, 229, 86, 178, 196, 175>>, "session", ~U[2022-01-29 18:51:37.904014Z]]
[error] GenServer #PID<0.2931.0> terminating
** (KeyError) key :id not found in: #Ecto.Association.NotLoaded<association :user is not loaded>
    (App 0.1.0) lib/App_web/live/post_live/post_component.ex:11: anonymous fn/2 in AppWeb.postLive.postComponent.render/1
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/diff.ex:372: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/diff.ex:658: Phoenix.LiveView.Diff.render_component/9
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/diff.ex:603: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/6
    (elixir 1.12.2) lib/enum.ex:2385: Enum."-reduce/3-lists^foldl/2-0-"/3
    (stdlib 3.12.1) maps.erl:232: :maps.fold_1/3
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/diff.ex:576: Phoenix.LiveView.Diff.render_pending_components/6
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/diff.ex:145: Phoenix.LiveView.Diff.render/3
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/channel.ex:769: Phoenix.LiveView.Channel.render_diff/3
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/channel.ex:625: Phoenix.LiveView.Channel.handle_changed/4
    (stdlib 3.12.1) gen_server.erl:637: :gen_server.try_dispatch/4
    (stdlib 3.12.1) gen_server.erl:711: :gen_server.handle_msg/6
    (stdlib 3.12.1) proc_lib.erl:259: :proc_lib.wake_up/3
Last message: {:post_updated, %App.Channels.post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "new ggggjjj ddd", channel: #Ecto.Association.NotLoaded<association :channel is not loaded>, channel_id: 1, id: 156, image: nil, inserted_at: ~N[2022-01-29 08:12:38], likes_count: 10, parent_id: nil, reposts_count: 7, updated_at: ~N[2022-01-29 18:51:37], user: #Ecto.Association.NotLoaded<association :user is not loaded>, user_id: 1, website: #Ecto.Association.NotLoaded<association :website is not loaded>, website_id: 1}}
State: %{components: {%{1 => {AppWeb.postLive.postComponent, 162, %{__changed__: %{}, flash: %{}, id: 162, myself: %Phoenix.LiveComponent.CID{cid: 1}, post: %App.Channels.post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "ddd sss. dddd", channel: #Ecto.Association.NotLoaded<association :channel is not loaded>, channel_id: 1, id: 162, image: nil, inserted_at: ~N[2022-01-29 08:51:47], likes_count: 2, parent_id: nil, reposts_count: 2, updated_at: ~N[2022-01-29 18:32:06], user: #App.Accounts.User<__meta__: #Ecto.Schema.Metadata<:loaded, "users">, admin: true, banner_image: %{file_name: "5.sm.webp", updated_at: ~N[2021-12-28 08:26:57]}, bio: nil, channels: #Ecto.Association.NotLoaded<association :channels is not loaded>, confirmed_at: ~N[2022-01-04 05:34:55], content_accessible: #Ecto.Association.NotLoaded<association :content_accessible is not loaded>, editor: false, email: "niccolox@devekko.com", first_name: nil, host: true, hosts_channels: #Ecto.Association.NotLoaded<association :hosts_channels is not loaded>, id: 1, inserted_at: ~U[2021-11-24 02:32:42.000000Z], last_name: nil, location: nil, owns_websites: #Ecto.Association.NotLoaded<association :owns_websites is not loaded>, profile_image: %{file_name: "pixel.png", updated_at: ~N[2021-12-27 03:41:38]}, seller: false, slug: nil, snowflake: "6862262450054500352", tagline: "the architect", updated_at: ~U[2022-01-04 05:34:56.000000Z], username: "niccolox", website: "devekko.com", ...>, user_id: 1, website: #Ecto.Association.NotLoaded<association :website is not loaded>, website_id: 1}}, %{__changed__: %{}, root_view: AppWeb.postLive.Index}, {37841891526857918788129004884530619461, %{}}}, 2 => {AppWeb.postLive.postComponent, 161, %{__changed__: %{}, flash: %{}, id: 161, myself: %Phoenix.LiveComponent.CID{cid: 2}, post: %App.Channels.post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "preloads are awesome", channel: #Ecto.Association.NotLoaded<association :channel is not loaded>, channel_id: 1, id: 161, image: nil, inserted_at: ~N[2022-01-29 08:49:30], likes_count: 3, parent_id: nil, reposts_count: 1, updated_at: ~N[2022-01-29 18:33:41], user: #App.Accounts.User<__meta__: #Ecto.Schema.Metadata<:loaded, "users">, admin: true, banner_image: %{file_name: "5.sm.webp", updated_at: ~N[2021-12-28 08:26:57]}, bio: nil, channels: #Ecto.Association.NotLoaded<association :channels is not loaded>, confirmed_at: ~N[2022-01-04 05:34:55], content_accessible: #Ecto.Association.NotLoaded<association :content_accessible is not loaded>, editor: false, email: "niccolox@devekko.com", first_name: nil, host: true, hosts_channels: #Ecto.Association.NotLoaded<association :hosts_channels is not loaded>, id: 1, inserted_at: ~U[2021-11-24 02:32:42.000000Z], last_name: nil, location: nil, owns_websites: #Ecto.Association.NotLoaded<association :owns_websites is not loaded>, profile_image: %{file_name: "pixel.png", updated_at: ~N[2021-12-27 03:41:38]}, seller: false, slug: nil, snowflake: "6862262450054500352", tagline: "the architect", updated_at: ~U[2022-01-04 05:34:56.000000Z], username: "niccolox", website: "devekko.com", ...>, user_id: 1, website: #Ecto.Association.NotLoaded<association :website is not loaded>, website_id: 1}}, %{__changed__: %{}, root_view: AppWeb.postLive.Index}, {37841891526857918788129004884530619461, %{}}}, 3 => {AppWeb.postLive.postComponent, 160, %{__changed__: %{}, flash: %{}, id: 160, myself: %Phoenix.LiveComponent.CID{cid: 3}, post: %App.Channels.post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "thihs 11111111", channel: #Ecto.Association.NotLoaded<association :channel is not loaded>, channel_id: 1, id: 160, image: nil, inserted_at: ~N[2022-01-29 08:18:03], likes_count: 17, parent_id: nil, reposts_count: 15, updated_at: ~N[2022-01-29 18:25:58], user: #App.Accounts.User<__meta__: #Ecto.Schema.Metadata<:loaded, "users">, admin: true, banner_image: %{file_name: "5.sm.webp", updated_at: ~N[2021-12-28 08:26:57]}, bio: nil, channels: #Ecto.Association.NotLoaded<association :channels is not loaded>, confirmed_at: ~N[2022-01-04 05:34:55], content_accessible: #Ecto.Association.NotLoaded<association :content_accessible is not loaded>, editor: false, email: "niccolox@devekko.com", first_name: nil, host: true, hosts_channels: #Ecto.Association.NotLoaded<association :hosts_channels is not loaded>, id: 1, inserted_at: ~U[2021-11-24 02:32:42.000000Z], last_name: nil, location: nil, owns_websites: #Ecto.Association.NotLoaded<association :owns_websites is not loaded>, profile_image: %{file_name: "pixel.png", updated_at: ~N[2021-12-27 03:41:38]}, seller: false, slug: nil, snowflake: "6862262450054500352", tagline: "the architect", updated_at: ~U[2022-01-04 05:34:56.000000Z], username: "niccolox", website: "devekko.com", ...>, user_id: 1, website: #Ecto.Association.NotLoaded<association :website is not loaded>, website_id: 1}}, %{__changed__: %{}, root_view: AppWeb.postLive.Index}, {37841891526857918788129004884530619461, %{}}}, 4 => {AppWeb.postLive.postComponent, 159, %{__changed__: %{}, flash: %{}, id: 159, myself: %Phoenix.LiveComponent.CID{cid: 4}, post: %App.Channels.post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "AWESOME", channel: #Ecto.Association.NotLoaded<association :channel is not loaded>, channel_id: 1, id: 159, image: nil, inserted_at: ~N[2022-01-29 08:17:11], likes_count: 7, parent_id: nil, reposts_count: 2, updated_at: ~N[2022-01-29 08:51:18], user: #App.Accounts.User<__meta__: #Ecto.Schema.Metadata<:loaded, "users">, admin: true, banner_image: %{file_name: "5.sm.webp", updated_at: ~N[2021-12-28 08:26:57]}, bio: nil, channels: #Ecto.Association.NotLoaded<association :channels is not loaded>, confirmed_at: ~N[2022-01-04 05:34:55], content_accessible: #Ecto.Association.NotLoaded<association :content_accessible is not loaded>, editor: false, email: "niccolox@devekko.com", first_name: nil, host: true, hosts_channels: #Ecto.Association.NotLoaded<association :hosts_channels is not loaded>, id: 1, inserted_at: ~U[2021-11-24 02:32:42.000000Z], last_name: nil, location: nil, owns_websites: #Ecto.Association.NotLoaded<association :owns_websites is n (truncated)
[debug] QUERY OK source="content_access" db=0.2ms idle=1644.0ms
SELECT c0."id", c0."user_id", c0."s

Could you share your function where you load the Post?
It seems like your User doesn’t get loaded.

more code snippets in original post @moodle19

user is loaded in the timeline, but after update and I guess at mount it crashes

Ah, okay. I see.
The Repo.update/1 returns the newly updated schema but doesn’t preload the fields (same for Repo.insert/1).

You can preload the user via Repo.preload(post, :user) in your create_post and update_post.


  def update_post(%post{} = post, attrs) do
    post
    |> post.changeset(attrs)
    |> Repo.update()
    |> case do
      {:ok, post} -> {:ok, Repo.preload(post, :user)}
      error -> error
    end
    |> broadcast(:post_updated)
  end
1 Like

that case pipe saves my Context file from sadness

I appreciate the help and keen eye for detail @moogle19 !!

THANKS

actually, after fixing new and edit

the incremental likes and reposts are broken

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

    broadcast({:ok, post}, :tale_updated)
  end

Yes, probably the same problem as the other ones.
Just add a

post = Repo.preload(post, :user)

before broadcasting it.
Neither update nor update_all preload any fields.

I also have an incomplete Delete feature, I’ll post that separately (sometime this weekend) and backlink

Delete bug: deletes but doesn’t broadcast and force updates via subscribe I think, only shows the delete on the page of the user deleting.

Anyway, thanks again !