longnightElixir

longnightElixir

Hi, I have some usage scenario to ask:

Users navigate between ` /product/product_a_id ` and ` /product/product_b_id` , and more ;
these routers are in the same liveview;

I have background running jobs will send events (and data) to users who is staying on specified product detail page; this may approach by using Pubsub or Register;

The user who left ` /product/product_a_id ` should not receive those event sent to product_a, but he should receive event sent to product_b for he navigated there.

For its in the same liveview, I have to add logics in handle_params to detect url changed then unsub, sub product_id related things.

are there some tools do this more elegantly ?

Thanks !

Showing Posts 1 to 10

krasenyp

krasenyp

You basically need a piece of code which filters the pubsub events based on the currently viewed product. I don’t think theres a much better solution to what you have - using handle_params. You can subscribe to receive all the pubsub messages of the background jobs and filter in handle_info based on the identifier of the currently viewed product. This avoids the unsubscribe/subscribe dance but introduces more messages in the mailbox.

garrison

garrison

Unfortunately LiveView lacks the primitives needed to perform generalized incremental computation. For example React has useEffect() and useMemo(), but Phoenix lacks even a full implementation of the older (inferior) lifecycle paradigm: it has mount() and update(), but no unmount().

This makes interop between declarative and imperative code rather… difficult.

Of course you can abandon the incremental approach entirely. The root of the LiveView can subscribe to everything and then meter updates out to individual components.

def handle_info({:product_update, message}, socket) do
  send_update Components.ProductA, id: "product-a", message: message
  send_update Components.ProductB, id: "product-b", message: message
  send_update Components.ProductSidebar, id: "product-sidebar", message: message
  # ...
end

But now, and this kind-of hints at the problem, the structure of your application has become entirely rigid and static. Yes you can unmount A or B and the messages will be ignored, but you have lost the ability to dynamically subscribe. The ability to dynamically mount/unmount components and incrementalize computation is the fundamental raison d’etre for React-style engines, and LiveView is unfortunately derelict of its duty in this department.

If you’re clever you might think you could transmit subscriptions from dynamically mounted components back up to the root. Believe me, I thought that too. But putting aside for the moment that component ids in Phoenix don’t compose (an unrelated issue), this is still insufficient. You cannot tell if a component has been unmounted, so there is no way for a component to clean up after itself.

Right about now some of you may be getting some ideas about how you might hook into the code that leads to an unmount, upstream, and unsubscribe there. Like at the router level, or in response to an event. Let me stop you right there: you are giving up not only incrementalization, but declarative programming as a whole. You are writing imperative code and will be cursed by the gods. Many stronger than you have ventured down this path, and all have fallen. Abandon hope, all ye who enter here.

To be clear, there is no fundamental or technical reason why LiveView cannot provide this functionality. Instead I think it’s mostly a cultural difference; that is to say, most of the devs using LiveView do not understand or need any of this, and would probably get by just fine with something like HTMX if they didn’t happen to be writing Elixir. I used to think that it would be a good idea to extend LiveView with more incremental functionality, but in reality the depth of the changes needed would likely serve to annoy those who don’t want, need, or understand them in the first place. Plus it’s not like I’m volunteering to do the work.

It will likely be easier to write a new framework from scratch.

(And in general I do think it would be nice to have more app frameworks in Elixir, which is why I’m rooting for Hologram!)

TLDR: No, not really.

krasenyp

krasenyp

I don’t think OP is using live components. They’re probably navigating to the same live view and because it’s in a session, only the handle_params callback is called.

Minor aside. Using sens_update to update child live components is such a code smell to me, I can’t even start to explain. Please use assigns.

garrison

garrison

The fact that there is any noticeable distinction between LiveViews and LiveComponents is another unfortunate design wart, but what I wrote still applies if you only have one component (the root LV).

I would love to see you try, but I think there is some misunderstanding here. The code snippet in my post is a PubSub router. It’s not holding state, it’s forwarding messages. Really it’s doing what you suggested above, just extended to components (which cannot receive messages). I do think this is a bad idea, but for much deeper reasons.

longnightElixir

longnightElixir OP

