Literally everyone who I worked with jumped on creating reusable components by default. The abstraction is both familiar and intuitive. That’s why people use it. Sending events between components or components and processes on the server side has been coming up on regular basis too, and it’s solvable with several different hacks, none super pretty.
Coupling of UI and it’s state, and events into some sort of components that can be reused, embedded into each other and behave like self-containing units is familiar to anyone who worked with an UI toolkit in either object-oriented or functional environments alike, be it Qt or React or Gtk or really almost anything.
There is actually a very interesting and fundamental difference between these approaches.
The LiveComponent approach is “composed” during rendering. Whether a child component is mounted or not is determined declaratively, at render-time, by what appears in the template.
But the assign_live_table approach “mounts” the component imperatively. This is actually a substantial restriction.
For example, let’s say we have a complex UI where we choose to render the table based on two variables, “current_tab” and “show_tables”. Imagine the former represents the UI state and the latter represents some setting or preference of the user.
With LiveComponents, we get:
<.live_component
:if={@current_tab == :foo and @show_tables}
module={LiveTable}
id="foo-table"
/>
So if the user changes the tab, or changes their settings the table will appear or disappear in real-time, taking its state with it.
With an imperative approach, we have to mount/unmount the table in response to an event, imperatively, meaning we now have to start writing special-case code. In this case, that would mean annotating event handlers for both tab change events and settings change events (which might come in over PubSub, say) with code to mount/unmount the table. That already sounds messy, but as the UI grows it will get exponentially worse, eventually becoming intractable.
One solution would be to add some sort of “side-effect” API to LiveView which allows you to run functions in response to some assigns changing - something like useMemo or useEffect in React. I have run into a number of cases where I would find something like that useful anyway, and I’ve been considering trying to implement it as a library. Is anyone aware of any prior art here?
Definitely. There are still good use cases for live components, conditional rendering can be one of them and another one is encapsulating application logic rather than generic UI elements.
I agree with your approach as a good default approach. In my experience, having 100% functional components and handling the logic from the LiveView makes things much easier to reason about when trying to use patch links and params to control a datatable with sorting/filtering/pagination etc. When datatable components have their own state, it becomes very difficult to manage patch links and param state. I find a lot of value in LiveView’s ability to mostly work without Javascript, and treating url params as first class citizens is much easier to reason about at the root of the tree rather than at the leaves.
Mm, but that’s what’s funny. It’s not actually conditional rendering which differs between the approaches. You can render the live table conditionally either way. You can even mount it conditionally either way!
The difference is that the LiveComponent approach is declarative, that is to say it will “automatically” mount/unmount the component based on the assigns. In the assign_live_table case we are forced to do this imperatively, meaning we need special-case code. It can still be done, but the imperative model is more prone to bugs and manual work, which is no fun.
But what I think is interesting, and the reason I brought it up, is that we could have a “declarative” assigns API, where we reduce some functions over the assigns state, similar to React useMemo and useEffect. For example, we might have an API like:
I think it would be possible to hack this together as a library by essentially overriding assign/3, and doing so is on my todo list because I run into use cases for this all the time. I have so many functions which imperatively re-compute assigns based on other assigns to get around this.
But I do think an API like this belongs in LiveView itself.
Declarative is often used to mean “hiding implementation details” - and don’t get me wrong, it is a big part of it - but the trouble is that LiveComponents and useEffects are also side-effects, so you are choosing to hide the side-effects as well as part of your implementation. And, in Elixir, we generally tend to avoid hiding them, because side-effects make understanding the code harder, since things change from under your feet without you noticing it or without your control.
That’s exactly why I prefer my approach, the state management is explicit, over an assign, and not hidden away elsewhere. That’s not to say all side-effects are bad but there is a trade-off here.
FWIW, I asked Claude “what declarative means in programming”, because I have a different read than yours. I think it can help is “equalize” our understanding:
Declarative programming is a programming paradigm that expresses the logic of a computation without describing its control flow explicitly. It focuses on “what” should be computed rather than “how” it should be computed. Here’s a breakdown of what declarative programming means:
Key Characteristics of Declarative Programming
Describes what the program should accomplish, not the step-by-step process to achieve it
Hides implementation details from the programmer
Minimizes side effects by avoiding changes to program state
Focuses on the relationships between things rather than sequential operations
Comparison with Imperative Programming
Declarative programming contrasts with imperative programming, which focuses on explicitly describing how a program operates through statements that change a program’s state.
I allowed myself this in some projects, but isolated into a state management module, and with a single derivation layer allowed (enforced by always shuffling the update order of derivations). Allowing dependencies between computed properties is dangerous, but a single layer of derived views on data can be useful.
In my previous life, as a front-end focused developer, we used to say “data down, events up”. I refer to it when evaluating component designs. In my opinion, live views should orchestrate communication between live components. The latter send events to the parent live view, it does what it does, and updates it’s assigns, which are also the inputs of the live components. Communication via send_event between components that are on the same level in the hierarchy is a code smell.
Unfortunately “declarative” might be the single most overloaded term in our field, and I am definitely not helping by using it here I am also quite partial to the “referential transparency” definition, but that’s not what I meant in this case.
What I was trying to convey is that I want an approach which “declares” the dependencies of a “computed/derived/memoized” assign up-front (ideally on mount), as opposed to sprinkling that dependency around in various event handlers and update callbacks like I find myself doing far too often.
I attempted to use “react-like” and “jquery-like” analogously to describe what I mean in a previous discussion on here, but that resulted in people thinking that I was literally referring to JS frameworks, so I abandoned that approach quickly.
I think a good candidate would be “reactive”, as used in my favorite programming article. Unfortunately I’m not sure this term has a good inverse (like “imperative”). Un-reactive? I’ll figure something out
I understand where you’re coming from, but I am not convinced that this sort of “hook” approach is actually a side-effect. In all of the cases I’ve encountered the functions would actually be pure. I just want to be able to declare “when either of these assigns changes, re-compute that one from them”.
I often have to do things like call “update_x(socket)” every time I change y, which could be in several places. I don’t like that pattern because it’s error-prone, and it buries the dependency in the implementation details instead of at the top of the component where it can be clearly observed.
I would definitely avoid using this sort of feature for actual side-effects (like hitting the database). I agree that’s a very dangerous road.
The idea is to essentially extend the “reactivity” of template rendering to some of the assigns. Templates in Phoenix are (almost always) a pure function of state, which is what I love. React tries to take the same approach but JS is not designed for immutability like Elixir so the UX suffers badly. We are able to do much better.
I was just trying to demonstrate the API - the example was not meant to be taken seriously. Obviously you would be better off performing a foo == "bar" check right in the template, because it’s cheap.
Your comment actually brings up a great point: there is a reason this functionality in React is called useMemo. I called that function derive without thinking because that’s what I’d been calling it in my head, but memoize() would be a much better name because it demonstrates that it’s meant to be used only for computations which are too expensive to perform on every render.
I’ll give you a real example. I have a full content component in my RSS reader (a standard feature) which fetches an article from the web, sanitizes it, and loads it into a sandboxed frame. I’ve been trying to build apps totally “reactively” (sticking with that terminology now), meaning changing any settings will immediately update the view.
The “article content” component has a number of dependencies:
The article content itself, which would change if the user switched articles
The theme (e.g. light/dark mode) - most theming is handled via CSS, but I have to re-render the article’s HTML because it is in a sandboxed frame
Redirect settings, which allow the user to add link redirects which should then update the links in the article
A setting to strip tracking tags from links for privacy, which also updates live
Reader mode on/off setting, which does a pass through a Readability algorithm to strip excess tags (like your browser’s reader mode)
Note that many of these dependencies are updated in different ways. The article to render is passed in via the parent’s template call. The settings come in over PubSub (dispatched through update/2). The reader mode comes in as an event (the toggle is in the component).
Note also that the rendering (sanitization + readability) is expensive. My readability is somewhat optimized but it’s still on the order of 30ms to compute. It’s not something I can do on every render, so instead any time I update any of the above I have to explicitly trigger a re-render of the content.
I agree with this completely, with one exception: it is acceptable for a component to communicate with itself and manage its own state by subscribing to the global state. In the limit, all state should be in the database and components should simply accept subsets of it.
Unfortunately, in practice we must violate that last part because existing databases are not good enough. But it’s something to aspire to.
The database is often not the only source of data or events. There’s UI state, which is transient, and certain things like if a drop-down is open or closed does not need to be saved there, or if a model is shown or hidden, or a current value of input before “save” is clicked, there are events for clicks, for tasks finishing their jobs etc. etc.
Of course I agree with you in spirit, but the point I was making is that the inability of existing databases to handle the things you listed is a failure of those databases to live up to their full potential. Now by the time you add such functionality maybe you would no longer call that “thing” a database - I would, but that is up for debate.
This is not true, persisting UI state often improves UX considerably. It is quite funny that you chose this as an example because literally a few minutes ago I was fixing some choppy animations for my sidebar which does exactly that: persists the expanded/collapsed state of a set of folders. It is nice to come back to things exactly as you left them - it allows the user to develop habits that mesh with the technology. For example, I might leave feeds in a folder I don’t use collapsed, but my favorite feeds will always be open at the top because I want to access them quickly. If the folders were always open or closed when I opened my app I would be very annoyed.
Again funny, because as I compose this reply each keystroke is being persisted to the forum’s database as a draft!
Obviously there are some things which really should be ephemeral. I would not persist whether the right click menu is open/closed, or the mouse position, for example. But a lot of UI actually should be persisted, and unfortunately much of that has been lost in the transition to webapps.
Here you are correct: events should go into the database, and (a view of) state should come out.
To be honest, I wouldn’t call 30ms slow in the context of the web. Most people are using a 60 Hz monitor so 30ms is roughly 2 frames to update. You can try to set a network latency using the liveview debug tool and anything up to about 100ms is barely noticeable IMO.
There is a big difference between 30ms of latency and blocking the render for 30ms on every render on the server, which is what I’m talking about.
If you want do do expensive things in a “reactive”/“functional” context you need to memoize sometimes. That’s why React hasuseMemo. In Phoenix I’m currently doing the memoization manually, which is what I don’t like.
And for the record I think I actually moved that particular piece of code to a task with async assigns (gotta love the BEAM), but the points still stand.