Show HN: A JavaScript UI library for imperative JSX

npmjs.com

41 points by danielvaughn 3 months ago

I've been building a web UI library for a side project of mine. I thought it might be useful to others, so I'm releasing it as open source.

To put it simply, I realized that most of my pain points with React come from its declarative model ui=f(state). So I'm trying something that I'm calling "imperative JSX." Instead of treating JSX as the source of truth for your UI, it essentially becomes a query interface for DOM manipulation.

I first had the idea for it a few months ago, and only began writing it in earnest last week, so it's extremely early and nowhere near production-ready. Still, I'd appreciate feedback on it (positive and negative)!

The side project I'm working on is called Matry, so the library is currently called @matry/dom. I'm slowly building up a list of examples of it in action at this repo: https://github.com/matry/dom-recipes

Cheers!

justin_murray 3 months ago

It's cool to see a novel idea like this get prototyped and then opened up for discussion.

As others have hinted at, this feels like jQuery with different syntax and more limitations. In its current form, I can't imagine choosing this over jQuery (in part because I already know jQuery, and in part because the limited reach of your selectors feels like it would be a roadblock very quickly).

Perhaps you could reshape this to be an extension of jQuery, where this alternative jsx syntax is available if you want it, but the full power of jQuery's selectors is also available (without having to mix two libraries with overlapping purpose)?

That aside, I can't imagine abandoning React for this. That would feel like a step backwards to me. IMO, the most valuable thing React brought to the ecosystem is a simple way to make reusable components. Much of React's convenience is because of its functional nature: each component instance has an isolated scope/context, making it easy to reason over the behavior of a small widget without worrying about external effects from the larger application.

From what I can see in your examples, you're relying on things like global identifiers/keys, which I think leads to a mess in a large application. Take your counter example: what if my application needs 5 such counters? Can I do this without significant code duplication? I could probably wrap it in a function, but I'd likely need to pass in some unique id to distinguish between the 5 usages. This is a huge part of what React does for you, just keeping track of which component instances go where in the DOM (without needing DOM `id`s everywhere).

It could be that I'm missing something. It'd be great to see some larger examples of a more complex application with widget reuse, to see how that feels.

  • justin_murray 3 months ago

    Just a few more thoughts to add:

    When I see code like

        count++;
        setContent(<p key="counter">The count is {count}</p>);
    
    I immediately think about how someone, at some point, will introduce bugs/weirdness where they update some state (`count`), but then forget to make the `setContent` call to update the DOM. That's a very useful thing that React's `useState` does; a single way to update the state that is guaranteed to always be synced with the DOM. Of course, I could make a `setCount` function to help abstract this, but that's more boilerplate and just more opportunity for someone to screw it up.

    Another point related to scoped components: one thing that I saw happen in jQuery codebases (and I think the same would happen here) is that it's too easy to mistakenly write a rogue selector that affects some part of the DOM that is far away from what was intended. It's one of those great power = great responsibility things... but I think this power more often leads to complex and confusing code that is hard for someone to understand and maintain.

    React can be frustrating at times, when it seems like it would be easiest for a parent component to directly manipulate some nested child (or vice versa), but I think this friction often just leads to code that is easier to reason over.

    • danielvaughn 3 months ago

      "greater power & greater responsibility" is definitely the tradeoff I'm making. One of the tradeoffs I list in the readme is that you really need to think about the UI in a way that you don't with React. It's certainly more expedient to use React and let it handle the platform itself. On the other hand, you pay for that in a multitude of other ways - it's more than just a few frustrations here and there.

  • Justsignedup 3 months ago

    I have to strongly agree with this. The entire point of react was to avoid all the challenges of old imperative coding of syncing up state with render state. The Dom digging was an absolutely genius idea to stop that nonsense and just render the correct state and let the framework handle the rest.

    I wrote a ton of backbone code, and was a major fan of jquery before Dom querying was even a feature of browsers. And once I went react I really fell in love with frontend dev. Frontend is so simple now because of it, while it used to be dare I say more complex than backend back in the day.

    • danielvaughn 3 months ago

      Same, used jQuery for years and had a love/hate relationship with it.

      You're correct that React solved the imperative problems from before, but that's not the only factor to consider. You have to weigh the problems it solves against the problems that it causes.

      • Justsignedup 3 months ago

        Eh. For me, it was vastly worth it. Yes there are drawbacks to react. But I feel like the gains far far far outweigh them.

  • danielvaughn 3 months ago

    Yeah I opted for quick, simple demonstrations of the basic ideas. In a real application, at least one of any significant complexity, I wouldn't be writing the code you see in those demos. I'm experimenting with how it might integrate with state management libraries, currently with mobx.

    I haven't done any work on this front yet, but I'm especially excited to see how this might fit into an SSR/MPA app. I have some ideas though; my goal is to implement ISR/streaming/etc without the need for weird little conventions like "use server".

