BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Crank, a New Front-End Framework with Baked-In Asynchronous Rendering - Q&A with Brian Kim

Crank, a New Front-End Framework with Baked-In Asynchronous Rendering - Q&A with Brian Kim

Leia em Português

This item in japanese

Key Takeaways

  • Major front-end frameworks such as React are getting more complex as they continue to add features. The added complexity is visible in the additional tooling, syntax and ecosystem that comes together with these frameworks.
  • Part of that complexity comes from the fact that large frameworks need to maintain a high level of backward compatibility and stability due their large amount of users. As such, they have an incentive to not revisit key design choices.
  • Crank revisits a key architectural part of React-like frameworks that dictates render functions to be pure functions. Instead, Crank leverages asynchronous generators to perform asynchronous rendering for free. Asynchronous generators are a standard language feature of JavaScript and do not carry the cost of a library implementing the functionality.
  • Working with the generators baked in the language and the async/await syntax allows developers to handle asynchronous tasks (fetching remote data, suspending and resuming rendering) as naturally as synchronous ones. The number of concepts to master to implement a front-end application that are foreign to the language decreases.

Brian Kim, author of the open source library Repeater.js recently released Crank, a new JavaScript library for creating web applications. Crank's originality is that it declaratively describes an application behavior with co-routines, implemented in JavaScript with async generators

While Crank is still in beta and further investigation is needed, the asynchronous rendering it enables may tackle use cases similar to the Suspense functionality offered by React.

When the generator is run, it may receive data (initial props) from which it returns an iterator which has access to private state held in the generator closure. The iterator computes and yields a view anytime it is prompted to by the iterating party (the Crank library functions), with the latter passing updated props in its iteration request. A Crank async iterator returns a promise which  fulfills with  the computed view to render, giving asynchronous rendering capabilities. Crank components thus naturally support local state and effects without the need for a dedicated syntax. Additionally, Crank components’ lifecycle is the generator's lifecycle: the generator is started when the matching DOM element is mounted; the generator is stopped when the matching DOM element is unmounted. Errors may be caught with the standard try... catch JavaScript construct.

Other frameworks have leveraged JavaScript generators to create web applications. Concur UI ported from Haskell to JavaScript, PureScript and Python uses async generators to compose components. Turbine self-describes as a purely functional web framework without compromises and leverages generators to implement a FRP paradigm.

InfoQ interviewed Brian Kim about the rationale for a new JavaScript framework and the benefits he believes can be derived from leveraging JavaScript generators.

InfoQ: Can you tell our readers about you?

Brian Kim: I’m an independent frontend engineer. I’ve used React for pretty much my entire programming career -- you can even see me mentioned in a React blogpost from 2013.

I’m also the creator and maintainer of the open source async iterator library Repeater.js, which strives to be the missing constructor for creating safe async iterators. [...] I created repeaters, a utility class which looks much like the Promise constructor and allows you to convert callback-based APIs to async iterators more easily.

InfoQ: Can you tell us quickly what repeaters strive for?

Repeaters bake in a lot of good async iterator design practices that I’ve learned over the years, like executing lazily, using bounded queues, dealing with back-pressure, and propagating errors in a predictable manner. In essence, it’s an API that’s been carefully designed to put developers in a pit of success when using async iterators, making sure their event handlers are always cleaned up and bottlenecks and deadlocks are discovered quickly.

InfoQ: You recently released Crank.js, which you describe as a new web framework for creating web applications. Why a new JavaScript framework?

Kim: I know it feels like a new JavaScript framework is released every week, and the blog post I write introducing Crank even starts with an apology for creating another one. I created Crank because I was frustrated with the latest React APIs like Hooks and Suspense, but I still wanted to use JSX and the element diffing algorithm that React popularized. I had used React happily for over half of a decade, so it really took a lot for me to say enough is enough and write my own framework.

InfoQ: What was enough?

Kim: I guess my frustrations started with hooks. I was excited that the React team was investing in making the function syntax for components more useful by allowing them to have state, but I was worried about “The Rules of Hooks,” which seemed easy to circumvent and unfair to other frameworks insofar as it called dibs on any function whose name started with use. And then, as I began to learn more about hooks in practice, and saw the new stale closure bugs which we hadn’t really seen in JavaScript since the invention of let and const, I started to wonder if hooks were the best approach.

But the real tipping point for me was the Suspense project. [...] 

InfoQ: Can you elaborate?

Kim: I began to experiment with Suspense at this point, because I thought it would allow people to use async iterator hooks I had written as though they were synchronous. However, I soon discovered that it would likely have been impossible to actually use Suspense, because Suspense has a hard requirement of a cache, and it was unclear how I could cache and reuse the async iterators which powered my hooks.

The realization that Suspense and async data fetching in React would require a cache was somewhat shocking to me, because up to that point I just assumed we would get something like async/await in React components. [...] I was very worried about having to key and invalidate every async call I made just to use promises.

[I came to realize that] everything React was doing in components with their componentDidWhat methods or with hooks could be encapsulated in a single async generator function:

async function *MyComponent(props) 
  let state = componentWillMount(props);
  let ref = yield <MyElement />;
  state = componentDidMount(props, state, ref);
  try {
    for await (const nextProps of updates()) {
      if (shouldComponentUpdate(props, nextProps, state)) {
        state = componentWillUpdate(props, nextProps, state);
        ref = yield <MyElement />;
        state = componentDidUpdate(props, nextProps, state, ref);
      }

      props = nextProps;
    }
  } catch (err) {
    return componentDidCatch(err);
  } finally {
    componentWillUnmount(ref);
  }
}

[...] By yielding JSX elements rather than returning them, you could write code before and after rendering similar to componentWillUpdate and componentDidUpdate. State becomes local variables, new props could be passed in via a framework provided async iterator, and you could even use JavaScript control flow operators like try/catch/finally to catch errors from child components and write cleanup logic, all within the same scope.

InfoQ: So you decided to use async generators as the base for a new framework?

Kim: [...] While the React team spent its considerable engineering talent building a “UI runtime,” I [realized I] could just delegate the hard parts like stack suspension or scheduling to the JavaScript runtime, which provides generators, async functions and a microtask queue which do exactly that. I felt everything which the React team was doing which seemed impressive and out of reach to me as a programmer was already available to me in vanilla JavaScript, I just had to figure out how the pieces of the puzzle fit together.

Crank is the result of a months-long investigation into this idea that components could be written with not just sync functions, but async functions, and sync and async generator functions, and it’s a bit of a detour in my life, where previously I was head-down working on a startup idea I had. Honestly, I’d love to get back to writing applications and not a framework, but the sudden interest from the JavaScript community has made Crank a happy accident.

InfoQ: You mentioned that Crank.js leverages JSX-driven components and async generators. JSX components are commonplace in frameworks using render functions (React-like frameworks, or Vue in some measure). Few leverage generators, and even less so asynchronous generators. How do such constructs relate to developing a web application?

Kim: I’m definitely not the first to experiment with generators and async generators; I’m always looking for new ideas on GitHub and saw a lot of people experimenting in the frontend space with generators.

However, perhaps due to the early association of generators in JavaScript with async/await and promises, many of these libraries seemed to use generators to yield promises and only return JSX elements, as a way to specify async dependencies to components. I realized that we could also simply yield JSX elements, and I figured out a separate semantics for async components based on the virtual DOM diffing algorithm. 

In the end, I think JSX elements and generators are actually a perfect match: you yield elements, the framework renders them, and the rendered nodes are passed back into the generator in a call and response sort of pattern. I think that on the whole, a lot of people, specifically people who come from functional programming backgrounds, tend to sleep on iterators and generators because they’re stateful data structures, thinking that the statefulness of generators makes them harder to reason about. But actually, I think that’s a great feature of generators, and, at least in JavaScript, the best way to model a stateful process is with a stateful abstraction

