Write JSX-driven components with functions, promises and generators.

Overview

Crank.js

Write JSX-driven components with functions, promises and generators.

Documentation is available at crank.js.org. Crank.js is in a beta phase, and some APIs may change. To read more about the motivations for this library, you can read the introductory blog post.

Features

Declarative

Crank uses the same JSX syntax and diffing algorithm popularized by React, allowing you to write HTML-like code directly in JavaScript.

Just Functions

All components in Crank are just functions or generator functions. No classes, hooks, proxies or template languages are needed.

Promise-friendly

Crank provides first-class support for promises. You can define components as async functions and race renderings to display fallback UIs.

Lightweight

Crank has no dependencies, and its core is a single file. It currently measures at 4.5KB minified and gzipped.

Performant

According to benchmarks, Crank beats React in terms of speed and memory usage, and is currently comparable to Preact or Vue.

Extensible

The core renderer can be extended to target alternative environments such as WebGL libraries, terminals, smartphones or smart TVs.

Installation

Crank is available on NPM in the ESModule and CommonJS formats.

$ npm install @bikeshaving/crank
/** @jsx createElement */
import {createElement} from "@bikeshaving/crank";
import {renderer} from "@bikeshaving/crank/dom";

renderer.render(<div id="hello">Hello world</div>, document.body);

If your environment does not support ESModules (you may see a message like SyntaxError: Unexpected token export), you can import the CommonJS versions of the library under the cjs directory.

/** @jsx createElement */
import {createElement} from "@bikeshaving/crank/cjs";
import {renderer} from "@bikeshaving/crank/cjs/dom";

renderer.render(<div id="hello">Hello world</div>, document.body);

Key Examples

A Simple Component

/** @jsx createElement */
import {createElement} from "@bikeshaving/crank";
import {renderer} from "@bikeshaving/crank/dom";

function Greeting({name = "World"}) {
  return (
    <div>Hello {name}</div>
  );
}

renderer.render(<Greeting />, document.body);

Try on CodeSandbox

A Stateful Component

/** @jsx createElement */
import {createElement} from "@bikeshaving/crank";
import {renderer} from "@bikeshaving/crank/dom";

function *Timer() {
  let seconds = 0;
  const interval = setInterval(() => {
    seconds++;
    this.refresh();
  }, 1000);
  try {
    while (true) {
      yield <div>Seconds: {seconds}</div>;
    }
  } finally {
    clearInterval(interval);
  }
}

renderer.render(<Timer />, document.body);

Try on CodeSandbox

An Async Component

/** @jsx createElement */
import {createElement} from "@bikeshaving/crank";
import {renderer} from "@bikeshaving/crank/dom";

async function QuoteOfTheDay() {
  const res = await fetch("https://favqs.com/api/qotd");
  const {quote} = await res.json();
  return (
    <p>{quote.body}” – <a href={quote.url}>{quote.author}</a>
    </p>
  );
}

renderer.render(<QuoteOfTheDay />, document.body);

Try on CodeSandbox

A Loading Component

/** @jsx createElement */
import {createElement, Fragment} from "@bikeshaving/crank";
import {renderer} from "@bikeshaving/crank/dom";

async function LoadingIndicator() {
  await new Promise(resolve => setTimeout(resolve, 1000));
  return <div>Fetching a good boy...</div>;
}

async function RandomDog({throttle = false}) {
  const res = await fetch("https://dog.ceo/api/breeds/image/random");
  const data = await res.json();
  if (throttle) {
    await new Promise(resolve => setTimeout(resolve, 2000));
  }

  return (
    <a href={data.message}>
      <img src={data.message} alt="A Random Dog" width="300" />
    </a>
  );
}

async function *RandomDogLoader({throttle}) {
  for await ({throttle} of this) {
    yield <LoadingIndicator />;
    yield <RandomDog throttle={throttle} />;
  }
}

function *RandomDogApp() {
  let throttle = false;
  this.addEventListener("click", (ev) => {
    if (ev.target.tagName === "BUTTON") {
      throttle = !throttle;
      this.refresh();
    }
  });

  while (true) {
    yield (
      <Fragment>
        <div>
          <button>Show me another dog.</button>
        </div>
        <RandomDogLoader throttle={throttle} />
      </Fragment>
    );
  }
}