udbhavs 3 months ago

> If we accept the fact that the web is inherently imperative, then I believe we can resolve most if not all of the above problems. We don't need to throw the baby out with the bathwater though, because JSX is fantastic.

Also look into SolidJS for dropping the "functional" component model while still retaining JSX - it looks similar to React but works more like Vue, running components as setup functions only on initial render and doing state updates with mutations via signals.

  • danielvaughn 3 months ago

    Yeah I like the idea of signals a lot. I haven't played with SolidJS but I'll give it a shot.

mati365 3 months ago

That's the worst idea for framework I've seen recently

  • danielvaughn 3 months ago

    Seems like others are downvoting you, but I appreciate seeing the sentiment. I’ll just note that the some of the most influential technologies I’ve encountered in my career were initially disgusting to me.

    I thought React was gross and I thought Tailwind was gross. But now I love them.

    Doesn’t mean this is going to be good, just that first impressions aren’t everything.

    • lgas 3 months ago

      Tailwind is gross. It's just that it's the least gross of the available options.

  • zarathustreal 3 months ago

    While I do agree, it would be nice to see something more than just “I hate this” in a comment.

    For example you could remind the author of the perils of imperative programming such as needing to mentally simulate application state at every line to understand the program vs functional programming where you have referential transparency

    • danielvaughn 3 months ago

      Yep fair, although imperative and functional aren’t exactly mutually exclusive. The examples I have right now are really just for demonstration purposes, but you could use this library in a ton of different ways. Right now I’m building some demos of it using mobx, for example.

peterleiser 3 months ago
gedy 3 months ago

Thanks for sharing, neat. This makes me curious about the psychology of declarative vs imperative, and why some people gravitate towards one or the other (at least with UI code).

Back in the jQuery days, a lot of my hatred towards "JavaScript" was actually imperative UI code in hindsight. In the years since, I now enjoy declarative UI more but see some developers bang their head against declarative UI frameworks. It's an interesting contrast.

  • danielvaughn 3 months ago

    I also enjoy declarative UI. I hated imperatively building up the interface with jQuery, even with the helpers it just felt so tedious. This library is my attempt to have my cake and eat it too.

    Like I love being able to just write <div><p>hello world</p></div> in my javascript, it feels very nice. At the same time, with React I absolutely hate it when I do things that are inherently imperative.

    A prime example is the following: I have a list of items that can be named by the user, so each item's display name will be a text input. When the user clicks the "create new item" button, a new item is added to the list, and the text input of the new item is immediately focused as soon as it's added to the DOM.

    ^ doing this in React sucks.

    • gedy 3 months ago

      The only thing that is awkward imho is the focusing, the rest is a really good fit for declarative, e.g. here's your list of "things", render a UI for each.

lgas 3 months ago

