Readit News logoReadit News
Posted by u/vanviegen 4 months ago
Show HN: Aberdeen – An elegant approach to reactive UIsaberdeenjs.org/...
Yes, another reactive UI framework for JavaScript. Bear with me, please... :-)

I 'invented' the concept for this back in 2011, and it was used (as a proprietary lib) in various startups. Even though many similar open source libs have been released since, and boy have I tried a lot of them, none have been able to capture the elegance and DX of what we had back then. I might be biased though. :-)

So I started creating a cleaned-up, modern, TypeScript, open source implementation for the concept about five years ago. After many iterations, working on the project on and off, I'm finally happy with its API and the developer experience it offers. I'm calling it 1.0!

The concept: It uses many small, anonymous functions for emitting DOM elements, and automatically reruns them when their underlying proxied data changes. This proxied data can be anything from simple values to complex, typed, and deeply nested data structures.

As I'm currently free to spend my time on labors of love like this, I'm planning to expand the ecosystem around this to include synchronizing data with a remote server/database, and to make CRUD apps very rapid and perhaps even pleasurable to implement.

I've celebrated 1.0 by creating a tutorial with editable interactive examples! https://aberdeenjs.org/Tutorial/

I would love to hear your feedback. The first few people to actually give Aberdeen a shot can expect fanatical support from me! :-)

xiphias2 · 4 months ago
Congrats for reaching 1.0! Nice little library, but as it's signals based, it would be nice to make it compatible with the signals proposal (https://github.com/tc39/proposal-signals)

At the same time for me, while it's super nice, in my opinion it just doesn't differentiate enough from other signals based frameworks to get mass adopted / make CRUD apps that much easier to make.

The problem with remote server/database is ,,what data to sync and when'' by the way, it's very different problem from what your framework is solving.

I loved Svelte until I started using SvelteKit and realized how hard the data synchronization part is.

vanviegen · 4 months ago
> Nice little library, but as it's signals based, it would be nice to make it compatible with the signals proposal (https://github.com/tc39/proposal-signals)

Based on the current proposals, it seems that a signal can only contain a single atomic value, for which changes can be tracked. Aberdeen's `proxy` can efficiently wrap complex data structures (objects within arrays within objects, etc), tracking changes on the level of primitive values (integers, strings).

For that reason, I wouldn't really call Aberdeen signals based.

Yeah, "what data to sync and when" describes the problem quite nicely! And indeed, it's entirely different from this library, except that I have a partial solution in mind that may fit Aberdeen rather well... We'll see. :-)

MrJohz · 4 months ago
Aberdeen's proxy is essentially an object of nested signals. In general:

    proxy({
      name: "Foo",
      email: "bar",
    });
is semantically equivalent to

    new Signal({
      name: new Signal("Foo"),
      email: new Signal("bar"),
    });
The syntax for getting a value will look different in each case, but using Javascript proxies you can fairly easily wrap the latter in such a way that it looks like the former. That's what happens with SolidJS's createStore utility, and I assume it's what's happening here as well.

Aberdeen looks like it's taking Vue's approach of making the proxy/automatically nested case the default, which is typically more convenient as nested reactivity is the thing that makes signals really sing. But it's still just signals, and the same behaviour can always be replicated using manually nested atomic signals.

EDIT: I've just realised you're OP, so you probably know more about your reactivity system than I do! But at least the way you're describing it, it sounds like it's working the same way as signals do, just with an emphasis on the nested signal case by default.

HWR_14 · 4 months ago
> The problem with remote server/database is ,,what data to sync and when'' by the way, it's very different problem from what your framework is solving.

I also feel that the data synchronization (and conflict handling) is where there are a lot of opinions and patterns but few drop in libraries. Although I'm posting this in no small hope that I get corrected with someone pointing out something I should be using.

austin-cheney · 4 months ago
I just read the signals proposal and was not impressed. There is a lot group thought in JavaScript, in Java too but more in JavaScript, around standardizing convenience based upon knowingly bad decisions from convenience abstractions.

Managing and updating the DOM is stupid simple and that simplicity has nothing to do with state, which a fully separate yet equally simplistic concern. That is something UI frameworks most commonly fail at horribly with a mountain of highly complex state bullshit that is forced on everything. But because framework people cannot architect original applications at any level these failures become indefensible standards enshrined by the most insecure among us.

llbbdd · 4 months ago
examples?
boredtofears · 4 months ago
> Nice little library, but as it's signals based

