I've contributed to all kinds of frontend setups; Elixir LiveView, React, Vue, Angular, HTMX and many more.
I've yet to find a frontend scripting solution that works well. My current stance is to have the frontend as thin as possible, no matter how it is written. Have your domain logic on the server output Plain-Old Data structures (JSON or similar) and render them with stateless, dumb views, be it a template, Vue, React or whatever.
A clear boundary between the domain logic and view is important. Rails templates that can basically access anything is a terrible idea.
I have similar kind of experience and made similar conclusions for myself.
In addition to the points you have made already, I would like to add that almost every larger project needs a lot of business logic on the backend side anyways. There is no possibility to have this logic in the frontend and there never will be (for security reasons at least). So given these circumstances, we usually already have quite a few people experienced with - and very efficient in - these backend technologies.
So why would I spend a lot of time re-creating all kinds of routing, models, properties, etc. in an SPA? We currently run an Ember SPA for our largest project and 1/3+ of the time spent on it is pure waste because it is simply additional work to facilitate the use of this SPA at all.
In every project I've worked on, the interactive and dynamic elements actually used that are written in JS end up being a lot fewer than originally anticipated. Unless you're building a product where the majority of the functionality relies on instant, realtime updates such as maybe a streaming site with chat, etc., building, maintaining and extending an SPA is a huge overhead that offers little benefit over "dumb", static views with small, isolated interactive components.
> Unless you're building a product where the majority of the functionality relies on
This is a huge problem in the ecosystem. People want a landing page, so they reach for some full-stack SPA backend+frontend framework that is hip today, which is absolutely bananas.
Sure, if you're building web app with lots of dynamic and interactive elements, go crazy, because you'll probably need it, lord knows you'll get tangled up in state management no matter how clever you think you are.
But for websites that aren't trying to became the new Google Maps/Figma/Whatever, simple and basic templating patterns won't just be faster to develop, but easier to maintain and a better experience for your visitors to use.
My experience exactly. Why one would do that anyway? Not listening to experienced engineers and being blinded by modern FE hype. Thinking just because many other businesses do it, one has to do it too. Then decision made by non-technical or not too technical management people.
And sure enough, then came the self inflicted workload. "Oh we need to change the router.". A problem you do not have with any popular mature web framework on the backend. "We need to upgrade to NextJS version 149, for feature blablub." Which would come naturally with something like Django or other traditional web framework. "We need to upgrade NodeJS!" ... and so on and on.
While all of what the FE actually did could have been just render static templates and a few lines of script.
Companies choose their own downfall by following the FE hype trains. The result are the shitty web apps, that don't need to be "apps" in the first place, but could simply have been static pages. It is quite a comedy show and I would laugh, if it did not affect me negatively.
I also have a lot of experience with various setups and front-end technologies, and I couldn't agree more!
Furthermore, I'd say this: if you can't simply serialize your view-model and send it to the client because something might get exposed or because it's a complex object with embedded logic, your first priority should be fixing that. Only after achieving this should you even think about changing how you handle the front-end.
It's a really strong selling point that you can use a popular FE framework for all the templates, but at the same time almost completely avoid state management.
> but at the same time almost completely avoid state management
Unless you're just building simple landing pages (I think the context is "web apps" here) or something, you might not have removed any state management, you've just moved it elsewhere.
So the question is, where did you move it instead? The backend?
That is a good point, and I can see that I probably did not communicate what I meant clearly enough.
In my experience most of the state management in FE apps is about fetching and handling data from different types of APIs. With inertia you get a very simple abstraction in that each page gets its own little API. Then inertia takes care of feeding the data from that API to the rendered page when you navigate to it.
For that reason the page can be a "dumb" component, and there really isn't much state to manage.
If the app needs modals, dropdowns, forms e.g., you will need to manage the state of those in the browser, which I think is very reasonable.
Obviously there can also be situations where you'd want to have a small part of the page to fetch some data asynchronously, and for those you'd need to use something else - inertia doesn't do everything.
Out of curiosity, what difficulties have you faced with Elixir LiveView? I am developing in it right now, and am enjoying managing state and interactivity in one codebase (I like how everything is channeled through the socket).
If you handle all interactivity server side so that the user can’t do anything at all without a round trip, sure. It’s just a terrible app for users with latency/patchy connectivity. As soon as you try to mix in slightly nontrivial client local state and transforms it becomes the most retarded JS imaginable.
This is the common misconception of LiveView coming from the JS community. LiveView is JS. It has a whole optimistic UI layer that solves all of the problems you cite. The UI from state updates coming from the server or the client doesn't matter because the reactive UIs of SPAs still require data updates from some remote source. So if we're talking about latency that latency is going to exist for all application types.
Where did I say it’s not JS? I said it becomes the most retarded JS imaginable. I have used the “whole optimistic UI layer”, hooks and commands. What happens is local diffs have to fight server sent diffs, at every level, resulting in more tangled logic than jQuery apps. This does not happen when you merely have to merge state and the view is a pure function of state.
My primary complaint is that it is hard to test (compared to "stateless" pages) and that the whole thing feels complicated.
I also feel like the learning curve is pretty steep and you can't just have a random, experienced non-Elixir dev join the project and have them fix something. They must be comfortable with Elixir & Phoenix before they get productive. I feel like it is multiple layers of complexity and uncommon patterns.
Other than that, I think it is very cool. I have very strong mixed feelings about the feature hah.
It is very easy to have very important choices made inside a rails template, like permissions (can X access Y?) and it is similarly easy to ignore permission checks and just query anything you like.
This makes it also very easy to end up with N+1 issues that are not caught in tests (because the tests don't render views), "forgetting" to scope the data (rendering more records than the user should be able to access) or rendering sensitive attributes the user should not have access to.
Even with tests that do include rendering the views, it is very easy to have a test like:
it "renders my posts" do
post0 = create(:post, user: current_user, title: "Foo")
post1 = create(:post, user: current_user, title: "Bar")
response = get "/my-posts"
document = parse_html response.body
expect(document.css("tr td.title").map(&:text))
.to match(["Foo", "Bar"])
end
Now there's so much wrong with this test, but the primary issue is when the view does something like this:
<%= render "posts", posts: Post.all %>
The test implies the document should only render the current user's posts, but the view / template actually renders all posts. You don't want to test this kind of logic by inspecting HTML, because it is error-prone and expensive to test this way.
What if you want to change the HTML structure? You'll end up rewriting all these tests, and it is VERY EASY to make subtle mistakes when rewriting the tests, resulting in false positives in the future.
Instead, I think you should separate this logic to a model, not to be confused with "activerecord classes" - I'm thinking of a `UserPosts` model, like this:
module UserPosts
extends self
def get_user_posts(user, opts)
user.posts.with_opts_or_something(opts)
end
end
Then you also disable automatic loading of relations (forgot the config name) and, by convention (or enforced in some way), never refer to any model modules or classes in a view (or any method called by the view, like the helpers).
Your controller will likely look like this:
class UserPostsController < Whatever::Base
include UserPosts
def index
@posts = get_user_posts(current_user, page: params[:page])
end
end
By keeping the domain logic inside specialized modules per access scope (i'd have separate `Posts`, `PublicPosts`, `AdminPosts`, `ModeratorPosts` modules), you can test the logic in tests per module (including tests what the module SHOULD NOT access). Then, when writing the views and controllers, you don't need to re-test or re-remember to test the "should not" cases - the test cases you're easy to forget.
You can also design the modules in a way that they only load (select) the properties that context would be allowed to access. If you have sensitive data (like a user's real name or email), you don't even want to load that property when loading the users from a `PublicPosts` context, since public posts should only render the user's id and public nickname for example. You can also wrap the posts/users in presenters using whatever fancy gems you fancy, as long as it gets the job done.
In an ideal world, I'd lock down Ruby templates to only have access to previously loaded data via instance variables and helpers, nothing else.
It is a bit hard to explain, since I have limited time to address it properly, but I hope I get the point across.
I've yet to find a frontend scripting solution that works well. My current stance is to have the frontend as thin as possible, no matter how it is written. Have your domain logic on the server output Plain-Old Data structures (JSON or similar) and render them with stateless, dumb views, be it a template, Vue, React or whatever.
A clear boundary between the domain logic and view is important. Rails templates that can basically access anything is a terrible idea.