For the best experience on desktop, install the Chrome extension to track your reading on news.ycombinator.com
Hacker Newsnew | past | comments | ask | show | jobs | submit | history | more dashersw's commentsregister

Thank you for the thorough comments! I wholeheartedly agree. And while I believe React invented most of its problems (mostly because we've been engineering GUI solutions since mid-70's and, as I always say, Excel, the god of all UI apps, shipped in 1985) I also acknowledge modern problems like suspenses that occur as a result of elevated expectations.

Gea is frankly very new, and for example doesn't ship a solution for suspenses or fine-grained error recovery yet. And since noone, including me, built a very complex Gea app yet, we don't exactly know if the simplicity will hold up.

But Gea is the 3rd generation of my frontend frameworks, and I've been building vanilla JS-esque frontends since 2011 (when I released my first library, tartJS). The main feature of a good framework is to contain code complexity as the app grows and I believe as GUI engineers we have some good patterns to flatten the complexity. Gea is just trying to hide repetitive DOM updates behind a compiler, and while it has proven somewhat difficult to account for all the different ways people (or AI) write JavaScript, I'm constantly improving the compiler with new patterns.

That's why Gea ships with several GUI app implementations—my approach is kind of simple. If I can get AI to generate several different UI apps in different domains, I can capture a good enough set of complexities for which I can deliver solutions. I've already fixed tens of bugs that I could only see as a result of building a UI with the library.

Having said that, it's still very early for Gea. I guess only time will tell if we will have to resort to different, non-idiomatic solutions to handle more complex demands. At least the philosophy is very clear—Google built its original web 2.0 apps with Google Closure Library, an imperative UI framework, with lots of repetitive boilerplate. And that was enough to give us maps and google docs, etc., so I am hopeful for the future that we will be able to find idiomatic solutions.


Would love to se tagged template Literals and w eb components as first citizen in this - lit.dev etc.


In fact the precedent to Gea, my previous framework erste made use of `pug` tagged template literals! I started with that base, but then decided to ship with JSX instead. This would be a great addition to Gea and would love to see it as a community contribution. I'm hesitant about web components but if we could help web components to feel more "plain old JS" to write, it would be awesome!


This is one reason Gea has lots of examples in the repository. It's an area of active development—I'm working on adding compiler support for more and more patterns. Basically, since we can analyze the code statically, and follow the dependencies even if they are destructured, we can create proxies or special handlers for each situation. It's a tiring job and unfortunately there's no one global, easy solution, especially as you pointed out, some of these patterns don't work in proxies. But I believe we cover an important base of idiomatic JavaScript right now, and I'm continuing with new releases to improve the compiler's handling on more exotic ways to write JavaScript.

Of course, one down-side of the compiler approach is, for example, if there's a statement you want to make reactive whose signature is only resolved in runtime (like a computed property name) it's practically impossible to wire. But Gea exposes enough of the underlying component structure so it's kind of straightforward for a developer to manually write an observer for these cases, and do the updates to static properties (that are rendered) in runtime.

Hope this clarifies the approach.


React has its own idioms like hooks, and virtual DOM operations. A developer well-versed in JavaScript and who never saw React before wouldn't know how `useState`, or a `Context` would work. They wouldn't know that components would re-evaluate and re-render all the time. They also wouldn't know they shouldn't put useState inside a conditional statement. They wouldn't know they would need a useEffect, or that it depends on an array to declare its dependencies.

Vanilla JS, on the other hand, requires a good knowledge of DOM APIs.

Gea tries to be as close to plain old JavaScript as possible, the way we write it on the backend. The only necessary notion is that everything is reactive and DOM will update automatically as component/store members change.


I personally don't believe anything can beat hand-built and fine-tuned vanilla JS code, and in the benchmarks we see Gea beating vanilla JS implementation (for example in partial update or swap rows). One reason for this is Gea's compiler has special cases like row swap's to be as minimal calculations and DOM operations as possible. The compiler, also, is evolving to recognize more and more patterns, and compile them into a miniscule overhead.

One thing I borrowed from my earlier library erste is event delegation. Instead of creating event handlers bound to each DOM element (say, in a list render) which is memory-heavy and also consumes a lot of CPU cycles, Gea simply attaches one event listener per type on the body and uses a `.matches()` call to check whether that event applies to a given DOM element. This is one of the main reasons why Gea is so performant—there's no excess/unnecessary memory allocation or CPU cycles. This is also reflected in the benchmark results.


