Crank.js – Write JSX-driven components with functions, promises and generators
crank.js.org
crank.js.org
I mostly abandoned the effort and any sort of greenfield React development, when I
came to understand what Suspense was and how unwieldy it would have been to
incorporate Suspense into the hooks I had written. As of April 2020, the mechanism
behind Suspense is for components which make async calls to throw a promise while
rendering to indicate that the component is doing something asynchronously.
“Throw” as in the way you would “throw” an error in JavaScript with the throw
operator. In short, React will attempt to render your components, and if a
thenable is thrown in the course of rendering, React will catch it in a special
parent component called Suspense, render a fallback if sufficient time has
elapsed, and then when the promise has fulfilled, attempt to render the component
again. I say “as of April 2020,” because the React team has consistently said the
exact details of the Suspense API might change, and has used this declaration to
preempt any possible criticisms of this mechanismThere are many good discussions online about this and all the different solutions out there with different trade-offs. And the Suspense system is what the React team came up with to solve it.
I don't see how someone wanting to rehash the discussion requires any other answer than "we're solving this with X, it's just not yet available."
This is one of the most common trope of any sort of forum system, github issues included: everyone thinks they're the first person to broach a topic, yet everyone else has seen it 1000s times.
Personally, I'm a bit on the side of Crank's author that I feel that the direction React has been taking wrt async support feels weird. Can't really put into words, but if I had to try, I'd say React's answer to async feels like something getting lost in translation. There are certainly interesting ideas being explored, for example, in Dan's blog post about algebraic effects[1]. But I feel that Crank's approach feels like a more elegant way of "translating" that concept into JS than the whole promise-throwing thing that React seems to be pursuing.
[1] https://overreacted.io/algebraic-effects-for-the-rest-of-us/
const foo = async () => {}
const mustAlsoBeAsync = async () => console.log(await foo())
const gen = function* () {
let n = 10
while (n) yield n--;
}
const doesntNeedToBeGenerator = () => {
for (const n of gen()) console.log(n)
}However, I would like to hear the author's thoughts on Concurrent Mode in general, and how Crank.js addresses the same issues. There's no mention here of time slicing, which is what Concurrent Mode really unlocks. IIUC Suspense was discovered, not invented, after implementing React Fiber and exploring the knock-on effects of being able to restart rendering from a place in the component tree.
React's insistence on rendering being "pure" is not dogma; it is an attribute that unlocks the ability to pause and resume rendering at any node in the component tree... like if an important update from a keyboard press comes in that you need to handle right now. This means that when CPU is scarce, jank can be alleviated by prioritizing user input and other high priority updates.
There's a tweet out there by sebmarkbage that gives a slightly cryptic response to using generators, but the gist that I remember is that because generators are mutable, you don't get the properties you need to arbitrarily pause and resume a component just like it was when you paused rendering it.
The author claims that this is just dogma, but I believe (and the React team believes too) that time slicing provides a measurable benefit to the user experience. This point is contentious still; Rich Harris of Svelte fame claims that _doing less work_ is more important than prioritizing it. But the author here doesn't address this at all, instead claiming architectural simplicity is its own goal.
I'm excited to see alternatives to React come out and generate excitement, and I'm very interested to see where Crank.js continues to evolve. My hope is that in 5 years we'll know way more about all of these questions than we do now, and we'll be able to put those into practice in better frameworks, delivering better applications.
On the other end of the discussion, the claim about impurity preventing prioritization is very weird, because simply going async will shuffle the rest of your rendering further out in the microtask queue, giving the opportunity for event handlers to fire half way through a vdom render pass, effectively prioritizing user input.
The case of user input is particularly relevant here.
Say React starts a render pass for a relatively low-priority update. It walks through the component tree asking components to render themselves, runs out of time on this tick, and pauses the half-calculated render pass. While paused, the user types a key in a text input or clicks a button. That will be treated as a much higher priority update, so React sets aside the half-calculated render tree, does the complete render pass based on the input event, and commits it to the DOM. It can then "rebase" the half-completed low-pri render pass on top of the new tree that resulted from the input update.
If components go kicking off side effects while rendering, whether it be as simple as mutating a React ref object or more complex like an async request, those side effects will likely result in unwanted behavior because they might happen multiple times or happen at the wrong time.
If I were to tell you that in order to use a hypothetical Floober framework, your code must be tail call optimizable or else the framework has undefined behavior, surely you would think the author of Floober framework was insane.
It seems strange, for example, to dismiss async functions on grounds of function coloring when the alternative is coloring functions with a conceptual requirement of "purity". Or to ignore the opportunity for a paradigm shift to something more naturally reactive (e.g. a la s.js) considering how there was already an API shift when going from OOP components to functional + hooks.
The reason this is possible is because Crank is executionally transparent. If you render a component, and that component calls refresh 4 times, the developer can know that the component has been updated 5 times. You get no such guarantees with React, and it will even go out of its way to make sure that you don’t put side-effects in your code by calling your components multiple times in development, which I think is crazy.
Oh you didn't memoize a function, well you just re rendered a FlatList with 500 items in it on an Android phone that cost $0. You need an addon library such as Mobx to really unlock the power of React and make it sane enough to use.
I had the excact opposite experience.
Frameworks like Cycle.js and Callbags are pure and try to emphasize that, but React always priorized practical solutions higher than FP concepts.
In fact, I found the MobX approach with reactivity harder to control performance for because it's triggered by state changes. But when you trigger a lot of state updates it can be really hard to reason about performance because it's not obvious what components will update due to that change in state.
> I would like to hear the author's thoughts on Concurrent Mode in general, and how Crank.js addresses the same issues. There's no mention here of time slicing, which is what Concurrent Mode really unlocks
I think Concurrent Mode is a really interesting project, but I also think that the developer experience of using CM as the React team has defined it is really lacking.
There’s the issue of querying when a specific component (or the whole tree) has finished rendering. With React, rendering is treated as a black box, and this was mostly okay when rendering was synchronous, but people often ask with concurrent mode, how do I know when rendering has finished, locally or globally. The answer the React maintainers would give would be, probably, useEffect hooks, but that’s not enough for me.
Promises are like the Planck constant of asynchrony in javascript; they’re the smallest units of time possible for executing code later and their callbacks have the highest priority. To not have a promise-backed API for CM seems like a step backwards. Even if I were to implement time slicing, I would want it to be promise based. Also, React fiber time slicing comes at a cost, in the sense that you have to check the clock for every unit of work to make sure you don’t exceed your budget. I would want any sort of priority-based it to be done on an opt-in, limited basis. I definitely don’t think every component should be a possible asynchronous breakpoint, that sounds like a nightmare to debug.
I dunno, I need to write a longer, better structured blog post about this stuff, but here’s something to think about: if you want concurrent mode or time slicing or scheduling priorities, the tools to implement it in user space are probably already there with Crank.
You end up with two issues IME with async tasks (rendering being one of them):
- Making everything async - without prioritizations - means that at some point you have simply moved the problem into the scheduler. Just like taking a sequence of sync operations and wrapping them in a promise chain doesn't speed them up, likewise making every render async by itself doesn't speed things up at all (on the contrary it can make things slower).
- Using prioritized async tasks only allows you to prioritize the things which participate. If a component renders, or any other computation occurs, and isn't sent to the scheduler, it can steal work from higher priority things because the scheduler doesn't control it.
This is why IME a la carte async async rendering isn't super useful. It is a lot of mental overhead (and potentially laborious) to manage your application's schedule while also trying to build features. It would be interesting to have tools to tune it when necessary (which I assume React will provide in the form of ways to schedule computations at different priorities), but having it opt-in means that the default case is everything will steal work from the things that you are actually marking as high priority!
One hard lesson I’ve learnt after doing 15 years of frontend is to avoid too much magic. As you build more complex things, it should be easy to bring in new devs and they should be able to simulate the system in their heads without expecting too many surprises. Or being laser diligent that a silly mistake could blow things up.
That’s what I like about original react. It was super duper simple idea. Classes are meant to have state. The state object is called state. Render function returns a view of state.
While generators are cool, all of this could be simply achieved with existing paradigms. That’s why I prefer preact over react. It’s way smaller than react, less bloated and equally fast.
No, it's not? Where do you think the state goes, if the instance doesn't hold it?
More here: https://reactjs.org/blog/2015/12/18/react-components-element...
State. Exists.
State exists, so let’s deal with it with the tools that the language provides, instead of pretending it does not exist, just to realize later that it does and jamming as hoc answers to this initial oversight.
A UI component is fundamentally “something that generates values (view objects) asynchronously (over human-scale timeframes)”. That’s the definition of an async generator!
It’s the opposite of all-state fanaticism, where nothing can be done or calculated without first 1. Creating classes, 2. Mutating their states, and often 3. Mutating global state.
And then... magic!
Both kinds of fanaticism are bad, and a middle-ground is probably best. Keep it simple. Use the best tool for the job, etc.
That said, having worked with lots of code deeply inflected with global state, I’m more lenient to accept function-purity fanaticism over all-state fanaticism because at least then you only need to understand this one local oddity, not how it depends on the entirety of the rest of the codebase.
But I have to say Dan Abramov does a good job of explaining why they need that purity here and it makes sense https://www.reddit.com/r/javascript/comments/g1zj87/crankjs_...
The ‘original React’ as you mentioned for example, was modeled in standard ML. There’s no class in the language and it is okay to model the state in function.
And people have to do massive meaningless things like swapping between class and function components every day, because of the introduction and elimination of states.
Except that's not how React works at all. There's no state in the class that lives between renders, it only seems that way due to... magic. The class gets destroyed and created between each render and the state object is automagically recreated with the previous state. You can complain about Hooks but saying they are magical and React classes are not is just wrong.
Calling hooks in wrong order gets the wrong value back? Can you explain this one?
https://overreacted.io/why-do-hooks-rely-on-call-order/
https://github.com/reactjs/rfcs/pull/68#issuecomment-4393148...
Await and async came after in ES7 and can be considered to use yield[1]. So there you go - that's a perfect example of the power of generators. Promises used to be more difficult until they were simplified using generators.
[1] https://stackoverflow.com/questions/36196608/difference-betw...
Yeah, I know sourcemaps and all that, but even sourcemaps workflow is impacted; setting breakpoints can be weird, debuggers are still not perfect.
But what does that have to do with generators? Do you have a specific concern about how Babel "compiles" generator functions using switch? Again, I tend not to look at compiled code, but I think the babel output for a generator function is easy to read.
The magic is "this.render". What is "this" in a function ? If we're storing any sort of state in this, then I expect to see "new <something>()", which creates a new this.
By default functions inherit the this as window, and it's frowned upon to use "this" in pure functions. Crank.js breaks the convention where "this" is magically something else. It will also make it hard to typescript type the function.
So now suppose you have mouse event handlers and key event handlers, when their functions get called, their "this" is something else and you have to bind it via either arrow functions or .bind(this).
So my point is, because of unconventional "this", as a developer I have to keep track in my head what this refers to inside a function because it's implicit. If I am working with a team, I have to carefully explain that "this" is unlike what you think it is, it's magically injected.
In javascript "this" is the source of many bugs, because it can be different things based on context. https://www.javascripttutorial.net/javascript-this/
Angular and Ember eventually gave way to Vue and React once dealing with the framework over the language became too cumbersome, and I would imagine Vue and React will eventually give way to “lighter” frameworks once they too become too bloated.
Vue also provides official batteries like Vuex and VueRouter but you do not really need those to use Vue.
https://github.com/wisercoder/uibuilder
Uibuilder is "close to the metal", so there is no heavy library--especially one that completely changes the programming model of the web--sitting between your code and the browser
Admittedly, react now comes with that API and the previous ways of doing things, and that could be seen as over complication.
https://github.com/Rajeev-K/eureka
I find myself more productive coding directly against DOM APIs, using these tiny libs:
MVC router (500 lines): https://github.com/Rajeev-K/mvc-router/
JSX templating (200 lines): https://github.com/wisercoder/uibuilder
Thanks!
Really weird to ascribe that to Svelte, given that it has its own compiler that introduces non-Javascript syntax...
> So we took a step back and asked ourselves what kind of API would work for us... and realised that the best API is no API at all. We can just use the language. Updating some count value — and all the things that depend on it — should be as simple as `count += 1` over `count.setState(count + 1)`
1) hooks are magical, finicky, abstraction that don't play well with promises. They save code at a cost of understandability, subtlety, and new rules. I wrote a ton of Perl, and fundamentally believe that 1 line is better than 2 lines, weirdly hooks are making me rethink that belief.
2) Throwing promises is super tricky... I just wrote a class based component to cache the results of a hook, so I could wrap it into a function that would throw. I am not proud. 5 layers of abstraction needed where the same thing in a class based structure would be 2.
3) The hard part of UI is and always will be state. React (initially) got it mostly right. Redux was a detour, and this seems a much better way forward.
Best of luck with this, I'll be rooting for you.
https://twitter.com/Vjeux/status/1250687160237211649
I've been using hooks since they came out and doing things with generators and async looks to be more intuitive to me. Instead of a language on top of a language, you just use JS, which has always been what made React great.
I also now have a reason to look for more places to use generators :) I didn't realize they could be so helpful
There are also other interesting tradeoffs, like suddenly making everything a generator as a result (as seen here). It's not that big of a deal to me, personally, but it's not something that everyone in the JS world is necessarily used to either.
Overall, it seems to be about target audience and priorities. Facebook seems to really care about intense performance at scale and they want all of their developers to be able to use their framework with ease. It seems they don't mind if that means requiring a bit of their own 'dogmatic' education as the author put it. Regardless, I agree. This is really interesting stuff!
[0]https://github.com/facebook/react/issues/7942#issuecomment-2... [1]https://twitter.com/swyx/status/1244749594472243200 (see @samselikoff's reply and following discussion)
I’ve been writing exclusively React for 3 years and the majority of react community are ill informed as to the origins of React.
Originally an OCAML implementation, there are clear benefits from pure functions, immutable data structures, and representing views declaratively in a DAG. I am simply restating the motivations and characteristics of React.
Then Redux comes long, inspired by Elm and it’s immutable update cycle, and the React community proceeds to spend the next two years utterly confused as to why anyone would want to use Redux. Followed by an onslaught of “You might not need redux” and “How I built my react app without redux” articles by shortsighted developers who merely reimplement Redux. :facepalm:
Redux-saga is the desired solution to handling async rendering while also staying true to the core properties of React. I’m glad you mentioned it in your comment.
I find the React community to be largely defined by its misunderstanding the core design and goals of React.
I find Crank an interesting and clever application of async generators.
However claiming that the core properties principles of React - functional properties, DAGs, and priority scheduling - are “dogma” is a shameful misunderstanding.
The correct criticism is of the behavior of the React team in their community response and the Suspense API alone - rather than the intrinsic properties of functional user interfaces.
Nonetheless, I am impressed by the technical effort in creating a new async generator API for rendering JSX. This project is no easy undertaking. Sincerely hoping for its future success (and self reflection).
I had a different way of dealing with events. Basically I was seeing an app as a process hierarchy of self-drawing widgets that communicated via channels. I ended up wanting to make my own language to get decent pattern matching and the possibility of preemptible processes...
And for example instead of the Timer example which uses this.refresh in Crank, my version would have something like:
let s = 0
while (true) {
this.draw(<div>{s}</div>)
await sleep(1)
++s
}
Then to deal with buttons, something kind of like: let i = 0
let increase = channel("+")
let decrease = channel("-")
while (true) {
this.draw(
<div>
{i}
<button onclick={increase}>
Increase
</button>
<button onclick={decrease}>
Decrease
</button>
</div>
)
switch (await pick([increase, decrease])) {
case "+": ++i; break
case "-": --i; break
}
}One thing I've really missed is JSX. I wasn't a fan when I first heard of it in... 2014?... but I went in on it and it really does make sense and save time IMO. I've been playing with Mithril after a post here the other day and now I'm going to add this to my list to toy around with!
https://kaihao.dev/posts/Stale-props-and-zombie-children-in-...
Curious about Crank.js, would this timer be valid as well (it works on codesandbox)? How does it stop/unmount?
const delay = t => new Promise(ok => setTimeout(ok, t));
async function* Timer() {
let seconds = 0;
while (true) {
await delay(1000);
seconds++;
yield <div>Seconds: {seconds}</div>;
}
}Your example will just work! The only thing to worry about when using `while (true)` with async generator components, is that Crank will continuously pull values from the generator even if the renderer or a parent isn’t updating it, so if you forget to await something you’ll end up entering an infinite loop (which starves the microtask queue so it’s even worse than regular infinite loops).
> How does it stop/unmount? Generators have this nifty feature where you can stop or `return` it from the outside. What this does is it resumes your generator at the most recent yield, but rather than continuing execution, it “returns” the generator at the point of execution. In other words, it’s almost like it turns your yield into a return statement. This is what breaks out of the `while (true)` and prevents your code from continuing to run when unmounted. The cool thing is that you can also then wrap the loop in a try/finally block, and execute some extra code when the generator is unmounted.
If you have further questions let me know!
[0] https://github.com/facebook/codemod/blob/master/README.md
But controlling or understanding the control flow in Crank generator components, ie. when does ‘yield‘ return, looks like a bit of a nightmare!
I wasn’t quiet sure what you mean by “when does yield return”?
I’d be interested in seeing this hooked up to Xstate.
// Books.js
import React from 'react';
import pray from 'pray';
// Wrap the async component in pray:
export default pray(async () => {
const books = await fetch('/books').then(res => res.json());
return (
<ul>
{books.map(book => <li>{book.title}</li>)}
</ul>
)
});
Would love to see good support for Async in React without the complexity that Suspense seems like it's going to bring.I really hope this idea makes it into the mainstream react community and even blend with it. Let’s call this react 2.0 and start building on it, while keeping it compatible with react 1.x for now!
Well done
What's interesting are the ideas of using native JS constructs where React opted for implementing things like hooks, refs, suspense, etc.