What is a bare bones example of doing a fetch call in liveview?

In Node if I want the data from a JSON api I do:

const response = await fetch('https://api.whatever.com');

What is the live view version of the fetch line I posted above?

When I search for (and use LLM’s to give me examples of) equivalent code for Elixir Liveview the replies are always seem large and bulky.

Claude AI gave me all this stuff


# First, we need to set up our LiveView module

defmodule MyAppWeb.ApiDemoLive do
  use MyAppWeb, :live_view
  require Logger

  def mount(_params, _session, socket) do
    # Initial state
    {:ok, assign(socket, 
      results: nil, 
      loading: false,
      error: nil
    )}
  end

  def render(assigns) do
    ~H"""
    <div class="container mx-auto p-4">
      <h1 class="text-2xl font-bold mb-4">API Data Demo</h1>
      
      <button phx-click="fetch_data" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
        Fetch Data
      </button>
      
      <%= if @loading do %>
        <div class="mt-4">Loading data...</div>
      <% end %>
      
      <%= if @error do %>
        <div class="mt-4 text-red-500">Error: <%= @error %></div>
      <% end %>
      
      <%= if @results do %>
        <div class="mt-4">
          <h2 class="text-xl font-semibold mb-2">Results:</h2>
          <pre class="bg-gray-100 p-4 rounded"><%= Jason.encode!(@results, pretty: true) %></pre>
        </div>
      <% end %>
    </div>
    """
  end

  def handle_event("fetch_data", _, socket) do
    # Set loading state
    socket = assign(socket, loading: true, error: nil)
    
    # Start async operation
    %{pid: pid} = Task.async(fn -> fetch_api_data() end)
    
    {:noreply, assign(socket, async_task: pid)}
  end

  # Handle successful API response
  def handle_info({ref, result}, socket) when is_reference(ref) do
    # Clean up the task
    Process.demonitor(ref, [:flush])
    
    case result do
      {:ok, data} ->
        {:noreply, assign(socket, results: data, loading: false)}
      {:error, reason} ->
        {:noreply, assign(socket, error: reason, loading: false)}
    end
  end

  # Handle task failure
  def handle_info({:DOWN, _ref, :process, _pid, reason}, socket) do
    {:noreply, assign(socket, loading: false, error: "Process failed: #{inspect(reason)}")}
  end

  # Function to fetch data from API
  defp fetch_api_data do
    case HTTPoison.get("https://jsonplaceholder.typicode.com/posts/1") do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:ok, Jason.decode!(body)}
      {:ok, %HTTPoison.Response{status_code: status_code}} ->
        {:error, "API returned status code: #{status_code}"}
      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, "HTTP client error: #{inspect(reason)}"}
    end
  end
end

# In your router.ex file:
# live "/api-demo", ApiDemoLive

# Make sure to include these dependencies in your mix.exs:
# {:httpoison, "~> 2.0"},
# {:jason, "~> 1.4"}

Yea this is overkill since you are awaiting in the JS example. You would simply call fetch_api_data() in the mount function or handle_event. It will block the view but so does your JS example by calling await.

If you want to do that asynchronously and have the page use a “loading…” type state for data that depends on the API data see Phoenix.LiveView — Phoenix LiveView v1.0.9

The async documentation doesn’t have a simple real world example of what code looks like for pinging a web API. I feel like this is something any web developer should either know how to do like the back of their hand or know how to quickly refresh their memory and find out via a quick search. I can’t even find an honest generic bare bones example of how to do it without feeling inundated with unneeded complexity.

I’m trying to put together a cliff note for future reference that I am confident is the right way to do it. I have a site where I create notes for all my phoenix learning and I just want to tack this on so I don’t need to hobble around the net like a leper.

Most of my notes are really stupid simple like this:
http://elixirblocks.com/How_to_Write_an_Event_Handler_in_Phoenix

I’m hoping to write one for an async call to a web api so I have a template to follow.

1 Like

Again the simplest thing to do is to just make the API call. As in literally just result = Req.post(….) This is identical to your JS code in that it blocks the currently executing function. It is still asynchronous with respect to the rest of your program, because each live view is its own process.

I can’t tell you the best way to do a call that is async to your live view process itself because you haven’t provided a bunch of relevant information, nor does your JS example. Are you limiting the request to one at a time? What do you do on error? What do you do with the result? What should the end user see while this is happening?