Can you talk a little about the pain points you encountered, which this presumably solves?

  • danielvaughn 3 months ago

    Sure! There are several, but let's take a trivial one - setTimeout. This is an example of something that is inherently imperative, and therefore requires some awkward logic to get it working correctly in React. Here's an article explaining how to do it:

    https://codedamn.com/news/reactjs/how-to-use-settimeout-in-r...

    In contrast, here's how you would do it in matry:

      let someValue = 0;
    
      setTimeout(() => {
        someValue++;
        setContent(<p id="some-element">value is {someValue}</p>)
      }, 1000)
    
    There's no special concept you need to understand, and what's more is that it will not perform any rendering logic on #some-element's siblings or descendants. React may do that depending on how you structured your component.
    • SebastianKra 3 months ago

      This is IMO a great example for React and declarative programming.

      > There's no special concept to understand

      By not understanding that you need to cleanup timers when your view unmounts, you've introduced a subtle bug that your coworker will discover in 6 months when users report that the counter sometimes increases twice as fast.

      I've worked on Angular-like apps before, and storing and cleaning up timers was always a pain point:

        private timer
        private someValue = 0
      
        onMount() {
          timer = setTimeout(() => {
            someValue++;
            setContent(<p id="some-element">value is {someValue}</p>)
          }, 1000)
        }
      
        onUnmount() {
          clearTimeout(timer)
        }
      
      React on the other hand, clearly defines the concept of an effect as something that is initiated after the component renders and then cleaned up. You also keep the setup and cleanup close together, so you don't need to manually store the timer handle.

        const [someValue, setSomeValue] = useState(0)
      
        useEffect(() => {
          const timeout = setTimeout(() => {
            setSomeValue(v => v + 1)
          }, 1000)
          return () => clearTimeout(timeout)
        }, [])
      
        return <p>value is {someValue}</p>
      
      People often criticise useEffect for being hard to grasp, but it is the perfect essence of how to manage, well... effects in a component-based system.

      That's why Vue and Svelte have the same thing: https://vuejs.org/guide/essentials/watchers.html https://svelte.dev/blog/runes

      • Spivak 3 months ago

        > By not understanding that you need to cleanup timers when your view unmounts

        I think this library eschews the entire concept of mounting so that point is kind of moot. Will you likely re-invent it again in any non-trivial app, sure, but when you're writing imperative JS it's probably a MPA where code runs top to bottom. Clean up will happen when everything gets thrown away on navigation.

        • danielvaughn 3 months ago

          Yep, and if you are writing an SPA where cleanup is an issue, personally I think a web component would be a better fit. It's native to the platform and offers lifecycle hooks out of the box. I'm currently playing around with how my library might be used for web components.

      • WorldMaker 3 months ago

        I appreciate the RxJS observables approach with similar built-in cleanup, but also ways to build higher-level tools and compose things together.

            interval(1000).pipe(…)…
        
        Puts the timer up front and center. There's some confusion between "hot" and "cold" to deal and sharing with, but the pipelines eventually clean themselves up and the higher-level operators can be very nice to you if you let them.
        • SebastianKra 3 months ago

          reactiveX will always have a special place in my heart, but at some point I had to accept that the soup of switchMap, combineLatest, distinctUntilChanged wasn't fun for anyone on the team but me.

          If we ever get native stream support in JS, I believe it will be closer to hooks than to rxjs.

          Signals could actually be a good middle-ground with how they are pull-based, automatically infer dependencies, don't require complicated Syntax like switchMap and combineLatest, and give more control to the consumer (batching, memoization).

          • WorldMaker 3 months ago

            Personally, I have a hard time not seeing Signals as just non-conforming Observables missing a bunch of useful operators. They remind me of how much people loved ko.computed() from Knockout (and I've joked at times that Signals are just a return to ko.computed), but then had an awful time debugging the edge cases or fixing the performance from bad dependencies. Hooks maybe lean too much the other side of the spectrum in trying to force manual dependency tracking but maybe making it harder than necessary. ReactiveX-style Observables hit a sweet spot for me in dependency management that is hard to beat, especially once you get into all of those complicated operators like switchMap/combineLatest/distinctUntilChanged that can be very powerful tools in the right hands, especially for things like dependency management and throttling. (Though yes, getting junior developers and some types of teams all on the same page can be the hard part. In general a lot of functional programming gets tot be that way: once over the learning curve it's all gravy, but the learning curve looks like a terrifying roller coaster to many that haven't yet.)

            A proper pipe operator or a C#-like "fluent extension methods" approach might help a lot to clean up some of "scariness" of the first hill of the roller-coaster. If we are wishing for ponies, it might even be really nice in JS to have a proper monadic do-notation, even just a limited form like C#'s LINQ syntax (from … in … let … where … select …). "Monadic do-notation" sounds like its own terrible rollercoaster hill, but is a powerful and succinct way that lets you use a (monad) like an RX Observable in a very imperative ("Signal-like" way that is easy to read, even if the underpinnings feel hard to grasp. (LanguageExt for C# is a fascinating example in how it allows using the from/select syntax to do some interesting monadic binding, that I've seen used successfully by junior developers that don't know what a monad is, but know C# enough and find they can read/write the LINQ well enough.)

            I've also had some success eventually training junior developers to think in RxJS or ReactiveX in general. Marble diagrams are particularly great for visual learners to better piece together what happens. (I'm a big fan of reactivex.io's Operators pages for that, and RxJS' marble testing-based unit test harness. I've seen a similar library for C# but it didn't support all the parts of RxJS' syntax which I needed. I'd love to see that need met, too.)

