Phoenix_live_view: Passing flash to child component seems weird(?)

Created on 28 Mar 2020  路  7Comments  路  Source: phoenixframework/phoenix_live_view

Environment

  • Elixir version (elixir -v): 1.10.1
  • Phoenix version (mix deps): 1.4.16
  • Phoenix LiveView version (mix deps): 0.10.0
  • NodeJS version (node -v): 12.14.0
  • NPM version (npm -v): 6.13.4
  • Operating system: 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

Actual behavior

I'm making stateless AlertMessageComponent like below, this will be child component.

  def render(assigns) do
    ~L"""
    <div class="message is-info">
      <div class="alert message-body"><%= live_flash(@flash, "info") %></div>
    </div>
    """
  end

I'd like to pass flash from parent live view to the child component like this,

  <%= live_component @socket, AlertMessageComponent, flash: @flash %>

On the parent live view, I control flash like this,

  def handle_event("delete", _params, %{assigns: %{selected: nil}} = socket) do
    Process.send_after(self(), "clear_flash", 2000)
    socket = put_flash(socket, "info", "There is no delete target.")

    {:noreply, socket}
  end

  def handle_info("clear_flash", socket) do
    {:noreply, clear_flash(socket)}
  end

In this situation, flash message is presented after cliking phx-click="delete", but it doesn't disappeared.

I changed assigns child component key name from "flash" to "flash2", then works finely.

Expected behavior

I'd like to pass flash easily by using key name "flash".

Most helpful comment

@josevalim
I made reproduce application here.
Could you check the behaviour? Reproduce page is root path, http://localhost:4000/.

Peek 2020-03-29 10-13

All 7 comments

Can you please provide a sample application that reproduces the error? Thank you.

@pojiro I had the same issue and was able to solve it by having a global live view module that mounts all components and putting the flash rendering only at the top level view.

  def render_game_or_lobby(assigns) do
    if joined?(assigns) do
      ~L"""
      <div class="game-container">
        <div class="main-panel">
          <%= live_component(@socket, PlayerCardComponent, id: :my_player_card, player: @player) %>
          <%= live_component(@socket, DiceRollerComponent, id: :dice_roller, dice_state: @dice_state) %>
        </div>
        <%= live_component(@socket, PlayerListComponent, players: @players) %>
      </div>
      """
    else
      ~L"""
      <%= live_component(@socket, LobbyComponent, id: :lobby) %>
      """
    end
  end

  def render(assigns) do
    ~L"""
    <p class="alert alert-info" role="alert"><%= live_flash(@flash, :info) %></p>
    <p class="alert alert-danger" role="alert"><%= live_flash(@flash, :error) %></p>
    <%= render_game_or_lobby(assigns) %>
    """
  end

But I agree with you that this is a weird behavior that flash is not being passed to child components.

@josevalim
I made reproduce application here.
Could you check the behaviour? Reproduce page is root path, http://localhost:4000/.

Peek 2020-03-29 10-13

I'm still seeing this issue on master, the :flash isn't being cleared (actually it's cleared and was cleared even before, the problem is that it doesn't appear in :changed as being changed, and thus the template will still render the old state).

# mix.exs
...
{:phoenix_live_view, github: "phoenixframework/phoenix_live_view"}

I have removed _build and deps folders prior to testing.

I think the problem happens here, where :flash is set to the empty map without :changed being updated to reflect this.

socket = configure_socket_for_component(socket, %{flash: %{}}, %{}, new_fingerprints())

In my attempted PR to fix this, I used Utils.assign(:flash, %{}), which would properly set flash: true in :changed if that was the case.

EDIT: I don't have the needed high level understanding of the entire flow, but just removing the assigning of flash: %{} in this call seems to fix the issue (~all tests are passing as well~):

- socket = configure_socket_for_component(socket, %{flash: %{}}, %{}, new_fingerprints())
+ socket = configure_socket_for_component(socket, %{}, %{}, new_fingerprints())

This is weird. We pass the empty flash on mount, and on mount everything is always considered as changed. I will be glad to accept a PR that removes the flash entry from there altogether but we first need a test that reproduces the issue in our suite. :)

Some further findings:

mount_component gets called after clear_flash if no :id is given to live_component:

  defp traverse(
         socket,
         %Component{id: nil, component: component, assigns: assigns},
         fingerprints_tree,
         pending_components,
         components
       ) do
    rendered = component_to_rendered(socket, component, assigns)
    traverse(socket, rendered, fingerprints_tree, pending_components, components)
  end

traverse -> component_to_rendered -> mount_component

clear_flash clears the :flash and properly reflects this in :changed, but then, when mount_component is called, it will overwrite the already cleared :flash

So, when :id is given to live_component, everything works as it should and clear_flash is reflected in the template. But, in the case of stateless components (no :id), clear_flash isn't reflected in the template.

Oh, great findings. That should help writing a test for this issue, as the root cause seems to be related to stateless components.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tfwright picture tfwright  路  3Comments

ccapndave picture ccapndave  路  4Comments

LightningK0ala picture LightningK0ala  路  5Comments

lukaszsamson picture lukaszsamson  路  5Comments

drapergeek picture drapergeek  路  5Comments