If anyone wants to take this idea to another level, I wrote a post showing how to do the same but in 10 times more lines of code: https://pomb.us/build-your-own-react/
Very cool, thanks for sharing that! Now all that's missing is a "eval" button that runs the result and drops you in a repl to experiment with the results.
Hey, that's what Flems is for. Check out https://github.com/porsager/flems - a single include embeddable sandbox (coincidentally written in mithril as the original post references). It's also used for https://flems.io
Hm, you seem a little to eager to promote your own stuff. This thread was about the "live-coding" aspect where the code updates on the left as you scroll down the post, so you can follow along with how the code is being built. I fail to see how your project does that. Yours seem like the standard code playground, so very different from what was linked earlier in the thread.
Or I misunderstand or simply missed where the "live-coding together with text" comes in.
Hehe, you’re probably right about that, but you could script the live part with the api, and then get the eval part to update as you mentioned, and allow the user to play around at the same time. I'm sorry you didn't find it related.
No harm done, just a few bytes stored on HNs servers somewhere so no worries :)
Yeah, I guess using the implementation linked before + your solution would create what I wanted, so thanks anyways for sharing. Never know when someone might find it useful.
Thanks for that super useful article @pomber! I read it a while ago and attempted to convert it to TypeScript and do a few extensions, here's my attempt: https://github.com/manasb-uoe/didact.
This library isn't meant to have full parity with React, but it's a great example that illustrates how simple a naive VDOM diff/patch is to implement.
That said, this is missing a lot of essential optimizations that libraries like React make. For example, key and type matching, batching updates, scheduling updates, and other stuff that isn't immediately apparent without reading the code.
"Generally speaking, React's approach to performance is to engineer relatively complex solutions.
Mithril follows the less-is-more school of thought. It has a substantially smaller, aggressively optimized codebase. The rationale is that a small codebase is easier to audit and optimize, and ultimately results in less code being run."
(Obviously, I'm not claiming the same for this toy example - just that batching updates etc, may not be necessary for performance).
I used Mithril recently in a small project and it's quite awesome.
I didn't miss any of React's features plus it includes a router and an http client in less than 10kB gzipped. Heck, my whole application weighted something like 38kB. Not the JS bundle, everything.
The other greatest thing about Mithril is that there is no reactive state per se. Your state are just vanilla objects and vars and then you tell Mithril to redraw the whole thing. It sounds wasteful but it's quite fast and the developer experience becomes a joy.
Mithril is like a sharp sushi chef knife. Since it leaves all the application architecture to you you better know what you're doing otherwise you will cut yourself pretty badly.
I wondered what happened to .prop() which looked like it had a lot of potential. Looks like that potential was (at least partially?) realized as it evolved into a "stream" library [1], which basically acts like cells in a spreadsheet. This seems like a really useful concept which could be used to create more kinds of UI frameworks than just declarative React style ones.
I just looked over their website for 10 minutes and still can't figure out how it handles more complex nested components with complex state. Is it like Elm and Redux where it has a giant tree and you have to update the root with a bunch of events, creating intermediate adaptor events from the deepest nodes all the way to the top? Does it have anything like React's Contexts? I really like the examples I saw on the landing page, but I would rather like some comparisons of how to do the exact real-world things React can do. Mithril's website did this pretty well. Maybe this is hurting adoption? Or maybe it's just me.
It depends on the use case. For applications that experience a high frequency updates near the top of the tree, a naive solution would typically block user input.
Mithril's solution is very simple but using a setInterval(render, 16.6667) has its drawbacks that can lead to jankiness in the UX. React 16 from what I understand is almost a complete rewrite of React and does some smart stuff with animation frames to ensure an optimal UX.
Mithril does use requestAnimationFrame unless you're in some version of IE. But Mithril rendering was never intended to run as the inner loop of an animation. Lifecycle hooks and CSS animations are preferable instead.
React Fiber basically breaks down large animation payloads so each "chunk" can fit within one frame budget, but at the cost of a lot of untreeshakeable internal and peripheral complexity in React core.
Agreed; all of complex scheduling solutions using RIC/RAF and priority queueing are probably not needed for 95% of webapps. The primary need is good state management and a good side-effect framework (e.g. hooks, lifecycle methods).
Do you intend for Mithril to support this sort of "incremental" priority work handling?
Snabbdom[0] might be a better one there, it's not quite 33 lines (the core is about 200), but it is intended to be usable and used in production systems, either on its own or underlying a components system or whatnot.
I made a similar implementation as the article describes in 60 LOC which also includes a lite versions redux and hooks. (Uses Snabbdom for diff and update of real dom)
I wonder how well the event binding works. For me this is the main reason to use React because it avoids all the mess that can come from that. Apart from that, React performance is much better than any naive re-rendering solution, on the other hand it's worse than programmatic diffing with native APIs or jQuery...
In what way would you say the event binding here differs from React's? (Genuinely interested).
The main difference from where I'm sitting is that React has a concept of "Component state" - updates to which trigger local redraws. Here you have to manually call:
renderNoughts()
any time you change state, this rerenders the whole mount point.
If I'm honest, React's state stuff has always seemed a little strange to me Would love people to try explain the point to me, is it just a performance related enhancement?
I've never deep-dived into that but it's possible to declare components as pure. In that case, as least in theory, React is able to only perform the absolute minimum amount of operations. [1] On the other hand, about binding, the worst-case scenario is that after an update onclick handlers stop working or memory leaks start emerging. It's easy to run into these problems when working for instance with older Frameworks like Backbone.js, which register click handlers procedurally but don't manage the bindings - automatically - during the whole life cycle. Also I've observed such problems with one of the minimalistic React clones, I think it was Preact or Inferno.
React's state stuff is just a really basic, batteries-included solution that works for basic things but doesn't really scale. There's nothing special about it in terms of performance. Once you get beyond a certain level of complexity in your app, and need to bring in richer state management, React's mechanisms become dead weight.
The one exception being contexts. Contexts allow you to hook into react's virtual DOM and "talk about" a subtree of it instead of just the very next layer down. This is something that an external library can't replicate, so it's more of a fundamental primitive than setState and hooks are.
Author here, nice to see this got some interest! As a very small benchmark (uh-oh!), in a console on the linked page, I tried to make/recreate 10,000 divs:
On my laptop (admittedly a fairly new macbook), I got (approx):
130ms to make divs from scratch
80ms to switch from 'hi'->'bye'
50ms to re-render with no changes
I'd be super interested as to what comparable figures would be for React. There's a fair bit of interesting discussion below surrounding performance, but with no datapoints.
50ms to rerender with no changes is quite slow. One frame is 16ms. Being able to rerender part of the render tree in ones of milliseconds is critical for most reasonable frameworks.
What I really like about this is it removes some of the feeling of magic around react and similar frameworks. Yes there's a lot more going on in real react, but the core idea is simple and you can build your own and really learn what's going on at the same time. Well done! Bookmarked!
This looks interesting! Not a working replacement for React, right? I would probably add that for optimal performance, don’t use functional differential rendering at all. Keep state and use direct DOM manipulation of only elements that change, right? The very idea of re-rendering by throwing a whole new page up and letting the system diff and update is guaranteed not to be optimal performance, it’s always doing more work than necessary. The tradeoff is that the optimally performant solutions are imperative and stateful and involve all the difficult and bug-prone techniques everyone’s trying to avoid. Engineering for functionality is a solved problem. Engineering for performance is not.
It has most of React's benefits, while being extremely lightweight and fully grokkable (no hidden parts, no magic, no framework logic) to any developer.
I'm not sure if you're the author, but in case you are I just want to say well done on the tutorial! I really like the way it's built up and not only shows the pattern, but also the why behind the pattern. I will definitely give this a try with Mithril.
> This function makes a move in the game, it takes 'x' or 'o' along with 2 integer coordinates. It will mutate all the state variables to reflect the new state of the game. After that, it calls renderNoughts(), this is a call to rerender the game - but we'll come back to that.
Dear programmers, not related to the actual article but, when you have to describe your function with a paragraph of text, please consider improving the naming of your function and its parameters.
Sometimes the comments are only required because the code is confusing.
In this case the function both mutates the state, and renders the app. If using a framework like React/Vue etc it might make perfect sense to call this function `move`, but here it has a lot more responsibility and would need a more representative name like update/setState/renderApp/etc. But I think it's pointless to debate as it breaks the single-responsibility principle and you're unlikely to find something like this in a large scale app anyway.
I'm guessing diffing and updating the DOM is really inefficient if there's a lot of DOM elements and a few frequently updating part of your UI?
e.g. if you added a clock to the game in the article that updates every few fractions of a section to show something like "14.3 seconds taken so far".
The solution to this would be to make the clock into its own tiny self-contained component and to make diffing smarter so that only the DOM parts that belong to the clock need to be diffed/checked each clock update?
You would think that diffing is inefficient, but apparently modifying the DOM is significantly more inefficient. Mithril -- the framework that inspired this example -- has an interesting take: https://mithril.js.org/vnodes.html
I think he means that this snippet of code doesn’t account for diffing like React does and would fail if scaled out. I think the OP is aware of that though.
I don't know how React does it so it was a genuine question. I'm assuming they do some smart diffing to be more efficient.
It'll be irrelevant for most apps but I'm curious how much less efficient all this is compared to (in CPU time, not developer time) updating the DOM directly e.g with:
Mithril's approach (as robertakarobin refers to) is that diffing VDOMs is cheap (where as updating the DOM itself is expensive), the "library" in this post takes the same approach.
It's interesting, there's a lot of chat about performance with these frameworks, yet I'm not sure how much is relevant to the real world. When I do eg a
document.querySelectorAll('*')
on airbnb map view (I guess a pretty good example of a mid complexity SPA), there are 3407 DOM elements - doing a diff on the VDOM elements of these should quick.
If I were doing something like a clock on a page (or an animation for example), I'd probably just do it out-of-band of my framework. These kind of things tend to be few and far between anyways.
That's why I prefer the Svelte way of doing things. It compiles down to code that directly changes the DOM, but only the parts that need to be updated.
This[0] (2-part blog entry) is a good write-up of what's going on, by a svelte contributor.
Also, as suggested by the author, taking the tutorial[1] is useful (and the repl generally) to see what's going on. In the repl, look at the "JS output" tab. Play around with it to see what the compiler outputs.
I searched for "preact" in the thread, since I'm a big fan. Its small size and compatibility with React API make it suitable for use in "embedded widget" type situations, where a dependency on React would be too heavy. It's also perfectly suited for building full apps, or static sites. I've been using Preact on various projects for years, I love it.
My feeling is that if html and js had syntactically shared data structures at their conception, we wouldn't have quite so much frontend mess as we do now and declarative UI would have been the norm for a while. Having said that, I'm not sure how much precedent this has in UI frameworks outside of the web.
I don't interpret "html and js had syntactically shared data structures" to mean what you wrote, unless you also replace all other JS data structures (arrays and dicts and ...) with <> syntax. You've made it more concise but it's still distinct.
For an example of a system where the programming language and document generator share data structures, look at Hiccup (and Reagent). One of their very first examples is putting a loop inside a list. There's no special API for the attributes -- it's just an ordinary map. There's no special API for the children -- it's just an ordinary vector.
The first one is imperative and locks the implementation to use the Document object and the createElement function.
The second one is declarative and leaves it up to the receiver to handle however they seem fit.
I'm not arguing one is better or worse, or that we would be better off if we we're using the JSX way with native support in the browsers, just explaining how I see the differences between the two.
Nice! Explorations like this are truly useful for understanding and maybe even for innovation. You might figure out new ways of expressing things that are unlike react, but solve particular problems better.
As a next step I'd recommend trying to add component-local state connected to the component's lifetime. This is essential when building larger applications out of abstract building blocks that hide inner workings. I expect this to be a nontrivial addition, but I might be wrong!
I'm not sure I can quite express what I'm thinking, but do you think this could be achieved orthoganally to the rendering library itself? (Just by "normal" js).
This is probably a naive question, coming from someone who has written very little JavaScript:
Why do we need React? Why can’t the browser do the same job? Instead of passing a virtual dom to react, and letting it ‘diff/patch’ the real dom, why can’t code regenerate the dom directly, in a similar way? This would seem to save both time, memory and effort, since the browser can do the diff/patch itself, much more efficiently?
Because the DOM is a very specific style of API which is both extremely procedural and extremely heavy, your average DOM element is full of cross-links to other elements, so generating a DOM is both a pain in the ass and quite expensive. It's also extremely stateful, so while you could generate a new DOM and replace the old one you'd lose all information in flight e.g. focus.
Browsers might be able to provide a lighter vdom layer but… well first I'm not convinced the gain would be that big, and second what would the API be? It's not like there's a clear winner that has emerged, react is quite popular but hardly the only contender, and react has kept evolving its API so the sort of stability you'd want enshrined in a browser isn't exactly there.
Because browsers were initially meant for another job: displaying static, declarative documents.
Slowly, they gained more and more features and performance until people started creating single-page apps, where you need to re-render on state changes all the time.
So people invented the best ways they could do so within the boundaries of the browser: the diff/patch approach, in order to work around the performance issues of the document model.
And in some time, perhaps browsers end up standardizing a way to do this natively. Or perhaps another app-model rather than doc-model (doubtful). They already tried to start with templates and whatnot, somewhat unsuccessfully.
In summary: the document model (and browsers) were not designed for that. The Web is a mess of features and features on top of features that has ended up with roughly a secondary operating system on top of a real operating system. The only (but critical) advantage of this model is that it is a de facto standard.
The big advantage of a library like React is that you can specify your UI declaratively once, instead of having to specify imperatively how to handle every possible state transition. This can dramatically reduce the number of different rendering cases you have to write if you have a lot of state to manage.
All the virtual DOM stuff is just an implementation detail that makes the declarative stuff fast enough to use in production. A more naive implementation that rebuilt your entire (real) DOM on every state change would not be fast enough for anything but the most simple applications, because DOM updates in browsers are currently a relatively slow operation.
Browsers could provide an API to more efficiently update large sections of the DOM at once, using a React-style diff algorithm or otherwise, but currently that isn't a feature they offer. If they did, you'd be correct that there would be less need for libraries like React.
I'm not sure this is a great argument. Web standards and browser implementations evolve partly through absorbing ideas that lots of people are using anyway. A decade ago, no-one was using CSS variables or HTML media elements, but lots of people were using CSS preprocessors and Flash video players.
"Web browser" is not a programming language. It's practically a complete operating system, and they have not in the past shown much reluctance to add features to that big ball of mud, when it helps programmers.
It seems like `m.update` function queries the current DOM state.
I'm interested in the performance implications of this. From what I know, other vdom libraries maintain the current DOM state in-memory for efficient computation of what should be updated.
No doubt that this is much simpler and ought to be at least more efficient than re-drawing everything. How much performance do you lose due to querying the DOM state?
In the source code to perform el.classList = v.classes the author uses:
for (const name of v.classes)
if (!el.classList.contains(name)) el.classList.add(name)
for (const name of el.classList)
if (!v.classes.includes(name)) el.classList.remove(name)
Why the standard decided to make classList read-only?
Could ele.className=(v.classes).join(" ") be a valid and performant solution?, perhaps is to avoid the string to token traslation for performance reasons?, then why don't they include a classList.set method?
> Why is classList read-only, I can think of any valid reason.
Because it's a live DOMTokenList, not a computed property. That is if you keep a reference to a classList and mutate the className parameter, classList will reflect the changes.
classList could also be a computed property which, when assigned to, clears the underlying token list and adds all elements but I'm guessing the developers of the API saw this as unnecessary complexity given you can do the same thing by setting className.
The point of classList is the ability to more easily check for and toggle individual classes, if you don't need that capability you just don't use classList.
> Perhaps ele.className=(v.classes).join(" ") is a valid solution?
I’m not sure this is the right place, but... I see a potential subtle bug there too: it doesn’t seem at first glance that the classes get added in the same order as they’re specified in the virtual element? If I made div.bar and then changed it to div.foo.bar, properties of .foo could override properties of .bar because it looks like it would end up with classList [“bar”, “foo”] instead of [“foo”, “bar”]? Maybe?
Edit: please don’t take this as criticism. I love the project and think it’s awesome! That’s why I took the time to read through the source and grok it!
> If I made div.bar and then changed it to div.foo.bar, properties of .foo could override properties of .bar because it looks like it would end up with classList [“bar”, “foo”] instead of [“foo”, “bar”]? Maybe?
What properties are you talking about here? Despite the name, classes are really an unordered set, the order of classes on the element should not matter: when CSS gets applied, properties are prioritised based on the most specific rule, then the latest rule. That is the prioritisation of CSS properties should depend entirely on the CSS and not in any way on the order of the class attribute / className property.
So let’s say .foo sets “padding: 8px;” and .bar sets “padding: 4px;”. It’s been a while, but I’m pretty sure that <div class=“foo bar”> ends up with 4px padding, and <div class=“bar foo”> ends up with 8px padding. I could be wrong though and will test it out when I’m back in front of a real computer.
Edit: Woah... It looks like I am incorrect. Nifty! I'm amazed I have never been burned by that before!
Edit 2: I suspect the reason I've never been burned by that before is because overrides like that have generally been applied over top of some kind of generic CSS file and the generic one was loaded first; since whichever rule is declared later is considered to have precedence, the override file wins over the generic file. My mind is kind of blown right now that over 20 years of web development I have never encountered this problem!
Agreed, let me know if you find a better way! I feel if the browser APIs were slightly more declaratively inclined, this article could be "10 line React".
Why not just be descriptive and call it `const grid`? It's like we're all playing some game to be as terse and obscure as possible with variable names in all languages.
>> if you change the state, it runs the function again, this returns a new virtual DOM
What do you mean by "change the state"? How do you do that? By calling some function, or perhaps by assigning a value to some property of a state-object? OR by calling the function again with a different state-object?
I have no problem with abbreviations over a limited scope but
looking at this code[1] I can't figure out what the author meant
by 'm' and that makes me wonder if the author does know what he's
doing.
So many code examples have things like this and worse.
One I saw the other day had about 10 variables created in one part and then these were all placed into another object that had the same names as all the variables.
You also see a lot of examples with loads of other irrelevant code in them.
If that grid was going to be used in a single loop or something I may write it like that but any larger scope and it would definitely have a descriptive name.
For small functions / blocks and descriptive types I’m ok with it in Go BUT I always use descriptive names anyway. So much easier to keep context in my head.
I thought the whole single-char-var thing was for very short-lived variables.
I very much prefer
for c in internalProductCategories
p = fetchProductsInCategory(c.id)
m = getAccountManagerName(c.id)
results.append({categoryName: c.name, products: p, manager: m.name})
to
for internalProductCategory in internalProductCategories
products = fetchProductsInCategory(internalProductCategory.id)
manager = getAccountManagerName(internalProductCategory.id)
results.append({categoryName: internalProductCategory.name, products: products, manager: manager.name})
They're both clear, neither needs comments, but one is unnecessarily verbose.
Isn't this the standard argument for making names more specific as the scope where they are visible gets larger?
Something only used within a short function, so it only ever appears within a few lines of where it's defined, can probably be very short or even a single letter, and that conciseness can be beneficial.
Something used elsewhere in a file should probably have a more descriptive name, so it can be easily distinguished and located.
Something exported from a module and used elsewhere in the program should probably have both the more descriptive element and then, one way or another, some further way to identify that it's the version from that particular module that is being used.
I get what the argument is saying, but there might still be times where I want to find every mention of a variable within a 3-line block. If that variable is a single letter, and possibly a letter that's used 10-20 times in the code in that block, I would consider that block confusing.
Ultimately, I don't think there's any hard rule or standard, just an evaluation of how easy it is to understand the code. Sometimes a single-letter variable will be fine, sometimes not.
But I'd bet a 4-letter variable will always lead to code that is easier to understand.
Put a comment block at eof defining them and their usage. Use these variables in such a way that ctrl,f replace all with a descriptive name doesn't break anything except the line count and pretty print.
If you want to get really clever, write a script that automatically renames everything and reflows the file on commit.
> "If I can't search the file for this variable and find it easily, it's a bad name"
Like parent mentions, this is about scoping. My rule about short variables is that I have to see their _full_ use (including declaration) in only a few lines on the screen.
Personally I prefer single letter variables especially in lambdas for consistency (x, y, z).
Just that the shorter names make for a more concise presentation, which in turn is quicker to scan than having to see and parse all the long names to reach the same conclusion.
I'm not sure this is "ordinary practice", especially as `nan` often refers to "not a number" as an error category.
Typing full words doesn't take that much longer than acronyms, and results in more readable code (although this is well enough established at this point that I don't think I need to go over it).
Bugs are ordinary practice. Importing naughts and crosses is not. I giggled mischeviously when I wrote it. Particularly since it looked Pythonic. YMMV.
> The programmer sets up the desired appearance of each window, then tells the curses package to update the screen. The library determines a minimal set of changes that are needed to update the display and then executes these using the terminal's specific capabilities and control sequences.
The submitted title was "React in 33 lines". We've since changed it. This is a case where rewriting the title did the article a disservice. To my ear there is a subtle but significant difference between that and "33 line React", which is what the author actually said.
"React in 33 lines" sounds reductionist. With the actual title, there's an implicit "a" (i.e. an indefinite article) at the start, i.e. "A 33-line React". In other words: a short exercise exploring the core idea of React. Not a claim of total equivalence.
Submitters: please read the site guidelines. They include "Please use the original title, unless it is misleading or linkbait; don't editorialize." Much of the time, when people rewrite a title, they make it more misleading or linkbait, even if they didn't mean to.
Not all applications have a requirement around "necessary optimizations". Sometimes if the application is simple enough, something like this submission could be good enough.
Or the constraints could be different where the bundle size vs performance implications could be worth trading.
You seem to be talking about something else. We're talking about how this library is not React in 33 lines. It could be better labelled as a "VDOM framework in 33 lines".
We all hate new syntax that we feel isn't "samey" enough to what we already know. That doesn't mean it's gross, it just means you learned something about your own preconceptions of what you thought JS was supposed to look and feel like.
Thankfully, most of the time we learn that's just us, and move past that. Arrow functions have been around for nearly five years now: they're fine. You learn that they're just part of JS, and you see them everywhere in modern code, just with normal indentation and sensible white spacing to keep them perfectly readable (because minification will solve the "the code as written is too verbose" part of the dev cycle).
The important thing is what they do: bind declaration context as execution context, so `this` keeps meaning the same thing, which drastically simplifies a lot code.
In this case, there is simply no reason to use arrow functions at all, because we don't need to preserve the declaration context (there is no `this` that needs to be preserved), so if this were real code you'd probably use a normal function, and your comment should probably be "why not this?"
function makeCell(value, i, j) {
function onclick() {
playMove(value, i, j);
}
return build('button.cell', { onclick }, value);
}
Or even, "actually, why not this?"
/**
* ...
*/
function clickCell(evt) {
const t = evt.target,
d = t.dataset;
playMove(t.textContent, d.i, d.j);
}
/**
* ...
*/
function makeCell(value, i, j) {
const e = build('button.cell', value);
e.setData({ i, j });
e.click(clickCell);
return e;
}
To which the answer of course is "this isn't production code, arrow functions are fine. I mean come on, it's just a fun little JS exercise, stop trying to turn fun into work".
I don't use javascript regularly so I have no notice of "samey". But I've been exposed to enough of a variety of programming languages from APL to C to Rust to Scala that I have a pretty good idea when some construct looks awkward or unergonomic. ES6 looks like a nightmare. Even compared to the nightmare that is Scala. And remember, Javascript's roots are scheme.
Here's the same thing in clojurescript. Tighter, balanced and no chaining of symbols.
(defn Cell [value i j]
(m 'button.cell' {:onclick #(move value, i, j)}))
I've never used clojurescript (but have used perl, php, java, prolog, oocaml, js, python, ...), so to me that: looks like a clusterfuck.
But of course, if I had a need where clojurescript was the best fit, and I had to use it daily for an appreciable amount of time, I'd acclimatize to it and consider this perfectly fine syntax. The same goes for ES6: it's objectively just syntax. If you think it's a nightmare, that's because you're the one doing the judging. Not because it is.
If anyone wants to take this idea to another level, I wrote a post showing how to do the same but in 10 times more lines of code: https://pomb.us/build-your-own-react/