Learn how modern JavaScript frameworks work by building one
nolanlawson.com
nolanlawson.com
> To grossly oversimplify things: React assumes that your entire virtual DOM tree needs to be rebuilt from scratch, and the only way to prevent these updates is to implement useMemo
Not quite, on a state update, it rebuilds the component that was updated and all of its children. Not the entire virtual DOM; old versions of Angular did this, but it was wasteful.
useMemo doesn't prevent that, but React.memo can (useMemo has a different role; it lets you choose when to recompute or recreate a normal JavaScript object. But on its own it won't stop rerendering of child components!) [0]
This invalidates some of their assumptions. The reason why React isn't "push-only" isn't because it does that, it's because it sometimes buffers updates instead of always pushing them immediately. In fact, other frameworks like ~~Svelte also aren't "push-only" and hence not strictly reactive~~! [edit: this is no longer true after Svelte v5, see discussion below] (Funnily enough, OP uses an article as a source that explains this correctly [1], but it seems they took the wrong lesson from it).
The reason why signals are so cool is because the framework knows for any given state change which exact attributes in the DOM need to be re-rendered, even more specifically than "the element and all its children". But this neither implies reactivity nor the other way around. The two concepts are orthogonal.
Anyways, kudos to the author for diving into this so deeply!
[0] useMemo is useful in combination with React.memo sometimes, as the latter compares objects shallowly/by reference instead of their contents, so useMemo can be used to only recreate shallow references if its contents changed. You could probably also reimplement React.memo with useMemo, but you probably shouldn't. [1] https://dev.to/this-is-learning/how-react-isn-t-reactive-and...
I did indeed mix up `useMemo` and `React.memo` – fixed it in the post.
You're right, I am skipping a lot of details (hence "to grossly oversimplify"). I know that React doesn't invalidate the whole tree, but it does in the worst case. Maybe I should add a note about that.
Svelte not being truly reactive makes perfect sense, but in Svelte v5 my understanding is that "runes mode" does exactly that. This is what I mean by "moving in that direction."
let count = $state(0);
function increment() {
count += 1;
console.log(count + " + 1 = " + countPlusOne); // prints "1 + 1 = 2"
}
let countPlusOne = $derived(count + 1);`<button on:click={() => increment()}>+</button>`
What's the definition of reactive is I think my question and having a clear demo is useful - if I understood it it looks a useful piece code
You mean *invalidate the whole subtree, right?
You can definitely use useMemo with JSX elements to prevent child components from being re-rendered too often.
There's an example right in the React Hooks FAQ where it reads: "Conveniently, useMemo also lets you skip an expensive re-render of a child"
https://legacy.reactjs.org/docs/hooks-faq.html#how-to-memoiz...
AFAIK there's no magic to React.memo. It's basically a shorthand for useMemo that takes the props as the dependency.
Pedantic note: this isn't quite true. memo() also allows a second `arePropsEqual` argument that useMemo doesn't have. Also, memo() compares individual prop values, while useMemo() can only look at the whole props object (which would be "fresh" on every render -- it's a diffferent object, even if it has the same values). So it's not like you can easily reimplement memo() via useMemo(). But of course, conceptually they are pretty close :)
Passing “Object.values(childProps)” as the dependency array for useMemo should do the same thing.
But yeah, there are good reasons to use React.memo for convenience with props. It’s not fundamentally different though, and you can definitely useMemo() for caching components when more convenient.
Only if those child components are memoized. By default, whenever the state of a component changes, React will rerender the entire subtree. The only time it doesn't is when a child component is memoized (React.memo) AND the props haven't changed. Utilizing useMemo and useCallback is how we prevent non-primitive props from being recreated unnecessarily
useMemo() works for memoizing child elements. I linked to the React docs showing this.
- https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-...
I used this to build Svekyll, a Jekyll clone (the original static blog tool).
https://extrastatic.dev/svekyll/svekyll-cli
Not to toot my own horn, but I'm really proud of it. Svekyll scores all 100s with lighthouse but still has all the cool things you get from a Svelte app. It's a true single page app, all JS is inlined and can be put on any web server for hosting. Plus, a bunch of other cool things that only are possible with a native JS blog.
Both Svelte and React are shifting to SSR-first, which is what Vercel can make money with, I read somewhere Vercel had many React core members now, after it bought out Svelte.
What goes around, comes around. It has always felt like the front-end frameworks, from things like Backbone to React always failed to learn the lessons of history. Preoccupied with being "new" in an area that's rapidly gaining capabilities via the browser.
Re-inventing the wheel isn't difficult. Improving it is.
I recently used a Windows 10 machine after years and I suggest you try it. It could not be more evident that Microsoft is fully betting on Web and Cloud. Windows is increasingly a thin client. Even the mail app isn’t an actual mail client now.
I just try different things in vanilla depending on the load. In terms of speed nothing can beat just serving a page rendered on the server. If you really need dynamic updates replacing dom nodes is good up to some very limited number, virtual dom and cloned nodes increase this number by a tiny irrelevant amount. If you need to update more than 100 nodes, from what I've tested, nothing beats replacing the parent node content with a html string. Sometimes inline onclick="" handlers are great compared to creating 1000 listeners one by one. Sometimes you put the listener on the parent and figure out what was clicked when it happens. I've even had cases where iframes are wonderful. At times I also put some or many hidden nodes in the html document and display them when needed.
Writing this I'm curious what the performance is for <output>....
https://jsfiddle.net/4ajzcfw9/
(Didn't feel like doing it right)
I'm 50% convinced there's a Chesterton's fence I'm mising but where?
At least that were the reasons I saw when I was thinking about the same. If anyone has ideas around it, please get in touch.
The default paths should be pretty easy to analyze as long as you pick sensible defaults.
1. the code you write must parse as valid JS
2. you must be able to write traditional JS
3. we want the equivalent of the Destiny Operator https://paulstovell.com/reactive-programming/
And, as a bonus I'll share all the warts with you! It's not perfect but I love blogging with it.
I don't disagree about your typescript point. Svelte community seems to have aligned around typescript being a challenge and I do like their assertion that jsdoc+eslint is a better approach.
For an intro from the OG, consider this 6 minute video from Solid [0] or this blog post [1] by Solid's author.
0: https://www.youtube.com/watch?v=cELFZQAMdhQ
1: https://dev.to/ryansolid/building-a-reactive-library-from-sc...
No abstraction, no nonstandard HTML tags, no <template>, no Proxy, no master class trying to figure out what part of the DOM should or shouldn't be redrawn based on inbound data. Every component should be autonomous, every screen should be able to destroy or resurrect its own components. If you need a central data cache, put that on the ping and let every component deal with it on the event firing.
[edit] I've built and maintained two frameworks, one for websites and one for single page apps, rewritten and improved over 20 years, originally in PHP, now in Nodejs. The main guiding principle for me has always been decoupling design from code.
It probably works just fine, but gets cumbersome if you want to know exactly where a piece of state is managed or the order of event processing is important for some reason.
- Incremental computing - https://en.wikipedia.org/wiki/Incremental_computing
- Self-Adjusting Computation (Umut A. Acar) - https://www.cs.cmu.edu/~rwh/students/acar.pdf
- Introducing incremental (JaneStreet) - https://blog.janestreet.com/introducing-incremental/
- Incremental computation and the web (JaneStreet) - https://blog.janestreet.com/incrementality-and-the-web/
- Self Adjusting DOM (JaneStreet) - https://blog.janestreet.com/self-adjusting-dom/
- Self Adjusting DOM and Diffable Data (JaneStreet) - https://blog.janestreet.com/self-adjusting-dom-and-diffable-...
- Incremental Computation (Draft of part 1) (Rado Kirov) - https://rkirov.github.io/posts/incremental_computation/
- Incremental Computation (Draft of part 2) (Rado Kirov) - https://rkirov.github.io/posts/incremental_computation_2/
- Incremental Computation (Draft of part 3) (Rado Kirov) - https://rkirov.github.io/posts/incremental_computation_3/
- Towards a unified theory of reactive UI (Raph Levien) - https://raphlinus.github.io/ui/druid/2019/11/22/reactive-ui....
In my opinion, one of the most interesting ideas to explore in this problem space is a hybrid solution: differential dataflow[1][2](model) + self-adjusting computations(view-model + view).
At this point I can't stop myself from pointing out that the underlying reactivity/diffing system is rarely what makes an application slow. I've heard the creator of XState and Stately [1] say that React's vdom is not fast enough for updating edges in their state chart in real time without lots of optimisations and I believe him. It's just that most people don't encounter such issues and spend adding a dozen tracking scripts that run before the actual application does.
0 - https://vuejs.org/guide/extras/reactivity-in-depth.html#conn...
Is all this pain and complexity really worth the gain? In practice it often leads to a slower experience for users due to the massive globs of js.
In a nutshell: we had a 7 year old server side rendered page on our site that shows a chat between two users that was more like email. So you have to refresh to see new messages. We added 2 lines of code to the HTML(!!) and now the page is fully multiplayer so messages come in directly when the other user sends one and you see other things update as they are changed in the database, etc.
2 lines of fucking HTML.
Sure turbo is built in JavaScript, but I have to spend zero time writing any and I get all the benefits in a 7 year old server side rendered page.
Minimal html, like just a couple of lines including regular html boilerplate.
The amazing thing of libraries like turbo or htmx is the client complexity does not increase with feature complexity.
The one I made is https://mutraction.dev/ The name is a portmanteau of mutation tracking.
I've yet to see any framework have O(1) collection mutation reactivity.
For mutations that change the length, extra elements are built or removed at the end. Assignment straight to an index (that doesn't change length) is able to use normal object property semantics.
arr.push(e) and arr[i] = e are both O(1). But not all array operations achieve this.
I have some vague ideas about implementing efficient .splice() calls. Right now knowledge about splicing semantics is lost in between the proxy layer and `ForEach` DOM node array. But I have a bunch of other plans to do first.
I am curious, to find the sweet area of maintainability + performance.
Is the problem usually latency ? If you load too many items into a grid view you get performance problems.
Maybe this problem is solved already - Facebook solved it - I would like to be able to create a "weak iterator" that handles forward and back navigation of collections with extremely fast rendering when moved backwards or forwards.
Computers are fast, I like the ideas of immediate mode but they burn CPU.
Is there a framework/library that supports the usage of an effect-system when it comes to rendering actions?
For instance, in react, a component (or rather it's render-function) has to return the element(s) directly. Is there a framework where the render-function accepts something effect-like or promise-like instead, even if that means that the rendering might potentially be delayed?
function MyComponent() {
const promise = ...;
const result = use(promise); // use is like await
// do something
}
function Wrapper() {
return (
<Suspense fallback={<Loading />}>
<MyComponent />
</Suspense>
);
}
You don't need to know this to use it, but the implementation is both interesting and horrifying: The `use` hook checks if the Promise has resolved, and if not, it throws an error that is caught by the Suspense boundary. And because there is no property like `hasResolved` on JS promises, `use` adds one itself. (At least this was how an early draft proposed it, a lot of changes have been done since, and my knowledge might be out of date.)Yeah, the implementation under the hood seems a bit crazy. Reminds me of how I found out that angular used to call toString on functions to get the parameter names for dependency injection.
But honestly, if I can use it without problems as a user, that's what I care most, even if the backend developer in me is horrified about it, haha. But I guess that is mostly Javascripts fault after all.
The decision to avoid a custom compiler is responsible for almost all oddities and downsides of React.
I've made my own though: https://github.com/marcellerusu/capable-js.
Its not for use but it was an interesting experience that enables a lot of new patterns by using generators.
I don't claim that it is better than other frameworks though, there's a lot of times where this pattern is significantly more cumbersome than just using react.
I think you should join the react developer's team. ;-)
I think this should be "...with a lot of React experience".
Here's one example app: https://github.com/WorldMaker/compradprog/blob/main/main.tsx
One of the obvious differences is that I'm still using TSX, but it is very different from React, it just looks a lot like React at first glance.
Also, because I was doing it across at least a couple of projects, I started it from the beginning as its own small framework and have been trying to document it: https://github.com/WorldMaker/butterfloat/tree/main
It's still very much in early "prerelease" stages, but feedback is welcome.
I suppose, if which ever choice is doing the trick - keep it while practicable
Oh come on! The article mainly mentions the specific frameworks it does to qualify and contextualize the set of features it discusses, which it goes on to implement. The article is excellent, and this kind of reflexive dismissal is so tiresome.
The walkthrough on how to build a JS framework from scratch is "hypester" because in the short section that mentions existing frameworks, your pet frameworks weren't included?
(I mainly work on native desktop apps, but can use JS if needed.)
Obviously a lot of these techniques are pretty novel, and maybe they won't stand the test of time. Or maybe a new browser standard will make them obsolete eventually. But for now these seem to be the current wave anyway.
I have good hopes for some disruptions to arrive on the web via wasm. There are some interesting things happening in that space that are increasingly less about Javascript, dom trees, css, and all the limitations that come with those and more about leveling the playing field with mobile where UIs are more competitive and non Javascript frameworks seem to be preferred over the poor man's choice (aka. web based).
I'm saying that as somebody actually pushing web based on mobile (we are about to release a PWA). Just acknowledging the reality that web-based is still considered a huge compromise on ux, performance, and capabilities on mobile. Good enough is the best you can say about it. Some of those mobile frameworks (flutter, compose web, and others) are now coming to the web via wasm. IMHO, there are a lot more interesting things that could be done in that space in the next years.