How do you reach that conclusion? There is nothing mentioning signals in the docs anywhere.

MrJohz · 4 months ago
The proxy object is a fairly classic way of doing signals (see Vue, SolidJS), and the whole thing about functions rerunning whenever data that was accessed inside that function changes is pretty much the definition of signals.

It looks like the author is trying to avoid making direct comparisons with signals and existing signal frameworks, which makes sense - depending on your target audience, it can be a bit "inside baseball" - but it's definitely using some sort of signals-like mechanism under the hood.

Etheryte · 4 months ago
Conceptually, this sounds incredibly similar to what Vue was in the 0.x stage. Are you familiar with Vue perhaps? What would you say are the core conceptual differences? At a glance, both aim for the same idea, that your data is proxied, you detect what changes actually matter, and you try and wrap all of that up in a neat little package. These days of course Vue is a larger beast, but I feel like the origins were very similar to what you have here. Interested to hear what you think about this comparison.
lucasacosta_ · 4 months ago
At first glance and on a syntax level, Vue from the start had part of its code in html syntax and the rest on JS. Aberdeen goes fully into JS.

So if I get it right, in Aberdeen there would not be any pure html written at all, right? Is that the "ideal"? Or it would be more of a hybrid with Aberdeen accompanying plain html?

vanviegen · 4 months ago
Correct!

As far as I know, Vue has always had its own HTML-based template engine, with special HTML attributes for conditions, loops, etc. Different trade-off.

Since Vue 3, it does indeed rely on `Proxy` for its reactivity, like Aberdeen.

The idea is the write whole applications without HTML. We've done some pretty big projects in this style, and in terms of DX and velocity it's actually really good. Recycling (tiny) components becomes really easy, as it's all just JavaScript functions.

tacitusarc · 4 months ago
Congrats! Building something like this is not trivial. You should be proud.

I think it would be useful to have an example of how to make “widgets”, larger components with more complex behaviors that could be reused across many applications.

I recommend making the HTML boxes scrollable, and allowing the code to scroll x. Reading wrapped code on a small screen is difficult.

vanviegen · 4 months ago
> I think it would be useful to have an example of how to make “widgets”, larger components with more complex behaviors that could be reused across many applications

Yeah, that's probably a good idea. Aberdeen doesn't prescribe any 'standard' for how components should be implemented; they're just functions that draw stuff after all. But figuring out some best-practices may not hurt!

> I recommend making the HTML boxes scrollable, and allowing the code to scroll x. Reading wrapped code on a small screen is difficult.

You'd think that in 2025, I'd remember to test my pages on a mobile phone, right? Doh! :-)

catlifeonmars · 4 months ago
Why not JSX? There’s no real cost to making the API JSX compatible, from what I can tell, and tsc has builtin support for transpiling JSX. It would also make porting code a lot easier. I’m only saying this because the type signature of $ is so similar to createElement.

As an aside, I really like the class name and text content ergonomics (e.g div.someclass, span:some content). Reminiscent of pug/jade

vanviegen · 4 months ago
I don't particularly like how control logic needs to be embedded within JSX using ?ternary : operators and .map(() => stuff) within the HTML.

Also, in order to transform JSX into individual rerunnable functions, we'd need a whole different transpiler. I like being able to code browser-runnable JavaScript directly.

To each their own. :-)

recursive · 4 months ago
> I don't particularly like how control logic needs to be embedded within JSX using ?ternary : operators and .map(() => stuff) within the HTML.

It doesn't.

In my[1] framework, there is JSX, but control flow like map is done with a function.

    <ul>
      { ForEach(model, item => <li>Fruit: { item }</li>) }
    </ul>
There is a lambda there, yes, but at the top level it's a ForEach() function call.

Likewise, it is possible to use get conditional elements in JSX without using react's ugly approach.

[1] https://mutraction.dev/

CooCooCaCha · 4 months ago
You don't have to do that in the html if you don't want to. You can easily assign those operations to a variable then insert the variable into the html.
gavinray · 4 months ago
You can just write an IIFE expression, and use regular imperative logic like if/else and switch statements.

If you find the syntax ugly, you can create a function like "run(expr)" to wrap it, similar to Kotlin's method of the same thing.

    <div>
    {(() => {
        switch (status) {
            case Status.LOADING: return <div className="loading">Loading...</div>
            case Status.ERROR: return <div className="error">Error: {error.message}</div>
            case Status.SUCCESS: return <MyComponent data={data} />
        }
    })()}
    </div>

WorldMaker · 4 months ago
> Also, in order to transform JSX into individual rerunnable functions, we'd need a whole different transpiler.

I don't think you would. `<Component prop="example" />` gets converted by current transpilers into `jsx(Component, { prop: "example" })`. The `Component` itself is passed as is. In the case of Components that are just functions, that passes the function as-is, as a function you can just call as needed.

JSX was built for "rerunnable functions". It's a lot of how React works under the hood.

i_dont_know_any · 4 months ago
re: syntax, I agree, it's stopped me from ever trying React/JSX-based frameworks, which I am sure is an over-reaction.

I have a POC syntax extension (babel parser fork) I named JSXG where I introduced "generator elements" which treats the body of the element as a JS generator function that yields JSX elements.

The simple/naive implementation of just running the generator was okay, but I (perhaps prematurely) worried that it would be not ideal to have the resulting list of child elements be actually dynamic-- as opposed to being fixed size but have false/null in place of "empty" slots and also using arrays for lists made by loops.

So, I also had a transform that followed conditional branching and loops etc. and made a template of "slots" and that resulted in a stable count of children, and that improved things a whole lot.

It's been a while since I revisited that, I should try and find it!

Comparisons below.

Aberdeen:

    $('div', () => {
      if (user.loggedIn) {
        $('button.outline:Logout', {
          click: () => user.loggedIn = false
        });
      } else {
        $('button:Login', {
          click: () => user.loggedIn = true
        });
      }
    });

    $('div.row.wide', {$marginTop: '1em'}, () => {
        $('div.box:By key', () => {
            onEach(pairs, (value, key) => {
                $(`li:${key}: ${value}`)
            });
        })
        $('div.box:By desc value', () => {
            onEach(pairs, (value, key) => {
                $(`li:${key}: ${value}`)
            }, value => invertString(value));
        })
    })
JSX:

    <div>
      {user.loggedIn ? (
        <button
          className="outline"
          onClick={() => user.loggedIn = false}
        >
          Logout
        </button>
      ) : (
        <button onClick={() => user.loggedIn = true}>
          Login
        </button>
      )}
    </div>

    <div
      className="row wide"
      style={{ marginTop: '1em' }}
    >
      <div className="box">
        By key
        <ul>
          {Object.entries(pairs).map(([key, value]) => (
            <li key={key}>
              {key}: {value}
            </li>
          ))}
        </ul>
      </div>

      <div className="box">
        By desc value
        <ul>
          {Object.entries(pairs).map(([key, value]) => (
            <li key={key}>
              {key}: {invertString(value)}
            </li>
          ))}
        </ul>
      </div>
    </div>
JSXG:

    <*div>
      if (user.loggedIn) {
        yield <button
                className="outline"
                onClick={() => user.loggedIn = false}
              >
                Logout
              </button>
      } else {
        yield <button onClick={() => user.loggedIn = true}>
                Login
              </button>
      }
    </*div>

    <div
      className="row wide"
      style={{ marginTop: '1em' }}
    >
      <div className="box">
        By key
        <*ul>
          for (const [key, value] of Object.entries(pairs)) {
            yield <li key={key}>
                    {key}: {value}
                  </li>
          }
        </*ul>
      </div>

      <div className="box">
        By desc value
        <*ul>
          for (const [key, value] of Object.entries(pairs)) {
            yield <li key={key}>
                    {key}: {invertString(value)}
                  </li>
          }
        </*ul>
      </div>
    </div>
Edit:

Come to think of it, I think it may have been <div*>...</div*> or even (:O gasp) <div*>...</div>.

jitl · 4 months ago
JSX is a better data structure than strings for expressing views, but this doesn’t use strings or any other data structure.

The beauty of the OP’s approach is the immediate mode render approach. JSX is a data structure, immediate mode implies a structure from the order of execution of computation.

You need JSX to pass around fragments of a view, unless your view is made entirely of computation, in which case you can just pass around functions.

WorldMaker · 4 months ago
JSX does not have any inherent data structure, it immediately converts to function calls. React is well known for taking the JSX calls and converting them to a reusable data structure, but that's React's thing and React's data structure is somewhat unique to React, other virtual DOM's have different data structures.

You can do "immediate mode" JSX. There's nothing technical stopping you, and there are at least a few libraries out there that do.

90s_dev · 4 months ago
The signature looked compatible with JSX to me. You could probably easily create JSX functions that just use $ internally, and make your web build tool's react-auto-import setting point to it.
phartenfeller · 4 months ago
Not for me as I like looking at actual HTML but definetly intersting. Good job!

I also like Svelte which uses it's own language and needs transpilation. I think that's key to elegance as JS was not really designed to control layout, style and logic all at once.

vanviegen · 4 months ago
It takes some getting used to, but the advantage of using JavaScript for layout, is that it mixes well with your control logic. JSX makes loops and ifs hard to look at. Svelte/Vue/etc invent templating languages with their own control logic - while we're already running within a full-fledged language.

Originally, we did this style of programming in CoffeeScript. Without all of the braces, it looks at lot cleaner. :-)

Svelte is one of the frameworks I stuck with the longest. There's a lot to like. What I didn't like was the gotchas around change tracking. I guess runes is intended to fix that, but... damn. :-)

netdevphoenix · 3 months ago
> it looks at lot cleaner

It always gets me when people say "cleaner" because the concept of cleanliness is subjective yet you see devs throw it around as if their views were the views of everyone and evidently right by any measure possible.

and0 · 4 months ago
IMO That example is way too complicated to be an elevator pitch. I'm trying to understand what working with your framework would be like for a web app, not the boilerplate to establish rules and state management of a small game.

The tutorial has a much better Hello World + incremental feature introduction, I'd put some of that before the larger example.

vanviegen · 4 months ago
You're right. Updated, thanks!
GordonS · 4 months ago
Which Aberdeen was the inspiration behind the name? (I'm near the original Aberdeen in Scotland, but I know there's at least one in Australia and several in the US too).
vanviegen · 4 months ago
The Scottish one! My girlfriend used to live there for half a year doing her Master's.

I also once made Glasgow (a React-clone created for educational purposes, when I was a CS teacher). So named, because it is ugly. ;-) https://www.npmjs.com/package/glasgow

I'm not sure what Edinburgh is going to be yet, but it would probably need to be rather iconic. :-)

amiga386 · 4 months ago
The Northern Lights of Old Aberdeen are home sweet home to me.

The Northern Lights of Aberdeen are what I long to see.

I've been a traveller all my life, and many's a sight I've seen.

God speed the day, 'til I'm on my way, to my home in Aberdeen!

https://www.youtube.com/watch?v=ufO8qNy2w6k

johnofthesea · 4 months ago
> I'm not sure what Edinburgh is going to be yet

Something with several layers? Old town, New town and space between.

ilikejam · 4 months ago
Oi! Glasgow's lovely! Well, some bits are...
vlod · 4 months ago
Disappointed there's no mascot with a cone.

https://www.bbc.com/news/uk-scotland-glasgow-west-65914456

test1235 · 4 months ago
There's also one in Hong Kong, surprisingly.

Fellow Aberdonian represent

amiga386 · 4 months ago
Named after Lord Aberdeen (born in Edinburgh)

https://en.wikipedia.org/wiki/George_Hamilton-Gordon,_4th_Ea...

> His diplomatic successes include organizing the coalition against Napoleon in 1812–1814, normalizing relations with post-Napoleonic France, settling the old border dispute between Canada and the United States, and ending the First Opium War with China in 1842, whereby Hong Kong was obtained.

GordonS · 4 months ago
Fit like!? (had to be said)
jmull · 4 months ago
> Express UIs naturally in JavaScript/TypeScript

I would disagree there. Conceptually, you're writing reactive HTML and CSS. I think it would be a lot more natural to express HTML and CSS using HTML and CSS (with extensions to add reactivity), not Javascript/Typescript.

(Svelte is an example of this, though it has it other issues, IMO)

vanviegen · 4 months ago
Meh, I think there's nothing magical about the HTML syntax that makes it particularly great for expression DOM structures. Except familiarity of course.
jmull · 4 months ago
It's not a question of HTML's syntax.

1. HTML is a widely implemented standard. What you learn and know about HTML, and what you create with HTML is widely applicable. Not so much for your HTML alternatives, like Aberdeen.

2. HTML is what the browser accepts, which means you end up dealing with it anyway, just with a transformation in between, making things harder. The bigger the transformation the harder it is. To develop with Aberdeen you still need to know HTML, but you also need to know Aberdeen and how it transforms into HTML. And, you typically end up needing to learn how to mentally transform backwards as you debug, e.g., using the browser dev tools, looking at problem HTML, and deciding how to change the Aberdeen code to fix it.