Thoughts on Remix 3
20 points by siddhartha_golu
20 points by siddhartha_golu
This is the first time I saw Remix 3 code and now I'm very confused. The code examples imply a different model to react entirely, and suggest a model that is much closer to solid.js. But for solid to work, it relies on some extra components such as For
or Show
as you can no longer really express collections or conditional stuff otherwise.
In general I recommend looking at solid FWIW. I'm a bit disappointed it did not get a lot of use compared to react, but the mental model of it is so much easier.
Yeah, Solid has an approach that looks a lot like React on the surface, but is way easier to reason about. I wrote about it here a few years ago: https://blog.startifact.com/posts/solidjs-fits-my-brain/
I'm not sure the comparison with Solid is accurate, based on the article Remix renders HTML chunks and uses a similar technique as HTMX to swap the innerHTML. SolidJS works on the DOM nodes directly (aside from an initial innerHTML, or so I've heard). That being said it's good to see some diversity in the approaches to modern web UIs, the React monoculture is kind of a problem for innovation.
You're right, I misunderstood what it does from that blog post. Remix appears to be some sort of hybrid between react and solid. It does create vDOM nodes like React, but it does not have any reactivity and requires manual updates which then re-creates the vDOM nodes for diffing.
It's a bit confusing, because i still don't know what update() does. From looking at this code I would have assumed that it's like solid, because how else is the counter maintained:
// State is just a closure
let count = 0;
return () => (
<div>
<div>{count}</div>
<button
class="p-2 text-green-500"
on={dom.click((event, signal) => {
count++;
this.update();
})}
>
Inc
</button>
</div>
);
}```
Note that the return value is a closure that can be rerun without having to recreate the entire component. So I suspect what update()
does is rerun that render function to get a VDOM tree, and then diff that tree against the real nodes.
To me, this looks like the old React class comments, where the main body of the function is the body of the class (defining state, methods, timers, etc), and the returned closure is the render()
method. this.update()
is just this.setState
but it doesn't do any setting of state, it just does the "please rerender this component now" part.
In that sense, this feels like an alternate history, where React still got rid of classes, but took a different approach instead of hooks. I kind of like that - I think one of React's biggest issues is how much it deviates from common JavaScript conventions, especially with hooks. But it's still looks like it's keeping React's approach to the DOM and this idea that you can patch the DOM to look like a particular target state.
Oh I missed that part of the syntax entirely. That makes a lot more sense.
In fairness I'd seen the closure syntax and been surprised by it, but it wasn't until your comment then that I realised how it all fitted together.
But I think "React class components, but as closures" seems to be the basic idea. Plus a general attempt to remove a bunch of the React-specific abstraction layers around the DOM. It's an interesting concept, but I'm not convinced it's enough to be worth investing much time into, especially when most other frameworks seem to be coalescing around signals and fine-grained reactivity.
That pendulum at the end is a bit much. I've never been able to see React as belonging to functional paradigm. Even when it used to be marketed as a view library, it felt to me at best a library that tries to use declarative syntax which I'd have to "declare" using somewhat functional, but heavily imperative innards. This declarative + imperative feel is reinforced with the concept of hooks.
Also, having tried to keep codebases stable across iterations of React Router, the team gives an aura of "Hey folks! This newest version contains the bestest solution we've (re)invented to the problems we've created! Rejoice, and forget about the previous versions!"
Yep, react is declarative except for event listeners in which case it’s imperative with an api to rerender the component. So basically, it’s only declarative on the surface, the second you incorporate side effects from user interactions it’s imperative.
I wonder where, or why, we completely gave up on separation of concerns when it comes to frontend. If we think of previous patterns like MVC there was a clear focus on that idea.
With all frontend frameworks that has completely gone away. Presentation logic is tightly coupled with state. Styling is tightly coupled with markup. Business and validation logic permeates throughout. Loading, routing, error states, all is in the same layer. With the increasing use of JS in front and backend even the database is now tightly coupled with rendering.
This isn't a "you're holding it wrong" situation. It's how it's intended to be used.
I'm not criticizing Remix 3 but it's just one more example. I'm really trying to understand how or why we ever came to a situation where loose coupling is seen as a bad thing. This is an example from the front page of Remix:
export default function Projects() {
const projects = useLoaderData();
const { state } = useNavigation();
const busy = state === "submitting";
return (
<div>
{projects.map((project) => (
<Link to={project.slug}>{project.title}</Link>
))}
<Form method="post">
<input name="title" />
<button type="submit" disabled={busy}>
{busy ? "Creating..." : "Create New Project"}
</button>
</Form>
</div>
);
}
Loading (from DB presumably), routing, rendering, linking, error states, styling would be here too...all in the same place. And what did we gain?
Actually, the concept of 'separation of concerns' is widely misunderstood. There is well-known research that suggests that software module boundaries should be set according to the parts which change together. See https://dl.acm.org/doi/10.1145/361598.361623
If we apply this concept to frontend UIs like the above example, we can see that the Projects
function is like a single module that takes care of a single concern–rendering the projects list and a form for creating a new project. Now, for any substantial change in this feature, we need to look in only a single module in a single file. This makes the codebase easier to maintain and update over time. Codebases that are easy to change are widely regarded as higher-quality.
We're not talking about a module here but a single 18-line function having 6 responsibilities.
For all large frontend projects I've worked on this is actually on the low-end of coupling, many components people write will also have some validation and payload formatting as responsibilities.
Code that changes together should live together, that's not my issue. I'm against taxonomical code organization. The issue is that there's no abstraction whatsoever happening in this rendering layer to help with loose coupling. That type of code is not just a quick example, it's very representative of code in the wild.
Empirically speaking it's not at all conducive to higher-quality codebases. If you look at successful large-scale frontend-first codebases they make deliberate choices to separate visual-only components from logic-based components from error states, etc. NextJS is a prime example of trying to add some organization to it.
Separation of concerns is just a natural consequence of keeping responsibilities to a minimum, which itself is a natural consequence of looking for loose coupling. To encourage that you need some form of abstraction. I just see nothing like that being researched in the frontend, which is a break from the past.
The hooks should, in theory, be the abstraction layers. These are loosely coupled from each other, and most of the complex logic of an application should live in testable custom hooks.
The component then acts as the "imperative shell" that links all of the abstracted parts together. It's tightly coupled to the hooks it's using, but only in the same way that the main()
function of a CLI is tightly coupled to all the functions it runs. If everything is working correctly, most components should be a handful of lines long, and there should be almost nothing worth testing inside them.
In practice, I think a lot of people struggle to build this sort of abstraction, and tend to bring more and more complex logic into their components without trying to abstract it out into hooks or other similar tools. But it's certainly possible to do this right, and the result is, I find, typically very easy to understand and modify.
I definitely agree with that (mostly), however:
If everything is working correctly, most components should be a handful of lines long, and there should be almost nothing worth testing inside them.
That's akin to "if everyone writes great code we should have no problems". There's a reason large projects struggle with this: it's a very leaky abstraction.
I can only speak for myself but the MVC model never really clicked for me. The vast majority of React components I write fit neatly into a single file with all the data dependencies and styling enclosed in that modular, mobile unit. My mental model of the software is clearer and there's less friction when I don't have to juggle with multiple files that often don't live in the same folder.
Just a note that MVC (originally user-model-view-controller) is about the users mental model and how the user-interface mediates between that, and the data model of the program.
It's well worth skimming the original paper - imnho it's still applicable and relevant today - just maybe not in the sense that most modern yak shaving outings about MVC vs MVVC or whatever tend to go.
It's much more interesting than that.
https://folk.universitetetioslo.no/trygver/themes/mvc/mvc-index.html
So,
Loading (from DB presumably),
That's the loader function, which is called from this function via useLoaderData
. You could argue that the data should be passed in as an argument, I guess.
routing,
Routing is not done in here, it's either done via where the file lives or in a configuration file that sets up routing.
rendering,
That's the primary job of a component, yeah.
linking,
Something that produces HTML is going to produce links, I'm not sure what the objection is here.
error states, styling would be here too...all in the same place.
Same with this. This is all part of "rendering".
routing, Routing is not done in here, it's either done via where the file lives or in a configuration file that sets up routing.
Yes it is, it's only omitted. If the form cannot be submitted it'll be the responsibility of this component to know what to show the user. It's just hidden behind "if the request is in X state, render this, otherwise that", often the result of a validation error.
A different implementation would POST, let the server return a 302 and the browser would redirect to that page. The calling code would never know or care and wouldn't be responsible with following the redirect. With forms that's perhaps a minor improvement SPAs give us, not losing form state, but that's an added responsibility.
rendering, That's the primary job of a component, yeah.
Yeah, one could argue it's the only job.
linking, Something that produces HTML is going to produce links, I'm not sure what the objection is here.
I'm fine with that. I probably edited and added rendering as a more generic term and forgot to remove linking.
Same with this. This is all part of "rendering".
No. Styling is a separate concern, hence CSS. That this has been conflated with HTML in components is an implementation choice.
Error states are also not rendering. The code has been omitted but if useLoaderData()
fails for some reason there'll be yet another conditional dealing with that, possibly inside the HTML itself. I remember when we used to say "don't write SQL queries and complex conditional logic in your templates", my question is why has that changed?
A different implementation would POST, let the server return a 302 and the browser would redirect to that page.
I mean, this is what is happening. This component will submit a POST, and the action runs on the server, and it'll return the 302. That the client is following the 302 instead of the browser is kind of inherent to the idea of client-side rendering, I didn't realize you were arguing about that in general, and not about remix-specific choices.
No. Styling is a separate concern, hence CSS. That this has been conflated with HTML in components is an implementation choice.
It doesn't have to be, it's just that many people have found "atomic css" or whatever way you want to call it to be more maintainable. As a former "semantic CSS only" nerd, I've come around to their perspective, but you can emit whatever HTML you want here, if you don't want to emit any styles, you don't have to.
Error states are also not rendering.
I have written tons of Rails templates that also would make error state rendering choices based on errors. I'm not sure what the alternative is, unless you mean entirely different templates for the errors on a page vs when there's no errors, or something? You want to render the errors. So that's intrinsically tied to rendering.
I remember when we used to say "don't write SQL queries and complex conditional logic in your templates", my question is why has that changed?
SQL queries are not being written in a template here, and as for complex conditional logic, there is one if in this template that is a single condition. That doesn't strike me as complex, personally.
Separation of concerns is a pretty standard objection you see on every thread about React going back to 2015, though usually the objection focuses on the allegedly pathological coupling between styling, HTML, and rendering logic. The standard answer there is that the objection confuses separation of technologies (HTML, CSS, and JS) with separation of concerns, and those are all actually the same concern and it was the separation of technologies that was pathological.
Your version of the objection is interesting (and new to me) because you are pointing to things that really are separate concerns: routing, rendering, data fetching. The answer is that they are separate in this example, and the code you're looking at is simply the high-level code that hooks them up together. Surely there must be such glue code somewhere in any system where the concerns are separated appropriately, otherwise nothing would do anything. This code expresses precisely the separation of concerns you are worried about.
The standard answer there is that the objection confuses separation of technologies (HTML, CSS, and JS) with separation of concerns, and those are all actually the same concern and it was the separation of technologies that was pathological.
I'd buy that argument if the results of coupling were demonstrably better. I don't believe anyone can say, with seriousness: that the end-result (for users) of frontend code today is better; that writing frontend code today is easier; that writing non-toy web applications with this intentional coupling is easier. I won't argue that everything is worse, just that it's at best the same result.
This is something I've kind of wished for my whole career: I've always wanted someone to show me at least one example where extreme loose coupling was actually the thing that killed software quality in a codebase.
If you pick the right set of tools and use them the way they want to be used, I actually do think writing high quality complex production web apps is much, much easier today than it was when I started my career in 2012. I think the best apps today are better and more sophisticated than the best apps then. It's easier to write integration tests, types make it easier to avoid entire classes of problems and force you to write code in a way that avoids the event spaghetti disasters of the past, frameworks like React Router tell you exactly how to structure your app, and bundlers like Vite do what most projects need out of the box. All of these tools are the product of a long process of refinement of the question of what webapps need to do and what they don't need to do — the complexity and unreliability of tools like webpack 10 years ago reflected IMO a lack of consensus and a lack of the right abstractions for customization.
I don't think the fact that most people are still out there doing it wrong undermines the above claims. Most people are going to be bad at most things.
The author seems to categorize Remix 3 as more imperative than React because Remix encourages mutating captured variables where React would use setState() callbacks. But aesthetically I'd consider them to be the same. To the developer, both operations are obviously mutating some internal value. In fact, I wonder if the explicit update() function will encourage developers to reinvent the useState hook?
Though I agree with the broader point that the Remix mental model seems simpler, at least from these simplistic examples.
I haven't really followed Remix, but their Remix 3 livestream caught my eye when they mentioned encapsulating events and state related to them to produce higher order events you can drop in to the on
handler (3h42min39s in https://www.youtube.com/live/xt_iEOn2a6Y?t=13359).
It reminded me of the functional reactive programming phase 10 years ago when RxJS and Bacon.js were making waves; being able to use functional operators on event streams and compose them. I really think the FRP way of thinking felt natural for many browser-based things.
I wonder if this is a "hooks" moment for Remix. If not, I'm at least glad to see the higher order events direction being explored more.
The good:
The bad:
The ugly:
Eh, I wish React had a SSR framework.
Remix 2 was almost there with its focus on "sticking to the platform" while keeping the best of React intact. It had a few rough edges that needed sanding, but it was okay. Unfortunately, now the Remix team proves that they aren't to be trusted with producing stable code.
Earlier, Next 12 was about the peak of how React SSR worked. The pages router was beautifully simple, injecting props from the server was very intuitive and if it weren't for the horrifyingly slow compilation times in development it would actually be quite pleasant to use. Unfortunately, the Vercel team proved that they aren't to be trusted with producing stable code.
Back to Express and client side React for me, I guess.