jtormey

jtormey

Using macros to create React-like callback props in LiveView

I’m sure some of you have experienced this as well, in a growing LiveView application it can become difficult to trace flow of data between a LiveView and child components that communicate via send/2 / handle_info/2.

Typically, it looks like this:

# In AppWeb.ParentLive

def render(assigns) do
  ~H"""
  <.live_component id="child" module={AppWeb.ChildComponent} />
  """
end

def handle_info({:from_child, data}, socket) do
  # Handle message from child
end

# In AppWeb.ChildComponent

def handle_event("some_event", _params, socket) do
  send(self(), {:from_child, %{some: :data}})
  {:noreply, socket}
end

With many children each sending multiple messages to a parent, you can see how this might make navigating code confusing.

Now, what if we could simply pass functions defined in the parent LiveView to children, like how is typically done in React? The reason we “can’t” do this is because we need access to socket… but there’s nothing stopping us from hiding implementation details with a macro.

Take this counter example using a defcall macro (shown at the end of the post):

defmodule AppWeb.CounterLive do
  use AppWeb, :live_view

  import Defcall

  def mount(_params, _session, socket) do
    {:ok, assign(socket, :count, 0)}
  end

  def render(assigns) do
    ~H"""
    <.live_component
      id="counter"
      module={__MODULE__.Counter}
      count={@count}
      on_inc={&inc/1}
      on_dec={&dec/1}
    />
    """
  end

  defcall inc(by_value, socket) do
    count = socket.assigns.count
    {:noreply, assign(socket, :count, count + by_value)}
  end

  defcall dec(by_value, socket) do
    count = socket.assigns.count
    {:noreply, assign(socket, :count, count - by_value)}
  end

  defmodule Counter do
    use AppWeb, :live_component

    def render(assigns) do
      ~H"""
      <div>
        Count: <%= @count %>
        <button phx-click="inc" phx-target={@myself}>Increment</button>
        <button phx-click="dec" phx-target={@myself}>Decrement</button>
      </div>
      """
    end

    def handle_event("inc", _params, socket) do
      socket.assigns.on_inc.(1)
      {:noreply, socket}
    end

    def handle_event("dec", _params, socket) do
      socket.assigns.on_dec.(1)
      {:noreply, socket}
    end
  end
end

Neat! Now, we can define functions in our parent and pass them to child components. It looks like they’re being called directly, but in reality we’re using the same message passing technique as before behind the scenes.

Here’s the full macro code for reference:

defmodule Defcall do
  @doc """
  This:

  defcall inc(by_value, socket) do
    count = socket.assigns.count
    {:noreply, assign(socket, :count, count + by_value)}
  end

  Becomes this:

  def inc(by_value) do
    send(self(), {:__prop_callback__, :inc, [by_value]})
  end

  def handle_info({:__prop_callback__, :inc, [by_value]}, socket) do
    count = socket.assigns.count
    {:noreply, assign(socket, :count, count + by_value)}
  end
  """
  defmacro defcall(fn_spec, do: block) do
    {fn_name, _, args} = fn_spec

    # Drop the socket arg, should add some error handling here
    args = Enum.drop(args, -1)

    quote location: :keep do
      def unquote(fn_name)(unquote_splicing(args)) do
        send(self(), {:__prop_callback__, unquote(fn_name), [unquote_splicing(args)]})
      end

      def handle_info(
            {:__prop_callback__, unquote(fn_name), [unquote_splicing(args)]},
            var!(socket)
          ) do
        unquote(block)
      end
    end
  end
end

Anyway, just wanted to share and hear if anyone had thoughts on a system like this. And even if it’s flawed somehow, it’s amazing how easy it is to expand our tools using the language constructs available to us.

How else have you organized messaging between components?

Most Liked

cmo

cmo

I namespace them, e.g "component:message" or {Component, :message}.

Where Next?

Popular in Discussions Top

sashaafm
Piggy backing a bit on @dvcrn topic BEAM optimization for functions with static return type?, I’ve been trying to understand in a deeper ...
New
lucaong
Hello Elixir and Nerves community, I have been working for a while on an open-source embedded key-value database for Elixir, that I call...
230 13924 124
New
Crowdhailer
I’ve been hearing much about the new formatter and it’s something I have been keen to try. I find examples buy far the most illuminating...
248 19204 150
New
PragTob
Hey everyone, this has been brewing in my head some time and it came up again while reading Adopting Elixir. GenServers, supervisors et...
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
ben-pr-p
In general I’ve been sticking to this community style guide GitHub - christopheradams/elixir_style_guide: A community driven style guide ...
New
wmnnd
The Go vs Elixir thread got me thinking: Would it be too hard to implement a simple mechanism for creating Go-style static app binaries f...
New
AstonJ
If so I (and hopefully others!) might have some tips for you :slight_smile: But first, please say which area you’re finding most challen...
New
scouten
I’m looking for a host for the server part of a small (personal) side project that I’m working on. It’s currently written in Node.js and ...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New

Other popular topics Top

mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement