12 ms·
Crank.js – Write JSX-driven components with functions, promises and generators
- madjam002 6y agoThis looks really interesting. I love the use of generators! Currently using redux saga so something like this would tie in very nicely.
- migueloller 6y agoHere’s [1] a blog post introducing Crank.js. It goes into detail as to why the library exists. The main motivator for the creator seems to be that they really disliked the suspense API and wanted to offer a better way using JS generators and async/await. [1] https://crank.js.org/blog/introducing-crank https://crank.js.org/blog/introducing-crank
- ethanwillis 6y agoHoly hell. I didn't realize this. This is nightmare fuel and the blog post you linked is leveling some really good criticism. My favorite part is where he linked to a Github issue where the React team instead of responding with a real answer just repeated their manta "Suspense will solve everything" and closed the issue. I know "open source devs don't owe you anything." But I've noticed that sentiment is now being used in a lot of open source projects, especially the well funded ones by big Corps, to just shutdown anything that derails whatever their underlying motivations are. 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 mechanism
- hombre_fatal 6y agoWell, I don't think this is fair, either. As someone points out in the thread before making some good points to OP, this topic is discussed ad nauseam in React's github issues and elsewhere. There 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.
- earthboundkid 6y agoReally great origin story for either a JS framework or a Batman villain…
- lhorie 6y agoDan Abramov responded to this on r/javascript and mentioned that the rationale for Suspense wasn't so much to chase some academic concept of purity (and shoehorning caching in to the system to make it happen), but rather that Suspense was there to support a pre-existing need for a cache for cases like snappy back/forward routing. 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/ https://overreacted.io/algebraic-effects-for-the-rest-of-us/
- k__ 6y agoWasn't one point also that generators and such are contagious?
- lhorie 6y agoasync is contagious, generators are not. 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) }
- lilactown 6y agoI enjoyed reading this blog post, and the perspective that the author brings. The notion that a cache is a big ask is novel to me, but I think it is a good point that it does complicate things more for simpler cases. 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.
- lhorie 6y agoAs an outsider, I honestly feel that there's a strong difference in opinions between Dan and Sebastian. One doesn't need to reason too hard about the purity thing: Dan himself admits hooks were criticized (accurately) for being impure. 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.
- pier25 6y agoAre there any benchmarks somewhere?
- migueloller 6y agoAs I understand it, this is extremely early and shouldn't be used in production, so there are no benchmarks. What's interesting are the ideas of using native JS constructs where React opted for implementing things like hooks, refs, suspense, etc.
- Eric_WVGG 6y agoWhen I read that bit about "No [hooks] are needed" I did a double-take. IMO once you get the hang of hooks, React suddenly starts to look somewhat sensible… but anyway the Crank sample code looks promising.
- leetrout 6y agoI'm learning Ember at work and there's a lot of "plain" JS floating around (async / await, generators). 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!
- markmiro 6y agoI love this. Vjeux, seemed excited about it, which gives the project some immediate credibility IMO. https://twitter.com/Vjeux/status/1250687160237211649 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
- CSSer 6y agoInterestingly, the thread you linked to also has links to other discussions where the React.js team provide their reasoning for why they decided against using generators[0] that the author of Crank.js didn't mention in their post. There's also a more basic breakdown of that explanation deeper in the twitter thread here[1]. As I understand it (and I am no expert), Crank.js's usage of generators is not the most performant because it doesn't provide an avenue to avoid recalcing work. In this case, memoization/caching does nothing to save you. 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-254987818 https://github.com/facebook/react/issues/7942#issuecomment-2... [1]https://twitter.com/swyx/status/1244749594472243200 https://twitter.com/swyx/status/1244749594472243200 (see @samselikoff's reply and following discussion)
- fiddlerwoaroof 6y agoI really like the way redux-sagas uses generators to represent side effects a values that get interpreted by the redux middleware they provide.
- 6y ago
- speajus 6y agoThis is spot on. I've been slowly switching from class based React to hooks based with Suspense, and a couple of things have become super clear. 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.
- difosfor 6y agoI prefer LitElement; no reinventing of the wheel like JSX etc. do and no build required. Plus you get browser supported CSS scoping and life cycle support.
- BenoitEssiambre 6y agoI didn't go full LitElement because I didn't want to add build steps (I'm waiting till they figure out ES6 module path resolution) but I found that the templating/rendering library LitHTML is great and easy to seamlessly and incrementally integrate in any webapp. It's just plain html, it's very fast and it just works. Javascript literal templates are a very elegant solution, turning strings and statements into lists of objects which can be efficiently cached, diffed and recursively traversed a la Lisp. To me JSX felt like a crutch to compensate for browsers being behind the curve. We no longer need a second html.
- arzel 6y agothis is amazing and just what i’ve been looking for. thanks my man!
- thebradbain 6y agoFully on board with this newer trend (Svelte also comes to mind) of using only native JavaScript language features to accomplish what other frameworks do using abstractions. It seems like the natural next step as JS matures, as it’s my opinion that old frameworks that made up for JS’s shortcomings by abstracting them away and designing around the language’s limitations are intrinsically unable to natively and sensibly integrate new native language constructs after the fact without somehow changing the core framework itself (e.g. Suspense to integrate async/await in React). 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.
- WiseWeasel 6y agoHow are React and Vue grouped together in that regard? More specifically, how is Vue less of a framework abstraction than Ember or Angular?
- thebradbain 6y agoIf I remember correctly, React and Vue started off as more view-layers than anything else, and popularized the idea of 1-way, isolated components. Angular and Ember had always been all-in-one-heavy frameworks with 2-way bindings by default, and “partials” similar to components but state was intrinsically shared in a MVC paradigm.
- pier25 6y agoVue in itself is just like React, a rendering library with reactivity. Vue also provides official batteries like Vuex and VueRouter but you do not really need those to use Vue.
- petilon 6y agoAgree. React started out simple, but now is over-complicated. Here's another tiny lib to use JSX in a native JavaScript application. https://github.com/wisercoder/uibuilder 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
- t0astbread 6y agoThis looks very interesting but I have one concern: Wouldn't code where the "view" part and application logic are tightly coupled be way harder to test than an architecture that renders the view in a pure function and does state management somewhere outside entirely? (Which is not what React is today but might correspond to the original idea.)
- bodhi 6y agoThe way the generators appear to work reminds me a lot of Concur: https://github.com/ajnsit/concur-documentation https://github.com/ajnsit/concur-documentation But controlling or understanding the control flow in Crank generator components, ie. when does ‘yield‘ return, looks like a bit of a nightmare!
- leetrout 6y agoWhich part looks like a nightmare to you? I wasn’t quiet sure what you mean by “when does yield return”?
- bodhi 6y ago“Nightmare” is too strong, but it looks from the simple example that generators are managed by jumping between coroutines, and one of the coroutines is the renderer? (Sorry, I’m on my phone and probably not conveying my point very well) I’d be interested in seeing this hooked up to Xstate.
- franciscop 6y agoOkay this is very interesting, I love React but I'm also worried about the "complexity explosion" that is going on. As an example look at this "zombie children" article that has bitten me when trying to build a library: https://kaihao.dev/posts/Stale-props-and-zombie-children-in-Redux 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>; } }
- bikeshaving 6y agoHi! Author here, thanks for the interest. 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!
- franciscop 6y agoI didn't know you could forcefully end a generator from outside. IMHO generators are overkill 99% of the times. This is one of the biggest issues I had with the PREVIOUS Koa.js' documentation[1] and why I decided to launch Server.js with plain async instead of trying Koa. I had been doing JS for ~6 years by then and I could not tell how the simple "hello world" worked. [1] https://koajs.com/ https://koajs.com/ [2] https://serverjs.io/ https://serverjs.io/
- franciscop 6y agoI tried to make a library as a workaround with async treatment in React which I also don't like, but besides it being a library (already a -1) it also didn't allow you to have Hooks inside these async components, which was a showstopper for me. But still a fun experiment! This is how it worked: // 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.
- felixfbecker 6y agoThis is what I wanted React hooks to be like. They are a huge hack and it shows every time you need to call on conditionally. But I guess if React never comes around to this generator idea I’ll never get to use this, because we rely on many React libraries... Maybe crank could support compatibility with React components somehow?
- mattigames 6y agoWhat would be more likely is to use a codemod[0] to convert react libraries into crank, or close enough that only a few further manual changes are needed. [0] https://github.com/facebook/codemod/blob/master/README.md https://github.com/facebook/codemod/blob/master/README.md
- deleted 6y ago[deleted]
- m1sta_ 6y agoI'd love to be able to use this with just a browser side script tag and no build step.
- nojvek 6y agoI don’t get the whole trend of jamming state in functions. hooks use closures for state and if you call hooks in wrong order you get the wrong values back. In this instance, this.refresh is magical. 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.
- TomMarius 6y agoWhile I tend to agree, IMHO your example is off - it is a "state" property because the class is newly instantiated each time a render is going on, and then the property is magically re-populated for you so you can still treat the class like a class, not because "classes are meant to have state" - you would use class properties directly as your state if that was the case. Hooks are exactly the same magic (the state property is populated the same way as the useState hook works), only accessed differently.
- Kkoala 6y ago> the class is newly instantiated each time a render is going on No, it's not? Where do you think the state goes, if the instance doesn't hold it?
- TomMarius 6y agoIt's stored internally in React. The instance of your component does not hold the state, just a reference to the state object which is repopulated each render (just like useState, it's the exact same mechanism), and after the render is done, the instance is destroyed. More here: https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html https://reactjs.org/blog/2015/12/18/react-components-element...
- aabbcc1241 6y agoIt looks like surplus which also support jsx/tsx. Both of them compose a dom element as oppo to vdom object
- crubier 6y agoThis is terrific! I have always wondered why people don’t use async generators more as they are the perfect fit for react like use cases: what is a react component if not “something” that generates new values asynchronously over time? 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!
- mbrock 6y agoI'm thrilled to see this, because I have been experimenting with using async functions that directly render their own DOM trees in between awaits, and I recognize a lot of the sentiment in the "Introducing" blog post, but I didn't have the insight of using generator yields with DOM trees, instead I had async functions that did this.draw(tree). 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 } }
- scanr 6y agoThis looks great. I really like the idea of component changes just being a series of yielded updates. Really tidy API too.
- MatthewPhillips 6y agoI love the blog post. Great to see some people out there that are still questioning the status quo and not simply giving in to appeals of authority. The author saw things they disliked about React and didn't let the hand-wavy reasoning silence those criticisms. Well done
- briantakita 6y agoThe author does not mention solidjs (https://github.com/ryansolid/solid https://github.com/ryansolid/solid) in his apology for yet another js component library blog post. From a cursory glance, solidjs & crank.js seem similar in motivation & api...
- danabramov 6y agoIt's great to see people experimenting with different tradeoffs. One thing I wanted to mention is that I don't believe the author's claim that React sees purity as an end goal is accurate. I wrote my thoughts here, hope they're of some use: https://www.reddit.com/r/javascript/comments/g1zj87/crankjs_an_alternative_to_reactjs_with_built_in/fnkkgg3/ https://www.reddit.com/r/javascript/comments/g1zj87/crankjs_...