phoenix_live_view icon indicating copy to clipboard operation
phoenix_live_view copied to clipboard

Webpage not rendering updates to a nested, stateful live component template

Open jmhossler opened this issue 4 years ago • 2 comments

Environment

  • Elixir version (elixir -v):
Erlang/OTP 24 [erts-12.0.3] [source] [64-bit] [smp:24:24] [ds:24:24:10] [async-threads:1] [jit]

Elixir 1.12.2 (compiled with Erlang/OTP 24)
  • Phoenix version (mix deps): 1.6.5

  • Phoenix LiveView version (mix deps): 0.16.4 I also still saw this in 0.17.5, I have a branch on my example repo that also reproduces this issue.

  • Operating system: Arch linux

  • Browsers you attempted to reproduce this bug on (the more the merrier): Chrome

  • Does the problem persist after removing "assets/node_modules" and trying again? Yes/no: No

Actual behavior

Child liveview that is rendered under a stateful live component won't send changes to the rendered page. I was able to boil this down into this example: https://github.com/jmhossler/example-liveview-issue

If you pull down this repo, uncomment line 12 and comment out line 13 of this file

It might be able to be simplified further, but this is the structure I got it down to when trying to replicate what I was seeing in our more complicated web app:

root liveview 
  |-> stateful live component 
     |-> child live view
        |-> child stateful live component

The issue seems to be related to the first stateful live component. With this structure, this is what I end up seeing on the rendered webpage:

https://user-images.githubusercontent.com/9029984/146722704-9f7e4852-2422-4ff1-b4b6-7f34243701d8.mp4

As you can see, the updates do seem to be showing up in the websocket messages, but the rendered page isn't changing. I also added some logging so you could see what state the child stateful live component sees for itself when it gets a new button press event, and it was what I expected (the text was being updated from the perspective of the child component's state).

Here is a sample of my logs from that video:

reverse event triggered, current text: [Hello, world!]
reverse event triggered, current text: [!dlrow ,olleH]
reverse event triggered, current text: [Hello, world!]
reverse event triggered, current text: [!dlrow ,olleH]
reverse event triggered, current text: [Hello, world!]
reverse event triggered, current text: [!dlrow ,olleH]

Expected behavior

It's a bit easier to explain with a video. This video is of it acting as I would expect it to (simulated by making the main live component not stateful, i.e. commenting out line 12 in the main view and uncommenting line 13):

https://user-images.githubusercontent.com/9029984/146722082-5881950c-b1e9-4ada-b2f4-d0f3995dc228.mp4

As you can see, when I perform an action that updates the state of the nested, stateful live component, it is able to update its state and the page is rerendered with the update according to its template.

jmhossler avatar Dec 20 '21 06:12 jmhossler

Thank you for the very detailed report. I am not sure if this is expected to work, so I will let @chrismccord chime in. If it should not, perhaps we should raise under such scenarios.

josevalim avatar Dec 20 '21 08:12 josevalim

Seem I got the same issue, the details of the live_view and live_component layers:


<div data-phx-main="true" id="phx-FsU7PoskN4yPKw3D" data-phx-root-id="phx-FsU7PoskN4yPKw3D">
	<div id="main_live_component" data-phx-component="1">
		<div id="data_live_view" data-phx-parent-id="phx-FsU7PoskN4yPKw3D" data-phx-root-id="phx-FsU7PoskN4yPKw3D" class="phx-connected">
			<div id="live_component_1" data-phx-component="1"><p>value1_not_update</p></div>
			<div id="live_component_2" data-phx-component="2"><p>value2</p></div>
			<div id="live_component_3" data-phx-component="3"><p>value3</p></div>
			<div id="live_component_4" data-phx-component="4"><p>value4</p></div>
		</div>
	</div>
</div>
root liveview 
  |->  live component 
     |-> child live view
        |-> list of child live component and the first one is not updated

With the layer above, the live_component <div id="live_component_1" data-phx-component="1"><p>value1</p></div> was not updated.

After that, I add the faked live_component to the top and the origin live_components were updated.