jauco 3 months ago

This reminds me of google’s incremental dom (https://google.github.io/incremental-dom/ ) it surely made some things (like making sure an element has focus) a lot easier to do and in general felt a bit more like something that works with the browser instead of against it.

  • danielvaughn 3 months ago

    That's super interesting, hadn't heard about that, thanks.

gushogg-blake 3 months ago

Hmm, interesting. What advantages do you see of using JSX for querying over the old jQuery-style $("#id").append(...)? Did you consider doing something more like that, but allowing JSX instead of HTML strings like it was done in jQuery?

  • danielvaughn 3 months ago

    Using JSX syntax allows you to consolidate all similar operations in a single call. matry-dom traverses the JSX you pass into a call, finds all leaf nodes, and performs the operation on each of them. So you could do this:

      function onUserLogin(user) {
        matry.replace(
          <main id="root">
            <img key="user-profile" src={user.profileImage} />
            <aside>
              Welcome, {user.name}
            </aside>
          </main>
        )
      }
    
    In this example, the img and aside elements could be anywhere in the tree.
llimllib 3 months ago

Mithril seems like it can do something pretty similar: https://mithril.js.org/jsx.html

(Not trying to discourage you, just linking it)

  • danielvaughn 3 months ago

    No this is great, thanks for sharing. I've had several people point me to things in this thread that I'd never seen before, and I appreciate it.

spankalee 3 months ago

I would never want to write it the same element in multiple places like this.

That's one of the main points of templating in the first place: to write the code for a piece of output once, with all of the bindings that can affect it.

What happens here if you update the JSX for an element in one place and forget to do so in the others?

  • danielvaughn 3 months ago

    It's true that the library doesn't stop you from shooting yourself in the foot by duplicating UI references. But you can limit the effect of this with a bit of design work. I'm currently iterating on a few ideas, but this lib is only a week old, so I'm still thinking my way through it.

padjo 3 months ago

> The structure of your application state ends up being shaped by the UI, when it should be the other way around

I don’t understand this premise at all. Are you saying your UI should be shaped by your application state? That doesn’t make any sense to me, surely the application state exists to implement a desired UI?

  • danielvaughn 3 months ago

    I could've said that better, what I really meant is that UI is (a) downstream of state, and (b) has a different structure than state. The UI is a tree but state is basically a graph, and it doesn't make sense to couple the two. This is the same position that SolidJS takes, for instance.

    To respond to your last statement though, I feel the opposite is true. UI's don't exist for their own sake, they only exist to support the application and its behavior.

crubier 3 months ago

This is jQuery-like. Just syntax sugar above the imperative DOM API. Looks like the younger generation didn't know the pain of years of imperative UI "frameworks" and needs to learn this the painful way. Enjoy!

  • danielvaughn 3 months ago

    I wrote jQuery from 2009-2016

    • crubier 3 months ago

      And I can tell you liked it! It’s fine, but not all of us did!

fbn79 3 months ago

Why not use template literals and a css like syntax for selecting and update?

setContent(css`p[key=counter]{ content: 'The count is ${count}'}; }`);

  • danielvaughn 3 months ago

    Great question. So right now the library is using xpath expressions under the hood, which are more powerful than css selectors because they can query for content as well.

darepublic 3 months ago

React, the good parts

  • danielvaughn 3 months ago

    Yep, although I do really like the declarative model. I don’t think anyone wants to go back to jQuery. But over time, the downstream effects of React are starting to show up. There’s significant complexity with adopting React in an SSR context, and a bunch of bespoke concepts and weird little rules are required to make it work.