Calling external APIs from Hologram

Hologram is amazing and I love playing with Elixir in Front-End as well instead of using some JavaScript/TypeScript libraries/frameworks.

Just another topic, but how can I interact with some external APIs on the client side (not from the database)? Can I use something like fetch API/Axios or even some Elixir’s HTTP client library like Req?

Anyway, thanks for the hardworking.

3 Likes

Welcome to the forum @liamnguyen! :slight_smile: Great to hear you’re loving the Elixir frontend experience! That’s exactly what Hologram was designed for.

For external API calls, Hologram’s server-side commands mechanism is the recommended approach - and it’s a feature, not a limitation. It keeps your API keys secure, allows proper request validation, and gives you full control over data flow between external services and your clients.

That said, there are a few specific cases where client-side requests might make sense, for example public APIs designed for direct client access like weather widgets, maps, etc.

I’m considering creating a module that would abstract HTTP requests with a consistent API for both client and server contexts, letting you choose the appropriate execution location. However, this isn’t a priority right now since the vast majority of use cases are better served by the secure server-side approach.

For now, I’d recommend routing your external API calls through Hologram’s commands mechanism. It’ll keep your application secure.

Hope this helps!

1 Like

Another common one is file uploading. An isomorphic Req could be cool.

2 Likes

Is there a way to fetch data from an API (on load) without render blocking? If I add :timer.sleep(5000) to my request, the page does not load until the request is complete.

I attempted to fetch the response with Task.async but I can’t seem to get the command to execute on init, whether in a page or component:

prop :todo, :map, default: %{}

def init(_params, component, _server) do
  put_command(component, :fetch_todos)
end

def command(:fetch_todos, _params, server) do
  IO.inspect("Fetching todos...")

  todos =
    Task.async(fn ->
    :timer.sleep(5000)

    endpoint = "https://jsonplaceholder.typicode.com/todos"
    Req.get!(endpoint).body

  end)
  |> Task.await(10_000)

  todo = Enum.random(todos)
  put_action(server, :add_todo, todo: todo)
end

def action(:add_todo, %{todo: todo}, component) do
  put_state(component, :todo, todo)
end
1 Like

Yeah, file uploading with pre-signed URLs is another great use case!

Besides that, an isomorphic HTTP client would become essential once we start implementing Local-First features.

1 Like

put_command/3 and put_action/3 queuing from init/3 isn’t supported yet. For put_command/3 we need to first support sending actions or events outside commands from any server-side code. But put_action/3 is straightforward to implement now, and I’ll prioretize implementing it, because it will enable creating workarounds for such cases as you described. This is how it would work:

prop :todo, :map, default: %{}

def init(_params, component, _server) do
  put_action(component, :fetch_todos)
end

def action(:fetch_todos, _params, component) do
  put_command(component, :fetch_todos_async)
end

def command(:fetch_todos_async, _params, server) do
  IO.inspect("Fetching todos...")

  todos =
    Task.async(fn ->
      :timer.sleep(5000)
      endpoint = "https://jsonplaceholder.typicode.com/todos"
      Req.get!(endpoint).body
    end)
    |> Task.await(10_000)

  todo = Enum.random(todos)
  put_action(server, :add_todo, todo: todo)
end

def action(:add_todo, %{todo: todo}, component) do
  put_state(component, :todo, todo)
end

Good to know, thanks! I’m seeing a repeating pattern where an action needs to be created to accept changes from a command.

def command(:fetch_todos_async, _params, server) do
  ...
  put_action(server, :add_todo, todo: todo)
end

def action(:add_todo, %{todo: todo}, component) do
  put_state(component, :todo, todo)
end

Would it make sense to have a function that facilitates that automatically, or maybe an anonymous function? Or does that feel too magical?

def command(:fetch_todos_async, _params, server) do
  ...
  update_state(server, todo: todo)
end
2 Likes

Yeah, I’ve noticed this pattern emerging too! You’re right that it feels repetitive having to create an action just to accept changes from a command.

I’m intentionally holding back on implementing sugar syntax or DSL features right now - I want to wait until more usage patterns emerge and solidify. I think we should especially wait until some primitives related to command error handling and resiliency are implemented first.

Something like push_state or put_component_state (and similar) seems nice at first glance - no side effects, clean and direct. But we also need to think about whether it might affect the DX eventually. The mental model of “actions on client, commands on server” is already a little confusing for developers new to this kind of architecture, and adding features that blur this separation could make it even more confusing.

I’d say let this idea ripen for some time and see how the patterns evolve as more people use the framework.

What do you think? Does that reasoning make sense from your perspective?

1 Like

Funny, this client/server interface is exactly the one that OTP enforces via GenServers :slight_smile:

