Webpage not rendering updates to a nested, stateful live component template
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.
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.
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.
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?
@jmhossler Please let us know if you can provide a reproduction, and we would be happy to investigate further. Thanks!