Thank you for your detailed answer!


GJ one your library

One random recommendation I could give based on my experimentations on both firefox and chromium

you can attach symbol based custom properties to each such dom node 'class'

and apart from using .matches(), symbol attribute check. this outperforms class based equality checks by a small margin.

very marginal but hey, you are clearly tryharding.


Thank you! Very interesting insight—I'll have a look.


Thank you! Honestly I tried building a replica of shadcn but that didn't prove to be viable. I thought it would be very difficult for developers to adopt without a first-party UI library so built one on top of Zag.js. And I expect a lot of AI-assisted coding sessions to happen with Gea so I also baked in enough AI skills to enable assistants, as well as migration guides for React. Hopefully these will help onboarding early adopters. Please let us know when you give Gea a try!


Thank you! It's the author here. I thought hard about this, and one angle Gea is bringing is that the old is new again, that's why I harkened to a retro style. I also just plain love synthwave, so... :) but you are right, and as Gea matures I believe we will iterate on the homepage.


Bro, is there anything you haven't thought hard about? I read throughout the thread and the answers sound really LLM-y. Although these days we might be calling wolf about gusts of wind.

Great framework tho. Awesome job.


Heh, sorry it comes across as LLM-y, honest and human-written answers here only. I'm tired of AI slop as much as the next guy, but, yes, I _really_ took my time with Gea. I've been working on writing JS frameworks for 15 years, this is actually the 3rd generation of similar ideas, following tartJS (2011) and erste & regie (2017-2019). It took me several years to solidify what I expect from this generation, and I've been working on Gea for the past six months. That's why I thought about and evaluated many aspects raised here.

And thank you!


The bindings in Gea work just like in JavaScript. Two-way if an object is passed, one-way if a primitive is passed. I think it's best to stick to the idioms of the underlying language.


I don’t know if I’d call it an idiom — rather I’d argue that in modern JavaScript, mutating passed objects is often an anti-pattern.


I personally agree. But the default expectation (and therefore the design) should follow the practices of the language. If JS allows mutations on the objects passed to a function to be reflected on the parent, I believe frameworks should follow this paradigm.

And in the end in Gea developers have full control over this, just in the same way they do in real life. `child({ ...obj })` easily solves this, for example, in both idiomatic JS and in Gea.


You're using various terms to refer to concepts which are similar but distinct, and it's confusing the issue a bit.

> But the default expectation (and therefore the design) should follow the practices of the language

Languages do not have practices, developers do.

Regarding "idiom": core language features/semantics are not idioms. In programming, "idiom" usually refers to small commonly-used patterns that reside atop the language. "Mutating objects" is not an idiom, if only because I can think of any number of non-idiomatic uses of mutation.

> If JS allows mutations on the objects passed to a function to be reflected on the parent

JS "allows" mutations on objects to be "reflected" elsewhere, because that's how mutation works. If JS had to support scoped mutability at the language level, the language would be significantly more complex.

But this implies nothing about the value or advisability of using mutation and two-way binding in an application framework. That is a choice on which framework authors usually land on one side or the other.

It seems that by more or less equating "idiom", "practice" and "paradigm", you're opting out of the sorts of choices that not only distinguish web frameworks, but simplify the patterns involved in building with them.


> `child({ ...obj })` easily solves this, for example

Spreading doesn't prevent you from mutating nested fields. The fact that you think this is an easy problem puts all your other choices under question.


Feel free to question anything you like and I'll help you find the answers.

This is a well-trodden path. All aspects of object mutation and its effects are obvious and well-known. What is pass by ref and what is pass by val is also pretty obvious. One can easily pass in primitive values and not worry about two-way binding if they choose to. One can also easily not mutate any props they receive from their parents. This is already the best practice in eslint for like 10 years. This is not easy, this is trivial.

I'd rather see some real concerns.


> But the default expectation (and therefore the design) should follow the practices of the language. If JS allows mutations on the objects passed to a function to be reflected on the parent, I believe frameworks should follow this paradigm.

Why? Why should frameworks be beholden to the mutation semantics of the language, particularly with JS where there is no choice of language in the browser? Why should frameworks follow this paradigm?