Thank you above two;
Of course, I am not talking about live component; my scenario is just simple case of liveview router/naviagtion.
Now I am considering , to offload these logics into some on_mount hook, to reduce code repeat.

garrison

garrison

You really can almost get away with it, though.

Conjuring a reconciler from the void:

def handle_params(%{"product_id" => new_id}, _uri, socket) do
  if (old_id = socket.assigns[:product_id]) != new_id do
    if old_id, do: unsubscribe(old_id)
    subscribe(new_id)
  end
  {:ok, assign(socket, :product_id, new_id)}
end

It handles mount and update because handle_params is called by the engine for both and the reconciler is idempotent. It does not handle unmount, but because this is the root LiveView we get lucky and the PubSub system will throw away our subscription when the LiveView dies.

However, we are cheating quite badly. First of all, this trick only works because the root LV always replaces itself. If you were to replace the root LV with another component in the same process (say by navigation), you would be back to square one because there’s no unmount. Same problem if you tried to use components. The fact that you can get away with this at all is sheer luck, and this solution is a house of cards that will quickly buckle as soon as you add real functionality to your app.

Also, notice that we are conveniently running the state through the router. If the product_id was a part of the app’s actual state (which serious apps must be able to do, contrary to what many think), there would be no single place to observe changes. Next thing you know you’re writing imperative code again, cursed by the gods, etc.

If you’re wondering what a real solution to this problem looks like, here it is:

function Product({productId}) {
  useEffect(() => {
    subscribe(productId);
    return () => unsubscribe(productId);
  }, [productId]);
}

This is not perfect either because React has warts of its own: for one, useEffect is not synchronous (and neither is useLayoutEffect; they lied to you).

But it’s certainly a lot better. The function returns a continuation that the runtime can slice properly into the three lifecycle events (mount, update, unmount) while you get to write code that looks like it’s in the proper order (subscribe first, unsubscribe later). And there’s no messing about with the diff because the runtime has a reconciler built in.

tfwright

tfwright

Seconding @krasenyp , and also would ask, why create a topic for each product rather than have a single “product_jobs” or something topic where the event contains the product id along with whatever metadata about the event, which you can then compare to state of current view, whether LV or whatever?

garrison

garrison

Because if you take this to its logical conclusion it won’t end well.

Say you want to add product reviews to your app. Now you’re back where you started: do you want to subscribe to product_events or review_events or review_events[product_id] or some combination thereof?

Applying your strategy again, you could then create a unified product_and_review_events topic and the client can again filter as they wish. Your app gets some more users and the client sure is receiving a lot of events now but it’s probably fine.

And then you decide to add “seller updates” to the product page. Okay, product_and_review_and_update_events it is, I guess. Whatever.

This happens a few more times, so you finally give in and just rename the topic to literally_all_events_from_the_entire_database. Every minuscule facet of your app is globally broadcasted to every user. You are essentially streaming the Postgres WAL to every single client in parallel.

Unfortunately this will not scale.

You can, by the way, shard your setup in some way such that you can stream all events for a particular shard. For example, if you had a Slack-style app it might be totally reasonable to send the client all events for a given workspace_id.

But, hey, how do you choose which workspace_id to subscribe to? What happens if the user switches workspaces?

And so we have invented the same problem again.

tfwright

tfwright

Not sure about any of your other hypothetical scenarios–those are not what OP has described and I think it would be prematurely optimizing to handle them in advance (YAGNI). But given the parameters OP has actually described, there’s certainly the tradeoff (already mentioned) that the product view is going to receive more irrelevant messages. Given a high enough number of products actively generating new events, it could create a problem, but personally I’d make that optimization then and not before because, unless you’re Amazon or Temu or whatever, OPs storefront most likely is going to have a fairly constrained number of “active” products at a given time. But my opinion is based on experience dealing with more pain from prematurely (and often poorly) optimized implementations than unperformant ones.

krasenyp

krasenyp

Well, many people have faced this problem and the solutions is to have a piece of code which filters the messages. The live view can have a sidecar process which is subscribed to a few topics and filters, and routes the messages to the live view process.

If more data is needed for some messages, the sidecar can enrich them by calling some internal or external API.

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
rahultumpala
Hello, I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews