How do I pass state between sibling components or up to a parent

I’ve been using chatGPT and looking for articles to explain this simply and everything is either wrong, outdated or too complicated for me to understand. In React I understand how to move state up/down and between sibling components. In Phoenix I do not.

I want to know the most generic way applicable to any app. I’ve read some forum posts but I think my example below is more generic and easier to apply elsewhere.

Below I have a parent live view named PageLive and two sibling components (Heading and Content). If I want to write code so that when a user clicks a Heading item it affects the Content, what is the thought process to make this happen? Maybe as a simple example, when a user clicks a Heading item the result is a string is appended to the “content”.

I understand how to set event handlers but I intentionally left that code out because in this case, whatever I write will probably be wrong.

EDIT

My example doesn’t work with state - I was focused on events as a precursor to moving state around. It was a bad example. Sorry :confused:



 defmodule Heading do
   use Phoenix.Component
   def head(assigns) do
	   ~H"""

    <ul>
      <%= for heading <- @headings do %>
        <li><%= heading %>!</li>
      <% end %>
    </ul>

	   """
   end
end


defmodule Content do
  use Phoenix.Component
  def content(assigns) do
    ~H"""

      <p>This is content: <%= @content %>!</p>

    """
  end
end



defmodule AppWeb.PageLive do
   use AppWeb, :live_view
   import Heading
   import Content
   def mount(_params, _session, socket)  do
	 {:ok, socket}
   end

   def headings do
    ["one","two","three"]
   end

   def content() do
       "This is some great content here"

   end

   def render(assigns) do
	   ~H"""

	     <Heading.head  headings={headings()} />

       <Content.content content ={content()} />

	   """
   end
end

EDIT

an AI gave me a working example and I’m playing with it now.


defmodule Heading do
  use Phoenix.Component

  def head(assigns) do
    ~H"""
    <ul>
      <%= for heading <- @headings do %>
        <li phx-click="append_content" phx-value-heading={heading}><%= heading %>!</li>
      <% end %>
    </ul>
    """
  end
end

defmodule Content do
  use Phoenix.Component

  def content(assigns) do
    ~H"""
    <p>This is content: <%= @content %>!</p>
    """
  end
end

defmodule AppWeb.PageLive do
  use AppWeb, :live_view
  import Heading
  import Content

  def mount(_params, _session, socket) do
    {:ok, assign(socket, :content, "This is some great content here")}
  end

  def headings do
    ["one", "two", "three"]
  end

  def render(assigns) do
    ~H"""
    <Heading.head headings={headings()} />
    <Content.content content={@content} />
    """
  end

  def handle_event("append_content", %{"heading" => _heading}, socket) do
    updated_content = socket.assigns.content <> " hello there"
    {:noreply, assign(socket, :content, updated_content)}
  end
end

Function components, which you are using here, do not have state - they simply accept assigns from a parent and render statelessly. The state in your example(s) is stored in one place, in the parent LiveView. When you update it, the function components will be rendered and re-sent down the wire accordingly, as I think you have discovered. This is a perfectly valid way to manage state, and similar patterns exist with React.

If you want stateful components, you would instead use a LiveComponent. LiveComponents manage their own state, which can be passed down to them from a parent (<.live_component id="foo" module={MyComponent} some_key="some value" />) or sent explicitly with send_update/3. The latter is how you would send “messages” (they are not real Elixir messages) between sibling components.

Finally, it is also possible to render multiple LiveViews on a page, though this is less common. If you were doing that then you can send actual Elixir messages between LiveViews because they are processes.

First of all, avoid using functions as you are doing with headings/0 and content/0 to populate data into the template. HEEX can’t perform change tracking with functions, and this seems to be what is causing the issue for you. Use assigns instead. See Assigns and HEEx templates — Phoenix LiveView v1.0.5.

Back to your initial question about passing states, when designing a LiveView, you need to choose where the “source of truth” for your data will be. In the example AI provided you, the “source of truth” is in LiveView. In this case, updating components simply involves assigning new values to assigns using the Phoenix.LiveView.assign/3 function, and LiveView will re-render components with the new assigns.

But the state can also be stored in your components using Phoenix.LiveComponent. In this case, you need to use Phoenix.LiveView.send_update/2. I recommend reading the Phoenix.LiveComponent documentation, which explains all these concepts very well. See: Phoenix.LiveComponent — Phoenix LiveView v1.0.5.