Because JavaScript _is_ the language and people know it. I never understood the concept of a "React developer", for example, although I saw many junior devs who were very well-versed in React and didn't completely understand JavaScript.

In the end, it's a design choice. Of course frameworks don't inherently _need_ to be beholden to the standards of the underlying language, but I think this is just simpler, therefore a worthy goal to pursue.


Stores and Components are basic classes that don't introduce any new concepts (other than the fact that the JSX goes into the template method of the Component, and that they are reactive behind the scenes). There are no hooks like useState, and the design philosophy is that everything should feel as native and natural as JavaScript.


you continue to contradict yourself by introducing concepts and saying they are not concepts.

I get what you're trying to say, that React hooks have special semantics, and that your abstraction feels more "native".

again, not sure how this is more "native" than Solid signals, just as an example.


You are bringing up an important topic. The way I see it is that Gea's Store is a plain old JS class. It's just a native class. There really is no special syntax you need to pay attention to. Whereas Solid signals require you to follow a specific syntax and approach, and has its own gotchas. Like, the language doesn't have a createSignal method by default, and you don't "execute" what look like values in JS as you need to do in Solid, and although I'm looking forward to the official Signal API, Solid isn't following that either.

That's basically how Gea is more native, because stores are plain classes. I hope this clarifies my point a little bit more.


    It's just a native class. There really is no special syntax you need to pay attention to.
Don't confuse syntax with code. Solid has no special syntax (other than JSX of course).

This isn't comparing apples to apples.

Solid has a Store primitive too, and it's a "plain old" proxied object.

How is `createStore` less native than `new Store()`? The `new` keyword didn't even exist in JS until 2015, and the underlying semantics (prototypical inheritance) never changed.

One of Solid's design goals is fine-grained reactivity for very high performance. That's why signals are getter functions, because calling them alerts the system to the precise "scope" that will need to be re-run when the value changes.

Since signals are functions, they can be composed easily, and passed around as values.


I'm not following—the `new` keyword has been with us since JavaScript's inception. You might be confusing it with the `class` syntax, but before that we could always `new` functions.

And yes, Solid has signals that require you to know how to write and work with them. I answered another comment on the thread about Solid stores—they also introduce a couple of gotchas and weird syntax. You just can't do `this.users.push(...)` or `this.users[2].loggedIn = true` in Solid stores.

Therefore `createStore` is less native than `new Store()`, because `new Store()` just gives you a plain (proxied) object you can manipulate in various ways and reactivity will persist thanks to the compiler.

And Gea's design goal is also fine-grained reactivity, which it delivers without getter functions in the code that the developer writes, but rather, the handlers are generated via the compiler.


What is the difference between mobx or solid stores or any of the reactive frameworks that do reactivity on proxy objects?


Solid stores are a great improvement over raw signals, but they still come with their own gotchas. First off, it's an entirely new syntax. You need to learn its documentation. You always have to use setStore, and it has a weird syntax like `setStore("users", 2, "loggedIn", false)` and even pushing items to an array is weird. In Gea it's just regular JavaScript: `this.users[2].loggedIn = false` or `this.users.push(...)`. MobX also comes with its own syntax.

In the end Gea is basically as simple as a plain old JavaScript class, made reactive by its compiler.


> You always have to use setStore

This is a design choice, and explained in their docs:

    Separating the read and write capabilities of a store provides a valuable
    debugging advantage.

    This separation facilitates the tracking and control of the components that
    are accessing or changing the values.

> You need to learn its documentation. You always have to use setStore, and it has a weird syntax like `setStore("users", 2, "loggedIn", false)` and even pushing items to an array is weird.

It's optional, and incidentally quite expressive and powerful. But they also support mutable drafts out of the box.

    import { produce } from 'solid';

    setStore(produce(state => {
      state.users[2]?.loggedIn = false; 
    }))


I understand this bit. The bit that I don't understand is how you compare the two invented concepts like `setStore` and `produce` to just `state.users[2]?.loggedIn = false`. To me it's very clear Gea's syntax requires you to write less code, while also requiring you to know less concepts.


The value judgement implied in "invented concepts" is kind of weird, and maybe gets at a core difference in how you and I think about this.

Frameworks have APIs; they define concepts. Learning concepts isn't a bad thing in and of itself. Especially if they are concepts which let you model your application more succinctly and efficiently.

What you mean is that you are leaving it to the user to learn (or conceive of) additional concepts which are external to Gea to in order to build non-trivial reactive applications.

But "Gea requires you to write less code / know fewer concepts" can be reframed as "Gea opts out of solving some types of vanilla JS boilerplate for you". When you don't give your users "concepts", they're still going to end up writing a lot of code and learning concepts, just not within your API.


I see mpalmer counters each specific claim, dashersw shifts to a slightly different argument rather than directly addressing the rebuttal.

One guy is doing the tech founder equivalent of a TED Talk ("my thing is more native and requires fewer concepts!") while another is quietly pointing out that the emperor has no clothes, and has receipts. One keeps doubling down because this is clearly his baby, while other is just some experienced dev who's watched too many "simple mutable state" frameworks turn into maintenance nightmares. One person is selling a vision. The other is explaining why that vision has been tried and mostly rejected by the industry for good reasons.

Only one of them is learning from the conversation.


This content is generally unwelcome and appears to be generated, which is against HN commenting rules.


Yeah, sure :)


Thank you for the discussion, I find it very interesting and I'd love to understand how you think. Why do you think setStore and produce let you model your application more succinctly and efficiently than just a direct assignment?

And what kind of types of boilerplate do you see Gea is opting out of?


Let's briefly set aside your belief that because JS supports mutation, a framework should as well.

Immutability and one-way dataflow is an unquestionable productivity win. It eliminates an entire class of complexity, and results in well-defined boundaries for the components of your application. With two-way data binding, those boundaries have to be carefully recognized and preserved by the developer every time they touch the code.

So one place Gea won't save devs any time or grief is in testing. If any part of the app can affect any other part of the app, the surface area of a change very quickly becomes unknowable, and you are only as informed as your tests are thorough. Not boilerplate in the literal sense, but quite a bit of overhead in the form of combinatorial test cases.

Yes, JS has mutability. Yes, you can make two-way data binding work as a framework feature. That you should is an argument I don't think you've successfully made yet.

Let me ask - why do you think JSX lets you model your application more succinctly and efficiently than just a direct createElement call?


I see your point. I designed Gea to be one-way binding only first, and then decided to add two-way binding for props passed as objects. People can still easily only use one-way binding. Maybe this becomes a preference in the compiler config?

The argument for Gea to support two-way binding is basically circular and I believe well-made at this point. I want a framework to respect a language. Breaking two-way binding when it's a concept in the underlying language is like breaking Liskov's Substitution Principle. You can do it, but you probably shouldn't.

JSX is more succinct and efficient than raw DOM API because it's declarative, where the raw API is imperative.


> Maybe this becomes a preference in the compiler config?

Maybe, but it could be more complicated for you, the maintainer, than it's worth!

> JSX is more succinct and efficient than raw DOM API because it's declarative, where the raw API is imperative.

But that's also the difference between (e.g.) Solid's signals vs a plain (proxied) object that's passed around and mutated. I'd go so far as to say that mutable objects are one of the most "imperative" things about JS.


I don't want to recurse into philosophy but one could argue an assignment is more declarative than a function call :) Solid is function calls everywhere, and extra code, vs plain objects.


Plain objects whose accessors contain nontrivial logic deferred using function calls - to be fair


I am pondering about this, but would love to see an example to make it more concrete. The way I see it is that this reactivity is completely on the compiler's side, and there's no more ambiguity or pitfalls than misrepresenting a dependency array in a react hook.


Should an application framework written in Rust encourage the usage of `unsafe` blocks in application code? It certainly allows for more power and flexibility, and it's supported in the language.


Hi, the author here. Gea is built upon erste and regie that I released in 2017 and 2019. Hence the copyright. I thought a lot about this, also thought of releasing it as a new version of erste, but it would be too big of a change and re-imagination, and I didn't want to introduce the dichotomy and stress introduced by Angular 2.


Hi, the author of Gea here. Gea is built upon the principles introduced by 11 years of history, based on all of the UI frameworks I have authored so far, the most recent of which are erste and regie from 2017-2019. And Gea itself is written over 6 months, the history of which is compressed in the initial commit as I don't believe in populating Github history with crappy commits that alter design decisions and introduce breaking changes. You can imagine Gea was closed source before the 1.0.0 launch, and made only open at the end.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search:

HN For You