[HN Gopher] JavaScript Views, the Hard Way - A Pattern for Writi...
___________________________________________________________________
JavaScript Views, the Hard Way - A Pattern for Writing UI
Author : voat
Score : 172 points
Date : 2025-04-19 02:10 UTC (20 hours ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| zffr wrote:
| The read me says this approach is extremely maintainable, but I'm
| not sure I agree.
|
| The design pattern is based on convention only. This means that a
| developer is free to stray from the convention whenever they
| want. In a complex app that many developers work on concurrently,
| it is very likely that at least one of them will stray from the
| convention at some point.
|
| In comparison, a class based UI framework like UIKit on iOS
| forces all developers to stick to using a standard set of APIs to
| customize views. IMO this makes code way more predictable and
| this also makes it much more maintainable.
| netghost wrote:
| Convention works when the culture is there, but I think you're
| right a dash of typescript and a class or interface definition
| could go a long ways.
|
| I think the maintainability comes from easy debugging. Stack
| traces are sensible and the code is straightforward. Look at a
| React stack trace and nothing in the trace will tell you much
| about _your_ code.
|
| I'd also point out that this looks like it's about seven years
| old. We've shifted a lot of norms in that time.
| _heimdall wrote:
| Any code base lives or dies by how well it defines and then
| sticks to conventions. We can enforce it in different ways, or
| outsource the defining of convention to other tools and
| libraries, but we still have to use them consistently in the
| codebase.
|
| I think the OP here is basically proposing that the developer
| should be directly responsible for the conventions used. IMO
| that's not a bad thing, yes it means developers need to be
| responsible for a clean codebase but it also means they will
| better understand _why_ the conventions exist and _how_ the app
| actually works. Both of those are easily lost when you follow
| convention only because a tool or library said that 's how its
| done.
| nonethewiser wrote:
| Using a framework like react constrains developers in a
| different way. React isnt simply a convention like the linked
| example.
| _heimdall wrote:
| I see it differently there, react (any framework) is simply
| convention built into shared libraries and enforced through
| tooling.
|
| React is a particularly interesting one because it is still
| flexible enough that there is still a lot of reliance on
| developers actively sticking to the conventions
| recommended.
| atum47 wrote:
| On my first official job after college I was working on making a
| web version of a Delphi software. The team was already on their
| third rewrite of the front end cause they had to change
| frameworks. I made the cass that we should write our own
| framework, so I prototyped FOS (the components I use on my
| website) to prove my point. The team (a bunch of mostly Delphi
| programmers) did not like my suggestion. Anyways, soon after that
| another company made me a better offer so I left. Years went by
| an I finally take a shot at another framework: tiny.js [1]. I've
| been using it in all my personal projects so far. I'm particular
| proud of the ColorPicker [2] component I wrote that I've used in
| two projects so far. As you can see, one can argue that tiny.js
| it's not a framework at all, just some wrapper functions that
| helps you create Functional components.
|
| 1 - https://github.com/victorqribeiro/TinyJS
|
| 2 -
| https://github.com/victorqribeiro/Chip8js/blob/master/js/Col...
| npodbielski wrote:
| I dont know... I kind of like diffrent look of HTML and JS. At
| least you know what is what. In tiny evrything looks like JS
| and you actually have to read it to know what is what. Also
| what if someone will define span variable? Does it override the
| span HTML component function?
|
| Otherwise looks like nice.
| atum47 wrote:
| In tiny evrything looks like JS and you actually have to read
| it to know what is what
|
| You don't, actually. If in HTML you write
| <select><option/><select/> in tiny you write select(option())
| Also what if someone will define span variable?
|
| I'm guilty of that myself. Tried to name a variable input
| when there's already a function with that name. It forces me
| to come up with better descriptive names. I could've wrapped
| those functions inside namespace like tiny.input() but I like
| the simplicity of it as is.
| hu3 wrote:
| You might want to look at something like morph Dom to keep
| input focus when you have to re-render the form, for example.
| atum47 wrote:
| Since the user decides what happens when the state gets
| updated its up to them to address that. For me, I usually
| avoid re-renders when possible, I rather update the property
| associated with the state.
|
| Have you faced any scenarios where that's needed? I'm
| curious.
| lylejantzi3rd wrote:
| I came up with something similar recently, except it doesn't use
| template elements. It just uses functions and template literals.
| The function returns a string, which gets dumped into an existing
| element's innerHTML. Or, a new div element is created to dump it
| into. Re-rendering is pretty quick that way.
|
| A significant issue I have with writing code this way is that the
| functions nest and it becomes very difficult to make them compose
| in a sane way. function printPosts(posts) {
| let content = "" posts.forEach((post, i) => {
| content += printPost(post) })
| window.posts.innerHTML = content } function
| printPost(post) { return ` <div
| class="post" data-guid="${post.guid}"> <div>
| <img class="avatar"
| src="https://imghost.com${post.avatar.thumb}"/>
| </div> <div class="content"> <div
| class="text-content">${post.parsed_text}</div>
| ${post?.image_urls?.length > 0 ?
| printImage(`https://imghost.com${post.image_urls[0].original}`) :
| ''} ${post?.url_preview ? `<hr/><div
| class="preview">${printPreview(post.url_preview)}</div>` : ''}
| ${post?.quote_data ? `<hr/><div
| class="quote">${printQuote(post.quote_data)}</div>` : ''}
| ${post?.filtered ? `<div>filtered by:
| <b>${post.filtered}</b></div>` : ''} </div>
| </div> ` }
| hyperhello wrote:
| I like it. Not only does it move the UI into JavaScript, but it
| moves the scripting into the HTML!
| Koffiepoeder wrote:
| Have a feeling this will lead to XSS vulnerabilities though.
| MrJohz wrote:
| How do you update the html when something changes? For me,
| that's the most interesting question for these sorts of micro-
| frameworks - templating HTML or DOM nodes is super easy, but
| managing state and updates is hard.
| dleeftink wrote:
| I find the coroutine/generator approach described in a series
| of posts by Lorenzo Fox/Laurent Renard to be a promising
| alternative[0].
|
| It takes a little to wrap your head around, but essentially
| structures component rendering to follow the natural
| lifecycle of a generator function that takes as input the
| state of a previous yield, and can be automatically cleaned
| up by calling `finally` (you can observe to co-routine state
| update part in this notebook[1]).
|
| This approach amounts to a really terse co-routine
| microframework [2].
|
| [0]: https://lorenzofox.dev/posts/component-as-infinite-
| loop/#:~:...
|
| [1]: https://observablehq.com/d/940d9b77de73e8d6
|
| [2]: https://github.com/lorenzofox3/cofn
| lylejantzi3rd wrote:
| I call printPosts with the new post data. It rewrites the
| whole chunk in one go, which is pretty snappy. I haven't
| decided how I'm going to handle more granular updates yet,
| like comment count or likes.
| MrJohz wrote:
| Yeah, that's a pretty common approach. Unfortunately,
| browsers aren't very good at doing patch updates, so it'll
| completely reset any UI elements in the region being
| rerendered.
|
| It also will make it hard to scope anything you want to do
| to an individual DOM element. If you want granular updates,
| for example, you want to be able to do something like
| `document.querySelector(???)` and be certain it's going to
| refer to, say, a specific text input in your `printPost`
| template, without worrying about accessing the inputs
| created by other instances of the `printPost` template. You
| can do that with unique IDs, but it's fiddly and error-
| prone.
| econ wrote:
| I prefer something like this before building the template
| string.
|
| image = post.image_urls?[0] || "";
|
| Then have the printImage function return an empty string if the
| argument is an empty string.
|
| ${printImage(image)}
|
| Easier on the eyes.
| chrismorgan wrote:
| This is _begging_ for injection attacks. In this case, for
| example, if parsed_text and filtered can contain < or &, or if
| post.guid or post.avatar.thumb can contain ", you're in
| trouble.
|
| Generating serialised HTML is a mug's game when limited to
| JavaScript. Show me a mature code base where you have to
| remember to escape things, and I'll show you a code base with
| multiple injection attacks.
| foota wrote:
| Yeah, OPs code is asking for pain. I suspect there are now
| developers who've never had to generate html outside the
| confines of a framework and so are completely unaware of the
| kinds of attacks you need to protect yourself against.
|
| You can do it from scratch, but you essentially need to track
| provenance of strings (either needs to be escaped and isn't
| html, e.g., user input, or html, which is either generated
| and with escaping already done or static code). It seems like
| you could build this reasonably simply by using tagged
| template literals and having e.g., two different Types of
| strings that are used to track provenance.
| brigandish wrote:
| Thus recreating Perl's taint mode. Everything new is old.
| lylejantzi3rd wrote:
| Posts are sanitized on the server side. This is client side
| code.
| hombre_fatal wrote:
| Server-side sanitization means that your view code is
| inherently vulnerable to injection. You'll notice in modern
| systems you don't sanitize data in the database and you
| don't have to manually sanitize when rendering frontend
| code. It's like that for a reason.
|
| Server-side sanitization and xss injection should be left
| in the 2000s php era.
| jdsleppy wrote:
| Where do you suggest we sanitize values? Only in the
| client, when rendering them?
| chrismorgan wrote:
| Depends on what you mean by sanitising.
|
| If you mean filtering out undesirable parts of a document
| (e.g. disallowing <script> element or onclick attribute),
| that should normally be done on the server, before
| storage.
|
| If instead you mean _serialising_ , writing a value into
| a serialised document: then this should be done at the
| point you're creating the serialised document. (That is,
| where you're emitting the HTML.)
|
| But the golden standard is not to generate serialised
| HTML manually, but to generate a DOM tree, and serialise
| that (though sadly it's still a tad fraught because HTML
| syntax is such a mess; it works better in XML syntax).
|
| This final point may be easier to describe by comparison
| to JSON: do you emit a JSON response by writing `{`, then
| writing `"some_key":`, then writing `[`, then writing
| `"\"hello\""` after carefully escaping the quotation
| marks, and so on? You can, but in practice it's very
| rarely done. Rather, you create a JSON document, and then
| serialise it, e.g. with JSON.stringify inside a browser.
| In like manner, if you construct a proper DOM tree, you
| don't _need_ to worry about things like escaping.
| juliend2 wrote:
| What's wrong about filtering before saving, is that if
| you forget about one rule, you have to go back and re-
| filter already-saved data in the db (with some one-off
| script).
|
| I think "normally" we should instead filter for XSS
| injections when we generate the DOM tree, or just before
| (such as passing backend data to the frontend, if that
| makes more sense).
| zdragnar wrote:
| Don't forget that different clients or view formats
| (apps, export to CSV, etc) all have their own
| sanitization requirements.
|
| Sanitize at your boundaries. Data going to SQL? Apply SQL
| specific sanitization. Data going to Mongo? Same. HTML,
| JSON, markdown, CSV? Apply the view specific sanitizing
| on the way.
|
| The key difference is that, if you deploy a JSON API that
| is view agnostic, that the client now needs to apply the
| sanitization. That's a requirement of an agnostic API.
| chrismorgan wrote:
| Although appealing, that's an extremely bad idea, when
| you're limited to JavaScript. In a language with a better
| type system, it can be only a very bad idea.
|
| The problem is that different contexts have different
| escaping rules. It's _not possible_ to give a one-size-
| fits-all answer from the server side. It has to be done in
| a context-aware way.
|
| Field A is plain text. Someone enters the value "Alpha &
| Beta". Now, what does your server do? If it sanitises by
| stripping HTML characters, you've just blocked valid input;
| not good. If it doesn't sanitise but instead
| unconditionally escapes HTML, somewhere, sooner or later,
| you're going to end up with an "Alpha & Beta" shown to
| the user, when the value gets used in a place that _isn't_
| taking serialised HTML. It always happens sooner or later.
| (If it doesn't sanitise or escape, and the client doesn't
| escape but just drops it directly into the serialised HTML,
| that's an injection vulnerability.)
|
| Field B is HTML. Someone enters the value "<img src=/
| onerror=alert('pwnd')>". Now, what does your server do? If
| it sanitises by applying a tag/attribute whitelist so that
| you end up with perhaps "<img src="/">", fine.
| krapp wrote:
| Server-side templating frameworks had context-aware
| escaping strategies for years before front end frameworks
| were even a thing. Injection attacks don't persist
| because this is a hard problem, they persist because
| security is not a priority over getting a minimum viable
| product to market for most webdev projects.
|
| The old tried and true strategy of "never sanitize data,
| push to the database with prepared statements and escape
| in the templates" is basically bulletproof.
| naasking wrote:
| You're unnecessarily complicating this. The server is
| aware of what fields are HTML so it just encodes the data
| that it returns like we've been doing for 30 years now.
| If your point is that this approach is only good with
| servers that you trust, then that's useful to point out,
| although we kind of already are vulnerable to server
| data.
| spankalee wrote:
| You should really check out lit-html[1]. It's not a framework
| like this README claims. It just renders template with template
| literals, but it does so with minimal DOM updates and safely.
| And it has a number of features for declaratively adding event
| handlers, setting properties, and dealing with lists.
|
| [1]: https://lit.dev/docs/libraries/standalone-templates/
| edflsafoiewq wrote:
| It appears to be exactly the kind of manual-update code that
| reactive view libraries exist to replace.
| kyleee wrote:
| It's probably about time for that to become fashionable again
| ChocolateGod wrote:
| IIRC its what frameworks like Svelte do when they hit the
| compiler and optimize, which makes the best of both worlds.
| wruza wrote:
| They still nail "state" to element trees, which creates
| unbenchmarkable but real update costs. Svelte does better
| than react, but only within the same paradigm.
| MrJohz wrote:
| Can you describe what you mean by that a bit more? As I
| understand it, with the new signals-based system in
| Svelte, updating data directly updates the DOM.
| division_by_0 wrote:
| It's also worth noting that the Svelte signals
| implementation is quite performant. [0]
|
| [0] https://github.com/sveltejs/svelte/discussions/13277
| JoeyJoJoJr wrote:
| Do you mean subscribing to events/callbacks, manually
| managing object lifecycle, manually inserting list elements,
| keeping it in sync with the state, etc, etc. Because that was
| all friggen horrible. Maybe new approaches could make it less
| horrible, but there is no way I'd go back to what it was like
| before React. If anything, I want everything to be more
| reactive, more like immediate mode rendering.
| vendiddy wrote:
| It's easy to forget how tedious things used to be before
| React became popular.
|
| Keeping data in sync with the UI was a huge mental burden
| even with relatively simple UIs. I have no desire to go back
| to that.
| _heimdall wrote:
| Reactive view libraries exist to hide those details. I think
| the OP is proposing that the benefit of reactive views/state
| isn't worth the cost and complexity.
| d357r0y3r wrote:
| It is absolutely worth the cost and complexity. The cost and
| complexity of building a web application using some home
| grown vanilla JS system will end up being a horrible
| engineering decision most of the time.
|
| There have been zero times in my career where I thought "hmm,
| maybe we shouldn't have build this thing in React and let's
| just go back to page scripts." If you're building landing
| pages and websites, then okay. But that's not most of what
| we're all hired to build these days.
| dullcrisp wrote:
| Why not use Web Components? Is it because they're classes?
| brigandish wrote:
| I think it's because that repo is from 7 years ago, when
| browser support[1][2] for components wasn't as widespread or
| comprehensive.
|
| [1] See the history section of
| https://en.m.wikipedia.org/wiki/Web_Components
|
| [2] https://caniuse.com/?search=web%20components
| azangru wrote:
| > Why not use Web Components?
|
| If you check out his examples (e.g. clock), you will notice
| that he is using web components.
| athrowaway3z wrote:
| This might be heresy to many JS devs, but I think 'state'
| variables are an anti-pattern.
|
| I use webcomponents and instead of adding state variables for
| 'flat' variable types I use the DOM element
| value/textContent/checked/etc as the only source of truth, adding
| setters and getters as required.
|
| So instead of: /* State variables */ let
| name; /* DOM update functions */ function
| setNameNode(value) { nameNode.textContent = value;
| } /* State update functions */ function
| setName(value) { if(name !== value) { name =
| value; setNameNode(value); } }
|
| it would just be akin to: set name(name) {
| this.nameNode.textContent = name } get name() { return
| this.nameNode.textContent} /* or if the variable is
| used less than 3 times don't even add the set/get */
| setState({name}){
| this.querySelector('#name').textContent = name; }
|
| Its hard to describe in a short comment, but a lot of things go
| right naturally with very few lines of code.
|
| I've seen the history of this creating spaghetti, but now with
| WebComponents there is separation of objects + the adjacent HTML
| template, creating a granularity that its fusilli or macaroni.
| fendy3002 wrote:
| this is what I did in jquery era and it works very well, since
| it seldom to have state management at that era. Sure there's
| data binding libs like backbonejs and knockoutjs for a more
| complex app, but this approach works well anyway.
|
| Having a manual state that do not automatically sync to
| elements will only introduce an unnecessary complexity later
| on. Which is why libraries like react and vue works well, they
| automatically handle the sync of state to elements.
| Galanwe wrote:
| I don't think that is heresy, essentially you are describing
| what MUI calls unmanaged components - if I understand you well.
|
| These have their places, but I don't see them as an either-or
| replacement for managed components with associated states.
| triyambakam wrote:
| I really appreciate the concision and directness
| mcintyre1994 wrote:
| I think this makes a lot of sense when you're just wanting to
| update a single DOM node. And if you wanted to eg update its
| color as well, scoped CSS with a selector based on checked
| state is probably as nice as anything else. But how does this
| look if you want to pass that value down to child elements?
|
| Eg if you had child form fields that should be enabled/disabled
| based on this, and maybe they're dynamically added so you can't
| hardcode it in this parent form field. Can you pass that get
| function down the tree the same way you would pass react state
| as a prop?
| preommr wrote:
| I think this is a very popular opinion.
|
| A lot of people just wanted slight improvements like composable
| html files, and a handful of widgets that have a similar api.
| And for a long time it just wasn't worth the hassle to do
| anything other than react-create-app even if it pulled in 100x
| more than what people needed or even wanted.
|
| But stuff has gotten a lot better, es6 has much better, web-
| components... are there, css doesn't require less/sass. It's
| pretty reasonable to just have a site with just vanilla tech.
| It's part of why htmx is as popular as it is.
| socalgal2 wrote:
| I feel you, but isn't the state of truth for most websites
| supposed to be whatever is in the database? The example TODO
| List app, each TODO item has stuff in it. That's the source of
| truth and I believe what is trying to be solved for for most
| frameworks. In your example, where does name come from
| originally? Let's assume it's a contact app. If the user picks
| a different contact, what updates the name, etc...
|
| If the user can display 2 contacts at once, etc...
| Etheryte wrote:
| This is the exact same thing, but with the state pushed one
| level deeper, unless I misunderstand something here?
| motorest wrote:
| > (...) I think 'state' variables are an anti-pattern. I use
| webcomponents (...)
|
| It's unclear what you mean by "state variables". The
| alternative to state variables you're proposing with
| webcomponents are essentially component-specific state
| variables, but you're restricting their application to only
| cover component state instead of application state, and
| needlessly restricts implementations by making webcomponents
| mandatory.
|
| > (...) but now with WebComponents there is separation of (...)
|
| The separation was always there for those who wanted the
| separation. WebComponents in this regard change nothing. At
| most, WebComponents add first-class support for a basic
| technique that's supported my mainstream JavaScript frameworks.
| _heimdall wrote:
| "State variables" is a section in the original article. It
| shows a variable in the view, "name", that holds the value
| separate from the DOM.
|
| setName(value) first checks the local state variable, and if
| different the value is both written to the state variable
| _and_ the DOM.
|
| The GP's pattern uses getters and setters to directly read
| and write to the DOM, skipping the need for a local variable
| entirely.
| kikimora wrote:
| This approach is simple but does not scale. People did this
| long time ago, perhaps starting with SmallTalk in 80's and
| VB/Delphi in 90's.
|
| You need separation between components and data. For example
| you got a list of 1000 objects, each having 50 fields. You
| display 100 of them in a list at a time. Then you have a form
| to view the record and another to update it. You may also have
| some limited inline editing inside the list itself. Without
| model it will be hard to coordinate all pieces together and
| avoid code duplication.
| epolanski wrote:
| That screams pub sub which is trivial with JavaScript proxy
| imho.
| austin-cheney wrote:
| So, state is simple, stupid simple.
|
| The way to keep it simple is to have a single state object,
| which is the one place where state is organized and accessed.
|
| The way to make it scale is _architecture_. Architecture is a
| fancy word that means a repeatable pattern of instances where
| each instance of a thing represents a predefined structure.
| Those predefined structures can then optionally scale
| independently of the parent structure with an internal
| architecture, but the utility of the structure's definitions
| matter more.
|
| Boom, that's it. Simple. I have written an OS GUI like this
| for the browser, in TypeScript, that scaled easily until all
| system memory is consumed.
| alternatex wrote:
| I feel like you completely misinterpreted their comment.
| They replied to a comment saying that state should not be
| centralized. They said that if the state decentralized (as
| in held by individual child components) it's difficult to
| coordinate between sibling and parent/child components.
|
| It seems like you're saying that it's easy to do UI with a
| centralized state, therefore agreeing with them whilst
| having the tone of disagreement.
| austin-cheney wrote:
| The parent comment said: _This approach is simple but
| does not scale_ and _You need separation between
| components and data._ That is garbage and is what I
| replied to.
|
| I completely understand why JavaScript developers would
| fail to read this as such as most JavaScript developers
| are wholly incapable of programming, a forest for the
| trees problem.
| homarp wrote:
| can you elaborate on the 'don't scale part'? because apps in
| 90's don't see 'smaller' than webapps now
| mondrian wrote:
| The problem the name will have to be updated in 6 places in the
| UI.
| exe34 wrote:
| what are those 6 places? how were they updated before?
| mondrian wrote:
| You have a card with a name, when you click it a detail
| popup opens and the name is in there. There's also a delete
| button saying "Delete <name>", which pops up a confirm
| dialog and the name is in there, too. When you update the
| name you have to update all those places.
|
| The alternative is there's a canonical name variable, and
| it's rendered in all those places. To update the name, you
| just update that variable and "re-render", and those places
| naturally pick up the new value.
| exe34 wrote:
| yeah okay, I agree.
| bk496 wrote:
| 1. Then add the 6 updates to the "setter" function 2. What UI
| has the same data presented 6 times? Seems unnecessary
| MrJohz wrote:
| A lot of UIs have redundant data, it's very common to have
| the same data represented in different ways. Consider the
| comments page on HN, for example, which has plenty of
| duplicate information:
|
| The list of comments on a submission tells you how many
| comments exist, but the comment count is also made explicit
| at the top of the page directly underneath the submission
| title.
|
| If one person comments multiple times, their user name will
| appear multiple times on the page, despite being the same
| every time.
|
| All the timestamps are presented as relative timestamps,
| which means they're all dependent on the current time.
|
| Now this is a very simple page, and it's not so important
| that everything be updated live. But if it were, you'd need
| to update every single timestamp on the page, keep all of
| the usernames in sync in case a user changed their name,
| insert new comments while also updating the comment count,
| etc. There is a lot of redundancy in most UIs.
|
| In fact, I vaguely remember one of the early React blog
| posts using a very similar example (I think something to do
| with Messenger?) to explain the benefits of having a data-
| driven framework rather than using the DOM as the source of
| truth for data. For a messaging application, it's much more
| important that everything be live, and that elements don't
| end up out-of-sync.
| wordofx wrote:
| HN is static data. It doesn't update. Re-rendering a user
| name 20 times doesn't matter.
| MrJohz wrote:
| I wasn't trying to suggest that HN needs to update the UI
| like this, just give an example of how even a relatively
| simple UI like the one we're using now is full of
| duplicate data. I then also gave the example of a
| messaging app because that's a case where you do want
| everything in sync all the time.
|
| If you just rerender everything every time, then it's no
| problem to keep the whole UI in sync. But you probably
| don't want to render everything all the time - that's
| unnecessary work, and will break any stateful elements in
| the UI (such as form inputs that will get reset with
| every render). That's where the idea of React comes from:
| write code as if the whole UI is being rerendered every
| time, but internally only rerender the parts of the UI
| that have changed.
|
| Now that has its own disadvantages, and I think there are
| similar approaches out there, but the point is that
| keeping UIs in sync is a surprisingly hard problem.
| bk496 wrote:
| +1 have had multiple bugs arise because the state in the
| variable was not the same as the UI / DOM. Haven't had any
| problems a pattern similar to yours.
|
| If you have the edge case of lots of update (assignments to
| .name) then just wrap the `.name = ...` in a leading debounce.
| SkiFire13 wrote:
| > use the DOM element value/textContent/checked/etc as the only
| source of truth
|
| How do you manage redundant state? For example a list with a
| "select all" button, then the state "all selected"/"some
| selected"/"none selected" would be duplicated between the
| "select all" button and the list of elements to select.
|
| This is the fundamental (hard) problem that state management
| needs to solve, and your proposal (along with the one in the
| OP) just pretends the issue doesn't exist and everything is
| easy.
| liveafterlove wrote:
| That is just select with multi. And one can also have class
| vs id.
| jdsleppy wrote:
| They could always fall back to storing a value in a hidden
| element in the worst case. All/some/none selected is often
| done with an indeterminate state checkbox
| https://developer.mozilla.org/en-
| US/docs/Web/HTML/Reference/... that can represent all three
| states.
|
| Maybe I don't understand the problem you are talking about.
| robocat wrote:
| As soon as you need to store some state elsewhere you can
| store it in another suitable form (there's often some state
| not visually represented). I seem to recall jQuery stored
| state on a POJO (plain old JavaScript object) within a
| JavaScript variable of an element.
| SvenL wrote:
| A List of items could just contain checkboxes holding the
| state of selected/not selected. Then it's a trivial query
| selector. To get every selected item or to know if every item
| is selected.
| johnisgood wrote:
| Did you know you can have "stateful" UI without any
| JavaScript, using pure CSS and HTML? JS-less (Tor) websites
| use them.
|
| I have implemented a fully functional, multi-state CAPTCHA
| using only HTML + CSS for state simulation, and PHP for real
| validation.
| starwatch wrote:
| I love the idea of a single source of truth. However, how does
| your approach handle browsers / plugins that modify the dom?
| For example, I can imagine Google Translate altering the
| textContent and some resulting downstream effects on business
| logic.
| _heimdall wrote:
| If the view needs to react to the updated DOM you could use a
| custom element and the attribute changed callback. If you
| don't need to react to if the updated text content would just
| be there the next time the view needs to read it.
| _heimdall wrote:
| This was my first thought as well. I like the convention the OP
| is proposing here, with this one tweak making the DOM the
| single source of truth rather than local state inside views or
| components.
|
| Hell, even in react I try to follow a similar pattern as much
| as possible. I'll avoid hooks and local state as much as
| possible, using react like the early days where I pass in
| props, listen to events, and render DOM.
| chrismorgan wrote:
| A couple of issues that will arise from this:
|
| * Using DOM attribute or text nodes limits you to text only.
| This is, in practice, a _very_ big limitation. The simple cases
| are Plain Old Data which can be converted losslessly at just an
| efficiency cost, like HTMLProgressElement.prototype.value,
| which converts to number. Somewhat more complex are things like
| classList and relList, each a live DOMTokenList mapping to a
| single attribute, which needs unique and persistent identity,
| so you have to cache an object. And it definitely gets more
| intractable from there as you add more of your own code.
|
| * Some pieces of state that you may care about aren't stored in
| DOM nodes. The most obvious example is
| HTMLInputElement.prototype.value, which does _not_ reflect the
| value attribute. But there are many other things like scroll
| position, element focus and the indeterminate flag on
| checkboxes.
|
| * Some browser extensions will mess with your DOM, and there's
| nothing you can do about it. For example, what you thought was
| a text node may get an entire element injected into it, for ads
| or dictionary lookup or whatever. It's hard to write robust
| code under such conditions, but if you're relying on _your_ DOM
| as your source of truth, you _will_ be disappointed
| occasionally. In similar fashion, prevailing advice now is not
| to assume you own all the children of the <body> element, but
| to render everything into a div inside that body, because too
| many extensions have done terrible things that they should
| never have done in the first place.
|
| It's a nice theory, but I don't tend to find it scaling very
| well, applied as purely as possible.
|
| Now if you're willing to relax it to adding your own
| _properties_ to the DOM element (as distinct from attributes),
| and only reflecting to attributes or text when feasible, you
| can often get a lot further. But you may also find frustration
| when your stuff goes awry, e.g. when something moves a node in
| the wrong way and all your properties disappear because it
| cloned the node for some reason.
| rs186 wrote:
| > I use the DOM element value/textContent/checked/etc as the
| only source of truth
|
| Interesting idea but breaks down immediately in any somewhat
| serious application of reasonable size. e.g. i18n
| MatthewPhillips wrote:
| Hey, I'm the author of this doc. The reason for the pattern is
| to make it so you always can find why a mutation occured. So
| combining state variables and dom changes is ok as long as
| that's the only place that does the mutation. If not, now
| you've made it harder to debug. I keep the strict separation so
| that I can always stick a debugger and see a stack trace of
| what happened.
| triyambakam wrote:
| I like to prompt Claude to create artifacts in plain HTML, CSS
| and JS. I like the portability and hackability of these. React is
| too heavy for a lot of simple ideas even if reactivity is needed.
| dsego wrote:
| This reminds me of the venerable backbone js library.
| https://backbonejs.org/#View
|
| There is also a github repo that has examples of MVC patterns
| adapted to the web platform.
| https://github.com/madhadron/mvc_for_the_web
| yumaikas wrote:
| I've been working on https://deja-vu.junglecoder.com which is an
| attempt to build a JS toolkit for HTML-based doodads that shares
| some ideas with this.
|
| I don't quite have proper reactive/two-way data binds worked out,
| but grab/patch seem pretty nice as these things go. Also, the way
| this uses templates makes it very easy to move parts of the
| template around.
|
| It's also largely injection safe because it's using innerText or
| value unless told otherwise.
| popcorncowboy wrote:
| ...eschews abstractions...
| admiralrohan wrote:
| Might be feasible with the advent of vibe coding. For frontend
| heavy applications can choose this route for performance reasons.
| Starred, will follow the project.
| atoav wrote:
| I program for roughly two decades now and I never got warm with
| frontend frameworks. Maybe I am just a backend guy, but that
| can't be it since I am better in vanilla JS, CSS and HTML than
| most frontend people I have ever met.
|
| I just never understood why the overhead of those frameworks was
| worth it. Maybe that is because I am so strong with backends that
| I think most security-relevant interactions have to go through
| the server anyways, so I see JS more as something that adds
| clientside features to what should be a solid HTML- and CSS-
| base..
|
| This kind of guide is probably what I should look at to get it
| from first principles.
| edflsafoiewq wrote:
| The basic problem is when some piece of state changes, all the
| UI that depends on that state needs to be updated. The simple
| solution presented in the link is to write update functions
| that do the correct update for everything, but as the
| dependency graph becomes large and keeps changing during
| development, these becomes very hard to maintain or even check
| for correctness. Also the amount of code grows with the number
| of possible updates.
|
| Reactive view libraries basically generate the updates for you
| (either from VDOM diffing, or observables/dependency tracking).
| This removes the entire problem of incorrect update functions
| and the code size for updates is now constant (just the size of
| the library).
| skydhash wrote:
| But what if your dependency graph never becomes large (HN,
| Craiglist,...)?
|
| I believe a lot of web applications can go without any
| reactive framework as using one is a slippery slope. You
| start with React and 80% of your code is replacing browser
| features. Imperative may not be as elegant, but it simpler
| when you don't need that much extra interactivity.
| edflsafoiewq wrote:
| Then you don't need it. Same for if you can do everything
| (or most everything) with page reloads, or if you don't
| have any reactivity at all. But the problem is still real,
| even if people use frameworks when they don't really have
| to.
| seumars wrote:
| It seems the "hard way" here is just avoiding frameworks. The
| real hard part of UI is in fact state management and the myriad
| of methods for handling state.
| smarkov wrote:
| People like to hate on PHP, but PHP provides you with all the
| tools you need to write a fully working backend, where as JS
| provides you with half-assed solutions for writing frontend,
| which is why we have 1000 frameworks and we still can't agree on
| how to write frontend code. Seriously, we don't even have a
| convention for writing a simple reusable component with vanilla
| JS, everyone makes up their own thing. Web components were
| supposed to be that, but they're a good example of what I meant
| by "half-assed", because they're ugly, verbose, clunky, don't
| really solve the right problems, and nobody likes writing them.
| pjmlp wrote:
| That is why I made my peace with Next.js.
|
| It is the only framework that feels like I am using JSP, JSF,
| ASP.NET, Spring, Quarkus, PHP.
|
| Don't plan to use anything else in JS space, unless by external
| decisions not under my control.
| girvo wrote:
| While I chafe at some of its decisions, you're still correct.
| It's the only thing really in that space that's fully
| featured enough.
| brulard wrote:
| I don't think PHP is any better in solving the backend, than JS
| is in solving frontend. On the Frontend the situation is not
| ideal, but we made big leaps every let's say 5 years, going
| from jQuery to React and from React to later generation
| frameworks like svelte / solid etc. Yes, the landscape is
| fragmented and there are maybe too many options, but you make
| it sound like PHP is universally used as the backend solution,
| while I see it being used little these days except for legacy
| systems from 15-20 years ago.
| smarkov wrote:
| I never said that PHP was universally used, just that it has
| answers to most problems.
|
| jQuery has become obsolete these days because the problems it
| solves have largely been solved by additions to JS, but the
| interactivity of websites has continued to increase and
| browsers have yet to catch up to that. Frameworks like React
| actively fight against the browser rather than work with it
| by maintaining its own DOM state and constantly creating
| copies of state for every re-render of a component, along
| with a bunch of other magic. That's a lot of unnecessary
| loopholes just to make up for JS's lack of features when it
| comes to writing reactive UI.
| hu3 wrote:
| > you make it sound like PHP is universally used as the
| backend solution, while I see it being used little these days
| except for legacy systems from 15-20 years ago.
|
| Your eyes deceive you.
|
| https://finance.yahoo.com/news/exclusive-laravel-
| raises-57-m...
| wruza wrote:
| Something is wrong with web developers culture, cause even in
| framework-free vanilla mode they cannot get rid of data
| localization and welding the data and "component" trees together
| irrepairably.
|
| Rather than building a querySelector-able tree of elements to and
| monkey-patching mutiplexing nodes for syncing element counts, you
| invent the most bizarre ways to chain yourselves to the wall. For
| long time I couldn't understand what exactly drives this almost
| traumatic habit, and it's still a mystery.
|
| For the interested, this is the outline I count as non-bizarre:
|
| - make an html that draws your "form" with no values, but has
| ids/classes at the correct places
|
| - singular updates are trivial with querySelector; write a few
| generic setters for strings, numbers, dates, visibility,
| disability, e.g. setDate(sel, date)
|
| - sync array counts through cloning a child-template, which is
| d-hidden and locatable inside a querySelector-able container;
| make syncArray(array, parentSel, childSel) function
|
| - fill new and update existing children through "<parent> :nth-
| child(n) <name>"
|
| - update when your data changes
|
| Data can change arbitrarily, doesn't require passing back and
| forth in any form. All you have to do is to update parts of your
| element tree based on your projections about affected areas.
|
| And no, your forms are not so complex that you cannot track your
| changes or at least create functions that do the mass-ish work
| _and_ update ui, so you don 't have to. For all the forms you've
| done, the amount of work needed to ensure that updates are
| performed is amortized-comparable with all the learning cliffs
| you had to climb to turn updates into "automatic". Which itself
| is a lie basically, cause you still have to jump through hoops
| and know the pitfalls. The only difference is that rather than
| calling you inattentive, they now can call you stupid, cause you
| can't tell which useCrap section your code should go to.
| epolanski wrote:
| I have been writing recently an application in plain "vanilla"
| TypeScript with vite, no rendering libraries, just old-style DOM
| manipulation and I have to say I more and more question front end
| "best" practices.
|
| I can't conclude it scales, whatever it means, but I can conclude
| that there are huge benefits performance-wise, it's fun, teaches
| you a lot, debugging is simple, understanding the architecture is
| trivial, you don't need a PhD into "insert this
| rendering/memoization/etc" technique.
|
| Templating is the thing I miss most, I'm writing a small vite
| plugin to handle it.
| klysm wrote:
| The problems with this approach are exacerbated in a team
| setting. The architecture might be trivial from your
| perspective but good luck getting a bunch of other folks on
| board with different mental models and levels of experience.
| klysm wrote:
| If you look at it as a tradeoff space it makes more sense why
| the majority of folks are on some kind of react. What kind of
| problems do you want to experience and have to solve in a
| production setting?
| iamsaitam wrote:
| "I can also ditch a database and just dump everything into a
| text file." <- This is what you're saying. It isn't hard to see
| the problem with this kind of thing.
| AmalgatedAmoeba wrote:
| ngl, a lot of the times, an in-memory "database" that gets
| backed up to a file is perfectly reasonable. Even consumer
| devices have dozens of gigabytes of RAM. What percentile of
| applications needs more?
|
| Just because a technology works well for a few cases
| shouldn't mean it's the default. What's the 80% solution is
| much more interesting IMO.
| skydhash wrote:
| > _an in-memory "database" that gets backed up to a file is
| perfectly reasonable._
|
| We have org-mode, application configs, and music playlists
| as three widely used examples for this.
|
| You switch to a database when you need to query and update
| specific subsets of the data, and there's the whole
| concurrency things when you have multiple applications.
| prisenco wrote:
| I take it a step further and go no-build js with jsdoc.
|
| The hardest part about scaling this approach is finding UX
| designers who understand the web. Just as frontend devs have
| trained themselves to "think in react" over the past decade, so
| have designers. The understanding of the underlying
| capabilities and philosophies of the web have been lost to the
| idea that the web and mobile can be effectively the same thing.
|
| This approach can go far if the team using it knows and respect
| web technology.
| designerarvid wrote:
| IMO designers should read hypermedia systems by the htmx guy.
| nonethewiser wrote:
| Can you elaborate on website functionality, team size, and
| production readiness?
|
| I mean I totally agree on small personal projects. Thats just
| never the limiting factor though.
| only-one1701 wrote:
| I get what you're saying but people still write SPAs
| jbverschoor wrote:
| You had me at JavaScript
| efortis wrote:
| I use a helper similar to React.createElement.
| const state = { count: 0 } const init = () =>
| document.body.replaceChildren(App()) init()
| function App() { return ( h('div', null,
| h('output', null, `Counter: ${state.count}`),
| h(IncrementButton, { incrementBy: 2 }))) }
| function IncrementButton({ incrementBy }) { return (
| h('button', { className: 'IncrementButton',
| onClick() { state.count += incrementBy
| init() } }, 'Increment')) }
| function h(elem, props = null, ...children) { if (typeof
| elem === 'function') return elem(props)
| const node = document.createElement(elem) if (props)
| for (const [key, value] of Object.entries(props)) if
| (key === 'ref') value.current = node else
| if (key.startsWith('on'))
| node.addEventListener(key.replace(/^on/, '').toLowerCase(),
| value) else if (key === 'style')
| Object.assign(node.style, value) else if (key in node)
| node[key] = value else node.setAttribute(key,
| value) node.append(...children.flat().filter(Boolean))
| return node }
|
| Working example of a dashboard for a mock server:
| https://github.com/ericfortis/mockaton/blob/main/src/Dashboa...
| simonw wrote:
| That looks like it replaces the entire document every time
| state changes. How's the performance of that?
| WickyNilliams wrote:
| Even if performance is fine, the big usability issue is that
| it will blow away focus, cursor position etc every render.
| Gets very painful for keyboard use, and of course is a fatal
| accessibility flaw
| efortis wrote:
| yes, that's the downside, focus is lost on init()
| WickyNilliams wrote:
| Really wish the browser gave us better APIs for updating
| the DOM. Creation is extremely easy, but after that you
| either have to invent some reconciliation system or stick
| with imperative updates
| floydnoel wrote:
| this is pretty much how i wrote one of my side projects,
| https://bongo.to
|
| it was fun and very fast to ship. no frameworks or libraries
| needed.
___________________________________________________________________
(page generated 2025-04-19 23:01 UTC)