Stateful live component is mounted more than once

I have a stateful live component. Besides the id, it receives another parameter, “products”, from the parent live view.
The parent live view constains a pubsub subscription and an handle_info that changes the products parameter.
Always when products is changed, the live component is re-rendered (as expected), and re-mounted which is not as expected.

The documentation says, a.o. “In stateful components, mount/1 is called only once, when the component is first rendered. For each rendering, the optional preload/1 and update/2 callbacks are called before render/1.”

So, I would expect that update is called with the new products, parameter. But it appears that also mount is called. What am I missing?

Hi @peter-de-boer, one explanation is that your live component is not actually stateful. Can you supply some code showing how you’re using it / setting it up?

My component:

defmodule BwWeb.CartComponent do
  use BwWeb, :live_component

  def mount(socket) do
    IO.puts("CartComponent mount")
    {:ok, assign(socket, show: false)}
  end

  def update(
    %{
      id: _id,
      cart_products: cart_products,
      cart_id: cart_id,
      store_id: store_id,
      store_slug: store_slug,
      show_checkout_button: show_checkout_button,
    } = _assigns, socket) do
    {:ok, assign(
      socket,
      cart_products: cart_products,
      cart_id: cart_id,
      store_id: store_id,
      store_slug: store_slug,
      show_checkout_button: show_checkout_button
    )}
  end

The component is called in the live view template:

   <%= live_component(
      @socket,
      BwWeb.CartComponent,
      id: Ecto.UUID.generate(),
      cart_products: @cart_products,
      cart_id: @cart_id,
      store_id: @store.id,
      store_slug: @store.slug,
      show_checkout_button: true
    ) %>

In the live view I handle a pubsub notification:

  def handle_info({Bw.Man.Cart, [cart_id, _], _}, socket) do
    IO.puts("handle_info")
    cart_products = Bw.Man.Cart.get_cart_products(cart_id, socket.assigns.store.id)
    {:noreply, assign(socket, cart_products: cart_products)}
  end

Always when this handle_info is executed, the CartComponent is mounted.

You updated one of the parameters, cart_products, so t has to be remounted. If you don’t want the remount, you have to send whatever thing you want to change via send_update

Thanks, I will try that. I guess I just do not understand the documentation as it seems to suggest that these kind of updates are handled by the update function.

This is your issue. This will generate a new UUID on every parent render which basically means you are unmounting the old component and remounting a new component on every render. Impure functions should generally be avoided in the EEX templating calls.

5 Likes

I’d even suggest you to avoid using stateful component until you are very comfortable with state-less components. Remember there is nothing you cannot do with a live view and a bunch of stateless component. Stateful component is only a feature to improve code organization; and you can improve code organization in other ways.

Awesome, that’s it!

(And I thought I was clever by generating some random id…)