renderer.render(<RandomDogApp />, document.body);

Try on CodeSandbox

Comments
  • addEventListener and delegation with tagName is awkward

    addEventListener and delegation with tagName is awkward

    Not an issue per se (like a bug or anything), but having just read through all the blog post and docs, this was the one bit that I thought "modern React" definitely does better at.

    No idea if this is even possible, but I'm sure I won't be the only one turned off by this.

    opened by tomhicks 37
  • Equivalent to React.createContext() with Provider and Consumer (or alternative)?

    Equivalent to React.createContext() with Provider and Consumer (or alternative)?

    Is this being implemented or did you have any thoughts about someone might implement it in this library?

    Basically, what is the strategy for avoiding props drilling down multiple layers of components?

    documentation 
    opened by ghost 31
  • Improve typings to add safety for props

    Improve typings to add safety for props

    Based on #49, I think I have everything right. Thoughts?

    • [x] Add tests to prove types are working
    • [ ] Cleanup commits
    • [ ] Split tests to separate PR or Upgrade to TypeScript 3.9.0 https://github.com/microsoft/TypeScript/issues/37198
    opened by monodop 29
  • build stuff

    build stuff

    Fixes #87 #89 This PR changes the build outputs slightly so the package supports more environments. It is scheduled for the first 0.2 release.

    • Moves esm files directly to the root.
    • Adds export map.
    • Adds UMD build.
    • Changes output formats for builds to not transpile generators/async stuff.
    opened by brainkim 28
  • Export mjs files so Node can natively import them.

    Export mjs files so Node can natively import them.

    Fixes #87.

    Based on https://nodejs.org/api/esm.html it seems like there's just two things we need to do here.

    1. The esm scripts need to have .mjs extensions
    2. package.json needs to have an exports section, defining the ESM and CJS exports.

    I think it would be nice to add built-in automated tests of this stuff, but I have no clear idea of how to do that.

    Here's the test script I used to verify that I did no harm and that I fixed the bug.

    #!/bin/bash -x
    
    rm -rf testesm
    mkdir testesm
    cd testesm
    npm init -y
    npm i ../crank
    cat <<EOF > index.mjs
    import {createElement} from "@bikeshaving/crank";
    import {renderer} from "@bikeshaving/crank/html";
    
    console.log(renderer.render(createElement("div", null, "Hello world")));
    EOF
    node index.mjs
    
    cat <<EOF > index.js
    const {createElement} = require("@bikeshaving/crank");
    const {renderer} = require("@bikeshaving/crank/html");
    
    console.log(renderer.render(createElement("div", null, "Hello world")));
    EOF
    node index.js
    
    opened by dfabulich 27
  • Crank Native [and other custom Crank renderers]

    Crank Native [and other custom Crank renderers]

    EDIT – Original title: "Is there an API for creating custom renderers?"

    If there is one, and provided it's reasonably similar to that of React, then I could probably port React NativeScript (https://github.com/shirakaba/react-nativescript) to it to make an iOS + Android framework (Crank NativeScript).

    documentation 
    opened by shirakaba 27
  • Add logo

    Add logo

    Hi. Thanks for this project! I never really looked into generators much before, other than Sagas, and am excited to see where you and the community will take this.

    A side hobby of mine is to design logos, and I thought I'd have a crack at one for Crank. I hope you don't mind. What do you reckon?

    image

    UPDATE: There's a slight cut-out around the crank handle to allow the icon to display well in monochromatic colours (e.g. icon fonts).

    UPDATE2: Rotated icon and increased the cut-out so that the negative space looks like a C.

    UPDATE3: Use @pjdon's involute gears.

    image image
    opened by will-stone 26
  • this.refresh() failed to update DOM

    this.refresh() failed to update DOM

    I wanted

    I tried to use Crank with MobX, and then I wrote observer() adopter for Crank.

    I wrote

    https://codesandbox.io/s/crank-router-7f5o3?file=/src/observer.ts

    I got

    Every invoking of this.refresh() created correct VDOM node, but the real DOM updated nothing...

    image

    opened by TechQuery 22
  • Question regarding components whose side effects depend on (possibly changing) prop values

    Question regarding components whose side effects depend on (possibly changing) prop values

    Normally the Github issue tracker is not necessarily there to ask questions about certain implementation cases. I hope you do not mind if I do nonetheless :-)

    Please have a look at the following React demo: https://codesandbox.io/s/frosty-kowalevski-4qp51?file=/src/index.js

    The implementation of component "Ticker" is quite trivial in React (normally I would use a "useInterval" custom hook for this, but this is not important here...)

    What is the best way to implemenent that "Ticker" component in Crank.js?

    Please find here a NOT YET WORKING Crank.js version of the demo: https://codesandbox.io/s/tender-greider-qto05?file=/src/index.js

    Many many thanks in advance.

    opened by mcjazzyfunky 19
  • Ready for the real world? - Episode #1

    Ready for the real world? - Episode #1

    [Edit] "Ready for the real world?" is just a catch phrase - don't take that too serious :wink: in particular I do not mean "production ready" or something


    Franky, I'm a bit concerned that most of the Crank demos I've seen in so far, have only covered use cases that are obviously well suited for Crank. As by the end of the day, Crank will have to prove its power in ALL use cases and not just the well suited ones, please allow me to start a little series of small React demos that I think are a bit more challenging for Crank. Would be great if some of you guys like to meet new challenges and try to find a good way how to implement those short examples with Crank.

    Important: It's really NOT necessary to provide an actual running Crank demo - just think of some useful helper functions (without implementing them) or desired API changes for Crank if you like and provide some short pseudocode - it's not important whether everything is 100% correct or not, it's just about the patterns. Also, it would be nice if the presumable code size of the Crank version (without the presumable code size of your imaginary helper functions) is more or less comparable with the code size of the React version.

    The first example is a little "Metronome" (tick ... tock ...tick ... tock...) demo that I have already mentioned in some other issue (and due to issue #43 I assume that this might be a tough nut):

    https://codesandbox.io/s/jolly-bash-t511f?file=/src/index.js

    Thanks a lot in advance and good luck.

    opened by mcjazzyfunky 18
  • Alternate implementation of events/handlers for codesandbox TodoMVC

    Alternate implementation of events/handlers for codesandbox TodoMVC

    I (and others) have not been super comfortable with the way crank.js encourages using raw strings in events and event handlers. I re-wrote your codesandbox example using something a little closer to a "action creator" and "reducer switch statement" style of coding, as well as binding directly to the on-attributes of elements instead of using addEventHandler everywhere. I thought it might be useful to show people an alternative without so many addEventHandlers and raw strings.

    https://codesandbox.io/s/crank-todomvc-alternative-jluse https://gist.github.com/geophree/a317c658fd6d7bc51e1e80aa5243be7d

    Let me know what you think!

    opened by geophree 17
  • Allow async generator components to use `for... of`

    Allow async generator components to use `for... of`

    There are two disadvantages with the way async generator components are currently designed:

    1. Errors thrown by async generator components don’t have stack traces which point to the the initiators of rendering (usually a call to render() or ctx.refresh()). This is because async generator components resume by unblocking the for await... of loop, rather than via calls to iterator.next().

    2. The differences in behavior between sync generator components and async generator components can be counter-intuitive for newcomers (TODO: find the issue/discussion). And even if you’re familiar with the differences, having to refactor to use for await and switch your mental model so that you can await a promise directly in a stateful component seems like a high cost relative to the benefits.

    For instance, one of the big reasons I initially wanted this is that I wanted easy rendering post-conditions, i.e. code which runs after the yield. This is useful but also limited: code after yields only runs after recursive child renders, which isn’t really a useful time to run code, and even if the async generator component does not render asynchronously, the real moment you’re trying to run code is not usually “when the component finishes rendering” but “when the component actually insert rendered children into the active document.”

    To be clear, I don’t think async generator components are mis-designed. I still think using for await... of is the best way to implement fallbacks states, and the idea of a component yielding multiple trees per update is elegant. Nevertheless, I do think the cons listed above are important to address.

    One of the main themes around the upcoming 0.5 release has been about making things easier to type, and I’ve found a lot of success in inspecting the internal state of components via the props iterators to implement quality of life conveniences. For instance, . This gives me hope that maybe we can allow async generator components to use for... of iterators. The idea would be that if we yield in a for... of loop, we switch to behavior that mimics sync generator components, just async.

    This was never impossible: async functions can call sync functions. The one thing I worry about is that now async generator components have three de facto modes: 1. while looping (outside any render loop), 2. for of looping, 3. for await of looping. Is this itself confusing? Not sure.

    opened by brainkim 0
  • Precise DOM mutations

    Precise DOM mutations

    Lately, I’ve been working on a contenteditable-based editor library, and one of the interesting challenges about working with these types of editors is that when you re-render, DOM nodes may be mutated in a way that the browser didn’t intend. Specifically, the behavior of selections, the cursor that moves around as you type, is fragile, and mutations around the selection will often cause the cursor to appear somewhere the user didn’t intend, like moving to the beginning of the line that you’re editing. For text editors, this is pretty much a show-stopping bug, in the sense that it is really annoying and disorienting for the user whenever it happens.

    The solution to this species of bug is to read the selection before DOM mutations happen, and write this selection back after DOM mutations have finished. However, the “before” and “after” here is the problem, in the sense that in an ideal world, the reading and writing would happen in the same synchronous block of code: read selection, make mutations, write selection. Any time I tried to split up the read/write logic based on things like requestAnimationFrame() or some other asynchronous timing, the result has been soul-crushing race conditions, because there is only a small window of time where the correct selection can be read. Inevitably, if you try to split up this read/mutate/write logic you end up with the same selection bugs that you were trying to prevent.

    Luckily, Crank is always synchronous for component trees which only contain synchronous components, so this selection fixing logic is easy to implement. The editor I’m currently playing with has all editable content defined as a single, sync generator component, which only has sync function components for children.

    Unfortunately, this sort of constraint doesn’t lend itself well to the building of a “library,” or in this case, reusable utility functions or components which do the selection fixing logic automagically. Ideally, the components you define should be composable; you should be able to call a stateful component from a stateless component and vice-versa, you shouldn’t have to worry about child components being independently stateful. The situation gets even hairier when you have components with dynamic, transcluded children, in which case you wouldn’t know what kinds of child components might be passed in. Requiring declarative editors to be defined exclusively with a top-level sync generator component with no async or stateful child components, violates this ideal.

    One possible way to mitigate this is to define a contract where, every stateful component which plays a part in the editable area has to call the selection fixing logic independently. However, I realized that when to read the selection as part of the ”read/mutate/write” sandwich is non-trivial. For the case of a sync generator component with sync children, you can simply read the selection when the generator function is actually executing, because everything is nice and synchronous. But the story gets more complicated when working with async components, because suddenly you’re reading stuff microtasks away from when the DOM is actually mutated in the best case, and possibly more time when components contains async children, because we defer the initial DOM mutations until those async children have resolved. Additionally, putting the reading of the selection right before a component yields or returns in the case of async components is annoying, insofar as components can have multiple yields/returns.

    We need a callback, probably the one passed to this.schedule(), which fires synchronously right before a component performs the DOM mutations the component is responsible for. Unfortunately, the timings of when schedule() fires hasn’t been that accurate, especially when working with async components.

    There is also a matter of what actual DOM mutations a component is “responsible” for. For instance, in the following component, the outer <div> seems like it’s the responsibility of the parent, and Crank can make the guarantee that framework-caused mutations to the outer div will only happen if the <Parent> component is refreshed or updated.

    function Parent({children}) {
      for ({children} of this) {
        yield (
          <div class="outer">
            <Child>
              <div class="inner" />
            </Child>
            {children}
          </div>
        );
      }
    }
    

    On the other hand, the inner <div> is passed into the <Child> component as its children, and it might be added or removed based on the whims of the child component, so clearly the parent component can’t be solely responsible for changes to that inner <div>. And what do we do about dynamic children? This is all a bit of a headache.

    Ultimately, it seems like whether a component is responsible for its descendant DOM mutations is based on whether or not there is another component between the root and the virtual host element. The problem is that, internally, we call DOM mutating functions independently for every element, and not based on when components commit, so I’m not actually sure that it works out Crank has a strict single responsibility rule for every DOM mutation being done in a single block for a single component (or the root if the rendering was initiated by renderer.render()).

    This was rambling and probably makes sense only to me, but I think I can sum up this desire as the following three guarantees for Crank:

    1. The this.schedule() callback should fire synchronously before a component’s mutations occur, regardless of whether a component is sync or async.
    2. All DOM nodes which a component is responsible for, as defined by host element children which are not themselves the descendants of other components, should be mutated in a single synchronous pass. Crank should cause mutations to elements a component is responsible for if and only if that component is updated or refreshed. This extends to attribute/property mutations as well as DOM nodes being added and removed. It might not apply to when DOM nodes are created. Ideally, we should create DOM nodes as soon as possible, so that we can make them available, for instance, to the schedule() callback. All mutations which happen to DOM nodes that are not attached to the current document are probably fine.
    3. The this.flush() callback should fire synchronously after a component’s mutations occur, regardless of whether a component is sync or async.

    It may already be the case that this logic is implemented, in which case this is all a matter of testing. I think the idea of components being responsible for, or “owning,” DOM nodes and mutations is appealing, not just for the niche text editor use-case, but also for making sure that the DOM is consistent view of state, and for sourcing potential errors caused by DOM mutations. It would be nice if, for instance, an error caused by a DOM mutation of a readonly property could somehow be linked to the component which is “responsible” for the error. It might also help with performance too, who knows.

    opened by brainkim 0
  • Context utility wish-list

    Context utility wish-list

    Some context properties I wish I have when dogfooding, which would be relatively easy to implement:

    • this.hasRendered Distinguish the initial run from subsequent runs in the render loop.
    • this.isRefreshing Distinguish between parent update and local update. Maybe it would be better to do this as an enumeration of render sources (this.refresh() called, parent renders, root render?, provision/consumer renders?).
    • this.renderer Get an instance of the renderer so you can do checks for SSR or whatever.
    • this.$key Get the key passed to the component. I often have to pass the key around separately this for debugging purposes.
    • this.$ref Get the ref callback passed to a component. Could be used to override what is passed to the component.
    opened by brainkim 2
  • Restart generator components which return when re-rendered

    Restart generator components which return when re-rendered

    The behavior of generator components not rendering or getting stuck after they return is confusing, and not useful, especially when you’re quickly refactoring from function components to generator components. The problem is that the getting stuck behavior doesn’t provide any hints to the developer about where the component is stuck, which makes finding returned generator components extremely difficult. I’m also pretty sure there are no valid use-cases for the current behavior.

    We should consider making it so that generator components which return are restarted when re-rendered. This would allow developers to at least see that the generator component is executing.

    Additionally, it might make sense to emit a console warning when generator components return early, though I’m not as sure about this last part. A console warning would only make sense if there are no conceivable use-cases for returning from a generator component.

    enhancement 
    opened by brainkim 0
  • Allow generator functions to yield once so we don’t have to use try/finally

    Allow generator functions to yield once so we don’t have to use try/finally

    Currently, we use the return() method of generator functions to allow for components to define cleanup code when they’re unmounted. This is nice because it allows us to use try/finally statement, which orders code in the way you would expect, but the extra block and level of indentation is annoying to type out and diff in version control. I wonder if it would be nicer to let the generator resume normally, so that we could allow generators to resume once and break out of the props iterator, so that the try/finally statement is not needed.

    Before proposal:

    function *Timer() {
      let seconds = 0;
      const interval = setInterval(() => {
        seconds++;
        this.refresh();
      }, 1000);
      try {
        for ({} of this) {
          yield <div>{seconds}s</div>;
        }
      } finally {
        clearInterval(interval);
      }
    }
    
    renderer.render(<Timer />, document.body);
    

    After proposal:

    function *Timer() {
      let seconds = 0;
      const interval = setInterval(() => {
        seconds++;
        this.refresh();
      }, 1000);
      for ({} of this) {
        yield <div>{seconds}s</div>;
      }
    
      clearInterval(interval);
    }
    
    renderer.render(<Timer />, document.body);
    

    We probably still have to return afterwards anyways to support while (true) type generators, but this would be a nice quality of life change. The way it works for async generators might be a little more complicated.

    enhancement 
    opened by brainkim 0
Write components once, run everywhere. Compiles to Vue, React, Solid, Angular, Svelte, and more.

Write components once, run everywhere. Compiles to: At a glance Mitosis is inspired by many modern frameworks. You'll see components look like React c

Builder.io 7.7k Jan 1, 2023
Yet Another JSX using tagged template

우아한 JSX Yet Another Simple JSX using tagged template 언어의 한계가 곧 세계의 한계다 - Ludwig Wittgenstein 우아한 JSX가 캠퍼들의 표현의 자유를 넓히고 세계를 넓히는데 도움이 되었으면 합니다 Example i

null 20 Sep 22, 2022
🍼 650B Virtual DOM - Use reactive JSX with minimal overhead

?? little-vdom Forked from developit's little-vdom gist. npm: npm i @luwes/little-vdom cdn: unpkg.com/@luwes/little-vdom 650B Virtual DOM Components S

wesley luyten 87 Nov 12, 2022
The simplest way to create web components from plain objects and pure functions! 💯

?? One of the four nominated projects to the "Breakthrough of the year" category of Open Source Award in 2019 hybrids is a UI library for creating web

hybrids 2.7k Dec 27, 2022
Relay is a JavaScript framework for building data-driven React applications.

Relay · Relay is a JavaScript framework for building data-driven React applications. Declarative: Never again communicate with your data store using a

Facebook 17.5k Jan 1, 2023
Write you own GitHub webhooks with Deno Deploy.

Hooray Write you own GitHub webhooks with Deno Deploy. Deno Deploy is where you can distribute your JavaScript/TypeScript code to the edge with one li

null 3 Jun 22, 2022
📃 Violet is an open-source discord bot with multiple functions written in TypeScript.

?? About Violet is a JavaScript Discord bot focused on utility and game functions. It have a temporary channels system, minecraft and league of legend

Violet 3 Nov 11, 2022
A Web Component compiler for building fast, reusable UI components and static site generated Progressive Web Apps

Stencil: A Compiler for Web Components and PWAs npm init stencil Stencil is a simple compiler for generating Web Components and static site generated

Ionic 11.3k Jan 4, 2023
⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOM.

Fast 3kB alternative to React with the same modern API. All the power of Virtual DOM components, without the overhead: Familiar React API & patterns:

Preact 33.6k Jan 8, 2023
jCore - JavaScript library for building UI components

JavaScript library for building UI components

iOnStage 11 Jan 21, 2022
Shoelace - A collection of professionally designed, every day UI components built on a framework-agnostic technology. 🥾

Shoelace A forward-thinking library of web components. Works with all frameworks ?? Works with CDNs ?? Fully customizable with CSS ?? Includes an offi

Shoelace 7.7k Dec 26, 2022
📓 The UI component explorer. Develop, document, & test React, Vue, Angular, Web Components, Ember, Svelte & more!

Build bulletproof UI components faster Storybook is a development environment for UI components. It allows you to browse a component library, view the

Storybook 75.9k Jan 9, 2023
Fest - A reuseable UI components builder for the web

fest Fest is actually fast. A reuseable UI components builder for the web. Rerender components or parts of components as users interact with your app.

Bismark Yamoah 3 Jul 6, 2022
🌱 React and redux based, lightweight and elm-style framework. (Inspired by elm and choo)

English | 简体中文 dva Lightweight front-end framework based on redux, redux-saga and react-router. (Inspired by elm and choo) Features Easy to learn, eas

null 16.1k Jan 4, 2023
A declarative, efficient, and flexible JavaScript library for building user interfaces.

React · React is a JavaScript library for building user interfaces. Declarative: React makes it painless to create interactive UIs. Design simple view

Facebook 200k Jan 4, 2023
Give your JS App some Backbone with Models, Views, Collections, and Events

____ __ __ /\ _`\ /\ \ /\ \ __ \ \ \ \ \ __ ___\ \ \/'\\ \ \_

Jeremy Ashkenas 28k Jan 9, 2023
A blazing fast React alternative, compatible with IE8 and React 16.

Nerv is a virtual-dom based JavaScript (TypeScript) library with identical React 16 API, which offers much higher performance, tinier package size and

null 5.4k Jan 4, 2023
Simple and elegant component-based UI library

Simple and elegant component-based UI library Custom components • Concise syntax • Simple API • Tiny Size Riot brings custom components to all modern

Riot.js 14.7k Jan 4, 2023
A functional and reactive JavaScript framework for predictable code

Cycle.js A functional and reactive JavaScript framework for predictable code Website | Packages | Contribute | Chat | Support Welcome Question Answer

Cycle.js 10.2k Jan 4, 2023