Show HN: 33 line React
leontrolski.github.io
leontrolski.github.io
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/
the post source: https://github.com/pomber/site/blob/master/posts/build-your-...
Or I misunderstand or simply missed where the "live-coding together with text" comes in.
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.
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).
https://krausest.github.io/js-framework-benchmark/current.ht...
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.
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.
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.
Do you intend for Mithril to support this sort of "incremental" priority work handling?
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?
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.
[1] https://medium.com/@esamatti/react-js-pure-render-performanc...
https://github.com/irony/pureact
Demo: https://mandatkollen.se which is built in Pureact.
const root = document.getElementById('noughts')
m.render(root, {children: [...Array(10000)].map(_=>m('', {class: ['hi']}, 'hi'))})
m.render(root, {children: [...Array(10000)].map(_=>m('', {class: ['bye']}, 'bye'))})
wrapped with: console.time('a'); ... console.timeEnd('a')
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.https://github.com/krausest/js-framework-benchmark https://localvoid.github.io/uibench/
Or even just something following mithril’s own benchmark suite: https://github.com/MithrilJS/mithril.js/blob/next/performanc...
- jsx
- routing
- performance (maybe?)
- component state management stuff
What else would you include?
For optimal performance, look into differential dataflow: https://github.com/TimelyDataflow/differential-dataflow/issu...
Router (500 lines): https://github.com/Rajeev-K/mvc-router
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.
> 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.
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.
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?
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:
document.querySelector('.timer').textContent = "5 seconds"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.
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.
[0] https://lihautan.com/compile-svelte-in-your-head-part-1/
I've also "re-created React" multiple times before it was actually created.
Diffing somehow always ended up being thr most natural way tondo things in the end.
My selling point in the end were the components
What would be different if
Document.createElement('a')
Would look like <a/>
?The rest would still be imperative code.
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.
var elem = <a href="#test">Test</a>;
over var elem = document.createElement("a");
elem.href = "#test";
elem.innerText = "Test";
jQuery allows one to do something similar to JSX (which is nice): var elem = $("<a href='#test'>Test</a>");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.
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!
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?
Similar questions are:
Why not build rails into ruby?
Why not build Django into python?
Why not build qt into c++?
It’s a programming flow and model, dom diffing is just one part of it.
"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.
Would it be as fast as React? Well it wouldn't need to be, but it well could easily be since it would be implemented in native code.
It could be an extension to the Shadow-DOM API, right?
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.
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.
If reactive data and data binding were solved at the browser level we'd save so many kBs and CPU cycles.
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 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?
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?
let merged = {...obj1, ...obj2}; See (1) which has 2808 votes.
(1) https://stackoverflow.com/questions/171251/how-can-i-merge-p...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?
Yes.
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!
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.
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!
If you are wishing to do something similar yourself it is worth looking at hyperscript source code:
https://umbrellajs.com/ is both smaller and better.
const g = [['', '', ''], ['', '', ''], ['', '', '']] // grid
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.
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.
Just keep it simple please!
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.In general, I follow a rule of "If I can't search the file for this variable and find it easily, it's a bad name".
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.
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.
If you want to get really clever, write a script that automatically renames everything and reflows the file on commit.
Damn it. Now I need to make this...
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).
> results.append({categoryName: c.name, products: p, manager: m.name})
In the second example you would see this:
> results.append({categoryName: internalProductCategory.name, products: products, manager: manager.name})
Do you know what the error would be? Probably that you're assigning the manager's name to a property called `manager` ;)
Technically it's consistent with ordinary practice. e.g.
import naughtsAndCrosses as nanTyping 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).
Code should be stupidly obvious so that reading it consumes as less cognitive resources as possible.
[1]: https://github.com/leontrolski/leontrolski.github.io/blob/ma...
edit: Ok, I should have read the article first. It's probably for https://mithril.js.org/
Lots of the code looks pretty code-golfy - I promise I don't do stuff like this at work, neither should you
Happy to s/g/grid/ - writing the whole exercise felt a bit golfy TBH.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?
Thanks
Or the constraints could be different where the bundle size vs performance implications could be worth trading.
Depends on context :)
The "diff & patch" pattern is super common, e.g. Git and Incremental by Jane Street: https://github.com/janestreet/incremental
> 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.
"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.
As someone who has sort of lost contact with the front-end world, it's nice to see a coding TL;DR.
That's why you can do anything (declarative) in them. You can loop, have conditionals, etc. without having to learn a template syntax.
const Cell = (value, i, j)=>m('button.cell',
{onclick: ()=>move(value, i, j)}, value
)
I can see why people are using things like clojurescript instead.https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Looks weird at first but you get used it pretty quickly. And I gotta say the auto binding thing is awesome.
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".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)}))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.
add = (a, b) => a + b
xs.filter(x => x > 10)
Are so elementary I'm not sure how they can look new to you unless you're new to programming.