By modeling the component lifecycle as generators, not only are we able to capture and model the state of the DOM inside a single function, but we can also do so in an exceptionally transparent way, because there is only one generator execution per component instance, and its closure is preserved between renders. The number of times a synchronous generator component is resumed in Crank is equal to the number of times a parent updates it plus the number of times the component updates itself. That sort of ability to reason about the exact number of times a component is executed is something people have mostly given up on with React, and in practice, it means that with Crank we can put side-effects directly in “the render method,” because the framework isn’t constantly re-rendering your components when you don’t expect.

InfoQ: What feedback did you receive from developers?

Kim: I’ve received feedback like “I wish we had something like this in Rust” and I’m excited to see people take ideas from Crank and implement them in other languages, which might have even more powerful abstractions like Rust futures.

InfoQ: What would be things that may be easier to do with Crank and harder to do with other frameworks? Can you give an example?

Kim: Because all state is just local variables, we’re free to mix concepts from React like props and state and refs within our generator components in a way that no other framework can. For instance, this example of a component which compares old and new props and renders something different based on whether or not they match blew my mind early on in the development of Crank:

function *Greeting({name}) {
  yield <div>Hello {name}</div>;
  for (const {name: newName} of this) {
    if (name !== newName) {
      yield (
        <div>Goodbye {name} and hello {newName}</div>
      );
    } else {
      yield <div>Hello again {newName}</div>;
    }
    name = newName;
  }
}
renderer.render(<Greeting name="Alice" />, document.body);
console.log(document.body.innerHTML); // "<div>Hello Alice</div>"
renderer.render(<Greeting name="Alice" />, document.body);
console.log(document.body.innerHTML); // "<div>Hello again Alice</div>"
renderer.render(<Greeting name="Bob" />, document.body);
console.log(document.body.innerHTML); // "<div>Goodbye Alice and hello Bob</div>"
renderer.render(<Greeting name="Bob" />, document.body);
console.log(document.body.innerHTML); // "<div>Hello again Bob</div>"

We don’t need a separate lifecycle or hook to compare old and new props, we just reference both in the same closure. In short, comparing old and new props becomes as easy as comparing adjacent elements of an array.

Furthermore, because Crank decouples the idea of local state from rerendering, I think it unlocks a lot of advanced rendering patterns which simply aren’t possible in other frameworks. For instance, you can imagine an architecture where child components have local state but aren’t rerendered, but then rendered all at once by a single parent component which renders in a requestAnimationFrame loop. Components that are stateful but don’t have to rerender every time they’re updated are easy to do in Crank because we’ve decoupled state from rerendering.

As an example, you can check out this quick demo I put together wherein I implement the 3D cubes/sphere demo which React and Svelte people were discussing on Twitter last year. I’m excited about Crank’s performance ceiling, because updating a component is just stepping through generators, and there are lots of interesting optimizations that you can do in user-space when state is just local variables and statefulness itself isn’t tightly coupled to a reactive system which forces every stateful component to rerender even if an ancestor component would have rerendered it anyways. While the initial release of Crank focused more on correctness and API design than performance, I’m currently trying to make Crank as fast as possible and the results are beginning to look promising, though I won’t make concrete claims about Crank’s performance just yet.

InfoQ: Conversely, what are things that may be easier to do with other frameworks than with Crank?

Kim: I’ve criticized Concurrent Mode and the future direction of React, but if the React team can pull it off, it’ll be kind of amazing to see components which can be scheduled for rendering automatically based on how congested the main thread is. I have some ideas on how to implement this sort of scheduling stuff in Crank, but I don’t have any concrete solutions yet. Hopefully, the fact that you can await directly in components means we’ll be able to implement scheduling stuff directly in user space in a transparent, opt-in sort of way.

Additionally, although I’m not a fan of React hooks, I think there’s something to be said about how library authors can encapsulate their entire APIs within one or two hooks. One thing I probably should have expected but didn’t was early adopters clamoring for similar hook-like functionality to integrate their libraries with Crank. I’m not sure what that would look like yet, but I have some ideas for this too.

About the Interviewee

Brian Kim is an independent frontend engineer. He is the creator and maintainer of the open source async iterator library Repeater.js, which self-describes as the missing constructor for creating safe async iterators.

Rate this Article

Adoption
Style

BT