Do you mean something like this?

  @impl true
  def mount(%{"id" => id, _session, socket) do
    with {:ok, customer} <- Customers.fetch(id) do
      {:ok, 
        socket
        |> assign(:id, id)
        |> start_async(:peppol, fn -> check_peppol(customer) end)}
    end
  end

  @impl true
  def handle_async(:peppol, {:ok, true}, socket) do
    socket = socket |> put_flash(:info, "Peppol is now active")
    handle_event("validate", %{"customer" => %{"extra" => %{"peppol_active" => "true"}}}, socket)
  end

  def handle_async(:peppol, {:ok, false}, socket) do
    {:noreply, socket}
  end

  defp check_peppol(%{vat: vat}) do
    Peppol.check_active?(vat)
  end

And in module Peppol:

  @spec check_active?(String.t()) :: boolean()
  def check_active?(vatnumber) do
    with {:ok, response} <-
           Req.get(
             url("api/verify/#{URI.encode(vatnumber)}"),
             auth: authorization_header()
           ),
         200 <- response.status do
      true
    else
      _ -> false
    end
  end

I piggyback on the "validate" event-handler to update the form with the result of the external request.

2 Likes

All I wanted was to do this and all my searches lead to big chunky blobs of code.


defmodule AppWeb.Sandbox do
  use AppWeb, :live_view
  def mount(_params, _session, socket)  do

  {:ok, socket}
  end

  def api do

    Req.request(url: "https://api.github.com/repos/wojtekmach/req")

  end

  def render(assigns) do
     IO.inspect api()
    ~H"""
       SANDBOX
  """
  end

end

I assume that to make this an async call it actually does require a bit more code

1 Like

You wouldn’t do this in render, but rather on mount or in a handle_event call in response to an event. render should be pure, that’s the same in React or Vue.JS or Phoenix.

  1. Your JS function is not actually async with respect to the function its in.
  2. It doesn’t have to be more code if you make it an assign. async_assign is really a small bit of code.
1 Like

I just wanted to ping the api and get something back. I know it is a poor way of doing it for a “real app”. Thank you

Sure, in that case:

defmodule AppWeb.Sandbox do
  use AppWeb, :live_view
  def mount(_params, _session, socket)  do
  results = Req.request(url: "https://api.github.com/repos/wojtekmach/req")
  socket = assign(socket, :results, results)
  {:ok, socket}
  end

  def render(assigns) do
    ~H"""
       <%= inspect(@results) %>
  """
  end

end
4 Likes

If you want to do the request asynchronously, you can use assign_async as @benwilson512 suggested. An example of that:

defmodule AppWeb.Sandbox do
  use AppWeb, :live_view

  def mount(_params, _session, socket) do
    socket =
      assign_async(socket, :results, fn ->
        {:ok, %{results: Req.request(url: "some.url")}}
      end)

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <.async_result :let={results} assign={@results}>
      <:loading>Loading...</:loading>
      {results}
    </.async_result>
    """
  end
end
3 Likes

Your question is a bit misleading. I don’t think you’re looking for the LiveView version of const response = await fetch('https://api.whatever.com');.

What you might actually be looking for is an Elixir HTTP client. This is the popular one:

1 Like

This looks excellent. I will reply more later.

To perform an async GET request from an event handler, it requires the developer to know that there is a relationship between Task.async and handle_info. This is not immediately apparent unless you’ve been told about it. If this is new to you, I’m telling you about it.

defmodule AppWeb.Sandbox do
  use AppWeb, :live_view

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

  def handle_event("start_task", _params, socket) do
    Task.async(fn -> Req.get!("https://api.github.com/repos/wojtekmach/req") end)
    {:noreply, socket}
  end

  def handle_info({ref, result}, socket) do

    Process.demonitor(ref, [:flush])
    IO.inspect"________________________________________________________________________________"
    IO.inspect({:task_result_received, ref, result})
    {:noreply, socket}
  end

  def render(assigns) do
    ~H"""
    <div>
      <button phx-click="start_task">Start Task</button>
      <p>Barebones</p>
    </div>
    """
  end
end

If I run the above code with the handle_info function removed,

defmodule AppWeb.Sandbox do
  use AppWeb, :live_view

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

  def handle_event("start_task", _params, socket) do
    Task.async(fn -> Req.get!("https://api.github.com/repos/wojtekmach/req") end)
    {:noreply, socket}
  end

  # def handle_info({ref, result}, socket) do

  #   Process.demonitor(ref, [:flush])
  #   IO.inspect"________________________________________________________________________________"
  #   IO.inspect({:task_result_received, ref, result})
  #   {:noreply, socket}
  # end

  def render(assigns) do
    ~H"""
    <div>
      <button phx-click="start_task">Start Task</button>
      <p>Barebones</p>
    </div>
    """
  end
end

I get this error:

undefined handle_info | Unhandled message

This is telling the user that there is a relationship between Task.async and handle_info albeit in a cryptic way that does not guide the user to the solution, unless they are already familiar with the pattern.

I don’t have a question, I’m just posting this for the next person that reads this thread.

At this point I need to ask you, what did you expect to happen?

This is documented

Alternatively, if you spawn a task inside a GenServer, then the GenServer will automatically await for you and call GenServer.handle_info/2 with the task response and associated :DOWN message.

Note: you can call assign_async/4 or start_async/4 from a handle_event/3 callback (or any other callback), you don’t need to use Task.

2 Likes

At an absoulte bare minimum, you can do this using assign_async

defmodule AppWeb.Sandbox do
  use AppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign_async(socket, :json_request, fn -> {:ok, %{json_request: Req.get!("https://api.github.com/repos/wojtekmach/req")}} end)}
  end

  def render(assigns) do
    ~H"""
    <pre>{@json_request.ok? && JSON.encode!(@json_request.result)}</pre>
    """
  end
end

As the docs state, json_request becomes an AsyncResult in which .result will contain the response from Req.get!, and .ok? indicates wether :ok was returned in your assign_async.


Id probably suggest using async_result component though to handle when its ‘loading’ a bit better

defmodule AppWeb.Sandbox do
  use AppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign_async(socket, :json_request, fn -> {:ok, %{json_request: Req.get!("https://api.github.com/repos/wojtekmach/req")}} end)}
  end

  def render(assigns) do
    ~H"""
    <.async_result :let={json_request} assign={@json_request}>
      <:loading>Loading content...</:loading>
      <:failed :let={_failure}>there was an error loading the content</:failed>
      <pre>{JSON.encode!(json_request)}</pre>
    </.async_result>
    """
  end
end

Whilst you could use start_async, it’s not nesscary in this case. That’s because it’s more suited for (imo) when you need to trigger the async after another async has completed.

The responses from the LLM, in this case, are unhelpful and inaccurate.

Given the choice, you should be using assign_async or start_async in the LiveView if possible. Their isn’t a need to be doing Task.async in your case.

I would recommend doing the JSON decoding in the async function rather than while rendering, that way if there’s an error it’s not raised while rendering

2 Likes