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.
• 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.
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.
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.
> 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.
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.
I don't think I understand your question, or its just a poor example.
Regardless of design pattern or framework; the state all/some/none of a list, should practically never exists as separately updated state variable. Whenever its required you need to derive it.
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.
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?
> (...) 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.
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.
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.
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.
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...
The requirements are a bit too vague so i'm guessing here.
The design were talking about is mutating local state to update the view.
Unchanging variables (like a name from a db) are provided on construction and not relevant.
Selecting a new contract to 'open' creates a new contract element. No need to update the existing element.
----
If you're talking about "if I edit <input> here it updates <input> there as well", than I believe those are gimmicks that reduce usability.
If I understand your example correctly: a multi-contract view where the user updates a 'name' in both. IMO its a better UI to explicitly have the name:<input> _outside_ the contract elements. The contract element can do nameInput.onchange =(e) => {...} when constructed to update itself.
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.
+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.
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.
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.
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.
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.
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.
I would love to see a new take on backbone in the modern web without any jQuery integration. I genuinely miss how easy and powerful backbone views are.
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.
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.
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?
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.
"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.
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.
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.
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.
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.
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.
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].
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.
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.
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.
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.
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.
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.
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).
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.
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
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:
it would just be akin to: 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.
• 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.
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.
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.
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.
Maybe I don't understand the problem you are talking about.
I have implemented a fully functional, multi-state CAPTCHA using only HTML + CSS for state simulation, and PHP for real validation.
Regardless of design pattern or framework; the state all/some/none of a list, should practically never exists as separately updated state variable. Whenever its required you need to derive it.
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?
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.
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.
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.
These have their places, but I don't see them as an either-or replacement for managed components with associated states.
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.
Interesting idea but breaks down immediately in any somewhat serious application of reasonable size. e.g. i18n
If the user can display 2 contacts at once, etc...
The design were talking about is mutating local state to update the view.
Unchanging variables (like a name from a db) are provided on construction and not relevant.
Selecting a new contract to 'open' creates a new contract element. No need to update the existing element.
----
If you're talking about "if I edit <input> here it updates <input> there as well", than I believe those are gimmicks that reduce usability.
If I understand your example correctly: a multi-contract view where the user updates a 'name' in both. IMO its a better UI to explicitly have the name:<input> _outside_ the contract elements. The contract element can do nameInput.onchange =(e) => {...} when constructed to update itself.
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.
If you have the edge case of lots of update (assignments to .name) then just wrap the `.name = ...` in a leading debounce.
Deleted Comment
Deleted Comment
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.
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.
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.
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
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.
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.
I mean I totally agree on small personal projects. Thats just never the limiting factor though.
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.
Deleted Comment
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.
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.
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.
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
[1]: https://lit.dev/docs/libraries/standalone-templates/
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.
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.
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.
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.
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).
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.