| ▲ | athrowaway3z 3 days ago |
| 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. |
|
| ▲ | chrismorgan 3 days ago | parent | next [-] |
| 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. |
|
| ▲ | kikimora 3 days ago | parent | prev | next [-] |
| 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 3 days ago | parent | next [-] | | That screams pub sub which is trivial with JavaScript proxy imho. | |
| ▲ | homarp 3 days ago | parent | prev | next [-] | | can you elaborate on the 'don't scale part'?
because apps in 90's don't see 'smaller' than webapps now | |
| ▲ | austin-cheney 3 days ago | parent | prev [-] | | 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 3 days ago | parent [-] | | 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 3 days ago | parent [-] | | 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. | | |
| ▲ | afiori 2 days ago | parent | next [-] | | But centralised state is "separation between components and data" | |
| ▲ | nsonha 2 days ago | parent | prev [-] | | > JavaScript developers would fail to read this > You need separation between components and data this is an universal way to do UI, in fact only javascript developers (out of curiosity, are you one?) would even argue against it, because "use the platform", vanila, simple... blabla, or you know, "incapable of programming" and thinking in terms of data flow as opposed to platform intricacies | | |
| ▲ | austin-cheney 2 days ago | parent [-] | | The worst part about working in JavaScript is the insecurity. Everybody likes to act like they know what they are doing, but very few people really do. So, for most people its entirely a game to remain employed when they probably aren't qualified to be there in the first place. Its like musical chairs where everybody hopes when the music stops its the next guy left standing. For the few people who do know what they are doing its an impossible situation. People simultaneously expect magic from you, because they know you can do it, but at the same bitch and cry about the output. They know they cannot write original code or extend somebody else's original solution so they demonize it out of self-preservation. When people are universally that insecure its convenient to hate on that one person who doesn't share that insecurity. That is why I switched careers. I have never seen a salary high enough to justify going back to that. |
|
|
|
|
|
|
| ▲ | SkiFire13 3 days ago | parent | prev | next [-] |
| > 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. |
| |
| ▲ | jdsleppy 3 days ago | parent | next [-] | | 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. | | |
| ▲ | motorest a day ago | parent | next [-] | | > They could always fall back to storing a value in a hidden element in the worst case. This approach sounds like it's desperately trying to shove a square peg through a round hole. Why would anyone choose to use an element, hidden or not, to store a value as an alternative to use a very pedestrian JavaScript variable? | |
| ▲ | robocat 3 days ago | parent | prev [-] | | 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. |
| |
| ▲ | johnisgood 3 days ago | parent | prev | next [-] | | 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. | |
| ▲ | athrowaway3z 2 days ago | parent | prev | next [-] | | 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. noneSelected = !querySelectorAll("input:checked")
| |
| ▲ | liveafterlove 3 days ago | parent | prev | next [-] | | That is just select with multi. And one can also have class vs id. | |
| ▲ | SvenL 3 days ago | parent | prev [-] | | 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. |
|
|
| ▲ | mcintyre1994 3 days ago | parent | prev | next [-] |
| 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? |
|
| ▲ | motorest 3 days ago | parent | prev | next [-] |
| > (...) 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 3 days ago | parent [-] | | "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. | | |
| ▲ | lelanthran 2 days ago | parent [-] | | I have an approach that I think no one else has tried before. Looking for someone to endorse me on arxiv so I can publish it. |
|
|
|
| ▲ | MatthewPhillips 3 days ago | parent | prev | next [-] |
| 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. |
|
| ▲ | fendy3002 3 days ago | parent | prev | next [-] |
| 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 3 days ago | parent | prev | next [-] |
| 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. |
|
| ▲ | rs186 3 days ago | parent | prev | next [-] |
| > 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 |
|
| ▲ | preommr 3 days ago | parent | prev | next [-] |
| 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 3 days ago | parent | prev | next [-] |
| 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... |
| |
| ▲ | athrowaway3z 2 days ago | parent [-] | | 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. |
|
|
| ▲ | _heimdall 3 days ago | parent | prev | next [-] |
| 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. |
|
| ▲ | bk496 3 days ago | parent | prev | next [-] |
| +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. |
|
| ▲ | mondrian 3 days ago | parent | prev | next [-] |
| The problem the name will have to be updated in 6 places in the UI. |
| |
| ▲ | exe34 3 days ago | parent | next [-] | | what are those 6 places? how were they updated before? | | |
| ▲ | mondrian 3 days ago | parent [-] | | 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. | | |
| |
| ▲ | bk496 3 days ago | parent | prev [-] | | 1. Then add the 6 updates to the "setter" function
2. What UI has the same data presented 6 times? Seems unnecessary | | |
| ▲ | MrJohz 3 days ago | parent [-] | | 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 3 days ago | parent [-] | | HN is static data. It doesn’t update. Re-rendering a user name 20 times doesn’t matter. | | |
| ▲ | MrJohz 3 days ago | parent [-] | | 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. |
|
|
|
|
|
| ▲ | starwatch 3 days ago | parent | prev | next [-] |
| 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 3 days ago | parent [-] | | 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. |
|
|
| ▲ | 3 days ago | parent | prev | next [-] |
| [deleted] |
|
| ▲ | Etheryte 3 days ago | parent | prev | next [-] |
| This is the exact same thing, but with the state pushed one level deeper, unless I misunderstand something here? |
|
| ▲ | 3 days ago | parent | prev | next [-] |
| [deleted] |
|
| ▲ | triyambakam 3 days ago | parent | prev [-] |
| I really appreciate the concision and directness |