<div data-phx-main="true" id="phx-FsU7PoskN4yPKw3D" data-phx-root-id="phx-FsU7PoskN4yPKw3D">
	<div id="main_live_component" data-phx-component="1">
		<div id="data_live_view" data-phx-parent-id="phx-FsU7PoskN4yPKw3D" data-phx-root-id="phx-FsU7PoskN4yPKw3D" class="phx-connected">
			<div id="fake_live_component_1" data-phx-component="1"><p>fake_value1_not_update</p></div>
			<div id="live_component_1" data-phx-component="2"><p>value1</p></div>
			<div id="live_component_2" data-phx-component="3"><p>value2</p></div>
			<div id="live_component_3" data-phx-component="4"><p>value3</p></div>
			<div id="live_component_4" data-phx-component="5"><p>value4</p></div>
		</div>
	</div>
</div>

I assumed that due to the data-phx-component="1" is the same at id="main_live_component" and id="fake_live_component_1" then fake_live_component is not udpated.

leductam avatar Dec 29 '21 13:12 leductam

I tried to reproduce this, but I couldn't. The nested LC in the nested LV updates without problems. Here's a single-file script:

Application.put_env(:sample, Example.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 5001],
  server: true,
  live_view: [signing_salt: "aaaaaaaa"],
  secret_key_base: String.duplicate("a", 64),
  pubsub_server: MyPubSub
)

Mix.install([
  {:plug_cowboy, "~> 2.5"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7.10", override: true},
  # {:phoenix_live_view, github: "phoenixframework/phoenix_live_view", branch: "main"}
  {:phoenix_live_view, "~> 0.20.3"}
])

defmodule Example.ErrorView do
  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule Example.HomeLive do
  use Phoenix.LiveView, layout: {__MODULE__, :live}

 def mount(_params, _session, socket) do
    socket
    |> then(&{:ok, &1})
  end

  def render("live.html", assigns) do
    ~H"""
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/priv/static/phoenix.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/phoenixframework/[email protected]/priv/static/phoenix_live_view.js"></script>
    <%!-- <script src="http://localhost:8000/priv/static/phoenix_live_view.js"></script> --%>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, {
        hooks: {
          FakeHook: {
            mounted() {}
          }
        }
      })
      liveSocket.connect()
    </script>
    <%!-- <script src="https://cdn.tailwindcss.com"></script> --%>
    <style>
      * { font-size: 1.1em; }
    </style>
    <%= @inner_content %>
    """
  end

  def render(assigns) do
    ~H"""
    <.live_component module={Example.HomeComponent} id="home-component" />
    """
  end
end

defmodule Example.HomeComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~H"""
    <div>
      <%= live_render(@socket, Example.NestedLive, id: "nested") %>
    </div>
    """
  end
end

defmodule Example.NestedLive do
  use Phoenix.LiveView

  def render(assigns) do
    ~H"""
    <.live_component module={Example.NestedComponent} id="nested-component" />
    """
  end
end

defmodule Example.NestedComponent do
  use Phoenix.LiveComponent

  def update(assigns, socket) do
    {:ok, assign(socket, :message, "Hello World!")}
  end

  def handle_event("reverse", _unsigned_params, socket) do
    {:noreply, assign(socket, :message, String.reverse(socket.assigns.message))}
  end

  def render(assigns) do
    ~H"""
    <div>
      <p><%= @message %></p>
      <button phx-click="reverse" phx-target={@myself}>Reverse</button>
    </div>
    """
  end
end

defmodule Example.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:accepts, ["html"])
  end

  scope "/", Example do
    pipe_through(:browser)

    live("/", HomeLive, :index)
  end
end

defmodule Example.Endpoint do
  use Phoenix.Endpoint, otp_app: :sample
  socket("/live", Phoenix.LiveView.Socket)
  plug(Example.Router)
end

{:ok, _} = Supervisor.start_link([Example.Endpoint, {Phoenix.PubSub, name: MyPubSub}], strategy: :one_for_one)
Process.sleep(:infinity)

Could you check if this still affects you in the current release of LiveView?

SteffenDE avatar Jan 14 '24 14:01 SteffenDE

@jmhossler Please let us know if you can provide a reproduction, and we would be happy to investigate further. Thanks!

chrismccord avatar Jan 15 '24 13:01 chrismccord