My opinion: if you want to build an interactive app, you want the state on the client. This is the key differentiator for Hologram.

If you want to ship state owned by the client to the server then the client should still be responsible for performing state mutations. You can then checkpoint the state back to the server at regular intervals. In this case you want the endpoint on the server to be as general as possible. So I don’t think a specialized push_state() is necessary as it should be trivial to create a one-off :update_state action and shovel all of your state through it anyway. What really matters is the policy for what can be accepted, which is not the framework’s concern (unless you want to design a framework for that).

Finally, if the server owns the state and the client only issues restricted updates then you want to stick to the “action” model you have in that case anyway. This is how e.g. LiveView works now, and is a reasonable model for some problems.

The cool thing about Hologram is that it can be all three of these at once. But I don’t think any of them necessitate the API proposed (push_state()). Ideally you should encourage developers to define a general policy for what state to accept based on their application’s needs.

Also, providing tools to accept state on the server with no validation sounds like a security footgun waiting to happen.

3 Likes

Just to clarify the idea that was floated: push_state would be called inside a server-side command and would only push a state update down to the client-side component. No state would be accepted from the client to the server through this API.

Concretely, it would just be sugar for the existing pattern where a command enqueues a client action and that action calls put_state on the client. The proposal doesn’t change the trust boundary or validation story, it only removes boilerplate for the “command → action → put_state” sequence.

I also think this is what @absowoot meant initially - @absowoot, does this match your intention?

Another example of the same pattern:

  1. User fills a form
  2. A command runs on the server to create a user in the database
  3. The command triggers an action that updates the client-side component state with the created record

Today:

def command(:create_user, %{data: data}, server) do
  {:ok, user} = MyApp.Users.create_user(data)
  put_action(server, :user_created, user: user)
end

def action(:user_created, %{user: user}, component) do
  put_state(component, :user, user)
end

Proposed sugar:

def command(:create_user, %{data: data}, server) do
  {:ok, user} = MyApp.Users.create_user(data)
  push_state(server, user: user) # sugar for enqueuing a client-side operation that calls put_state/3
end

Anyway, this is just an idea - I’d still like to let more usage patterns emerge (and land resiliency/error-handling primitives) before adding sugar so we don’t blur the “actions on client, commands on server” mental model.

3 Likes

I see, looks like I had it the wrong way around.

I imagined Hologram would synchronize state like this back automatically. Maybe an API focused on sync or subscription rather than explicit pushes would be more intuitive? Of course you have to be careful not to accidentally build a database :slight_smile:

A wise path to take in general.

2 Likes

push_state() seems like a good idea.
It clearly states the intent of pushing state from the server to the client.

For me the put_action in the command seems like issuing instructions to the client to change the state with the data being passed. Just like dispatching an event.

push_state() - prima facie seems better and more intentional.

1 Like

How about push_client_state instead? Recently I found that I really like the explicit naming even if it’s longer and declaring that some data would be set on client side more clearly shows (without a need to read documentation) where the rendering would happen. :books:

Also since the typical naming for 1st argument is server it may be seen as confusing at least at first. The explicit naming says that we are instructing server to push state to the client which make more sense in my opinion. :thinking:

4 Likes

100%

Fwiw, I really like this idea and think it’d be worth exploring. Maybe it’s explored in tandem with the planned subscription DSL and local first features.

2 Likes

Will there be the possibility of storing state on the server as well? I don’t mean session or cookie state, but state that is used during the lifecycle of the page or component.

For example, with something like AshPhoenix.Form, you would store a form struct in the live view assigns, and reuse it on each form validation and submit.

1 Like

Having state in 2 places might lead to extreme confusion.

2 Likes

Yeah maybe… the server state would only be available in commands though so it won’t be exposed everywhere. It will be interesting to see how patterns play out as more things get built

1 Like

I am nothing and nobody to post anything here, so forgive my intrusion but is it feasible, in the interest of aiming for a truly isomorphic approach, to keep action/command separation invisible and make code run through three modules in a kind of kernel: Action | Dispatch | Response. Then there’s no ‘client or server?’ branching in app code because the transport swaps out under the hood so to speak.
ie contract in Behaviours/Protocol, the kernel + two transports in one ‘interface’. It’s probably not very Elixir, sorry.

(I learnt this kind of pattern through John Elm Labs & a bit of coding gnome - kudos to them)

Loving Hologram btw. It’s a thing of beauty.

1 Like

Yep, automatic/declarative data sync is planned for the next stages - would definitely be more intuitive and reduce boilerplate!

2 Likes

Thanks, noted!
This needs to settle and crystallize properly - it could have consequences for other naming patterns. For example, would we have push_session, push_cookie etc? This could streamline the whole model but also potentially be confusing. But looks promising!