The Just JavaScript Framework.
npm install @b9g/crankThe fastest way to try Crank is via the online playground.
Other links:
- crank.js.org
- NPM
- Introducing Crank.js
- Examples
- Deep Wiki
``javascript
// State is defined with generator components
function* Timer() {
// setup goes here
let seconds = 0;
const interval = setInterval(() => this.refresh(() => seconds++), 1000);
for ({} of this) {
yield
clearInterval(interval); // Cleanup just works
}
renderer.render(
// Async components just work on client and server
async function UserProfile({userId}) {
const user = await fetchUser(userId);
return
await renderer.render(
`
- Intuitive: Uses async/await for loading states and generator functions for lifecycles. Updates are just execution and control flow makes sense
- Fast: Outperforms React in benchmarks while weighing in at 13.55KB with zero dependencies
- Flexible: Write build-free vanilla JavaScript with template literals or write ergonomic JSX
- Transparent: State lives in function scope. Explicit re-execution means no mysterious why did you render bugs.
- Future-proof: Built on stable JavaScript features, not evolving framework abstractions
Other frameworks claim to be "just JavaScript" but ask you to think in terms of
effects, dependencies, and framework-specific patterns. Crank actually delivers
on that promise — your components are literally just functions that use standard
JavaScript control flow.
The Crank package is available on NPM through
the @b9g organization (short for
bikeshaving).
`shell`
npm i @b9g/crank
`jsx live
/* @jsxImportSource @b9g/crank /
import {renderer} from "@b9g/crank/dom";
renderer.render(
This paragraph element is transpiled with the automatic transform.
,$3
Starting in version
0.5, the Crank package ships a tagged template
function which provides similar syntax and semantics
as the JSX transform. This allows you to write Crank components in vanilla
JavaScript.`js live
import {jsx} from "@b9g/crank/standalone";
import {renderer} from "@b9g/crank/dom";renderer.render(jsx
No transpilation is necessary with the JSX template tag.
, document.body);
`$3
Crank is also available on CDNs like unpkg
(https://unpkg.com/@b9g/crank?module), esm.sh
(https://esm.sh/@b9g/crank), and esm.run
(https://esm.run/@b9g/crank) for usage in ESM-ready environments.`jsx live
/* @jsx createElement /
import {createElement} from "https://unpkg.com/@b9g/crank/crank?module";
import {renderer} from "https://unpkg.com/@b9g/crank/dom?module";renderer.render(
,
document.body,
);
`Key Examples
$3
`jsx live
import {renderer} from "@b9g/crank/dom";function Greeting({name = "World"}) {
return (
Hello {name}
);
}renderer.render( , document.body);
`$3
`jsx live
function *Timer(this: Context) {
let seconds = 0;
const interval = setInterval(() => this.refresh(() => seconds++), 1000);
for ({} of this) {
yield Seconds: {seconds};
} clearInterval(interval);
}
`$3
`jsx live
import {renderer} from "@b9g/crank/dom";
async function Definition({word}) {
// API courtesy https://dictionaryapi.dev
const res = await fetch(https://api.dictionaryapi.dev/api/v2/entries/en/${word});
const data = await res.json();
if (!Array.isArray(data)) {
return No definition found for {word}
;
} const {phonetic, meanings} = data[0];
const {partOfSpeech, definitions} = meanings[0];
const {definition} = definitions[0];
return <>
{word} {phonetic}
{partOfSpeech}. {definition}
>;
}await renderer.render( , document.body);
`$3
`jsx live
import {Fragment} from "@b9g/crank";
import {renderer} from "@b9g/crank/dom";async function LoadingIndicator() {
await new Promise(resolve => setTimeout(resolve, 1000));
return (
🐕 Fetching a good boy...
);
}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 (
);
}async function *RandomDogLoader({throttle}) {
// for await can be used to race component trees
for await ({throttle} of this) {
yield ;
yield ;
}
}
function *RandomDogApp() {
let throttle = false;
this.addEventListener("click", (ev) => {
if (ev.target.tagName === "BUTTON") {
this.refresh(() => throttle = !throttle);
}
});
for ({} of this) {
yield (
{throttle ? "Slow mode" : "Fast mode"}
);
}
}renderer.render( , document.body);
`Common tool configurations
The following is an incomplete list of configurations to get started with Crank.$3
TypeScript is a typed superset of JavaScript.
Here’s the configuration you will need to set up automatic JSX transpilation.
`tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@b9g/crank"
}
}
`Crank is written in TypeScript and comes with types. Refer to the guide on
TypeScript for more
information about Crank types.
`tsx
import type {Context} from "@b9g/crank";
function *Timer(this: Context) {
let seconds = 0;
const interval = setInterval(() => this.refresh(() => seconds++), 1000);
for ({} of this) {
yield Seconds: {seconds};
} clearInterval(interval);
}
`$3
Babel is a popular open-source JavaScript compiler which allows you to write code with modern syntax (including JSX) and run it in environments which do not support the syntax.
Here is how to get Babel to transpile JSX for Crank.
Automatic transform:
`.babelrc.json
{
"plugins": [
"@babel/plugin-syntax-jsx",
[
"@babel/plugin-transform-react-jsx",
{
"runtime": "automatic",
"importSource": "@b9g/crank", "throwIfNamespace": false,
"useSpread": true
}
]
]
}
`$3
ESLint is a popular open-source tool for analyzing and detecting problems in JavaScript code.
Crank provides a configuration preset for working with ESLint under the package name
eslint-plugin-crank.`bash
npm i eslint eslint-plugin-crank
`In your eslint configuration:
`.eslintrc.json
{
"extends": ["plugin:crank/recommended"]
}
`$3
Astro.js is a modern static site builder and framework.
Crank provides an Astro integration to enable server-side rendering and client-side hydration with Astro.
`bash
npm i astro-crank
`In your
astro.config.mjs.`astro.config.mjs
import {defineConfig} from "astro/config";
import crank from "astro-crank";// https://astro.build/config
export default defineConfig({
integrations: [crank()],
});
`API Reference
$3
`javascript
import {
createElement,
Fragment,
Copy,
Portal,
Raw,
Text,
Context
} from "@b9g/crank";import {renderer} from "@b9g/crank/dom"; // Browser DOM
import {renderer} from "@b9g/crank/html"; // Server-side HTML
import {jsx, html} from "@b9g/crank/standalone"; // Template tag (no build)
import {Suspense, SuspenseList, lazy} from "@b9g/crank/async";
`---
$3
Function Component - Stateless
`javascript
function Greeting({name = "World"}) {
return Hello {name};
}
`Generator Component - Stateful with
function*
`javascript
function* Counter() {
let count = 0;
const onclick = () => this.refresh(() => count++); for ({} of this) {
yield ;
}
}
`Async Component - Uses
async for promises
`javascript
async function UserProfile({userId}) {
const user = await fetch(/api/users/${userId}).then(r => r.json());
return Hello, {user.name}!;
}
`Async Generator Component - Stateful + async
`javascript
async function* DataLoader({url}) {
for ({url} of this) {
const data = await fetch(url).then(r => r.json());
yield {data.message};
}
}
`---
$3
The context is available as
this in components (or as 2nd parameter).`javascript
function Component(props, ctx) {
console.log(this === ctx); // true
return props.children;
}
`#### Properties
this.props - Current props (readonly)
this.isExecuting - Whether the component is currently executing
this.isUnmounted - Whether the component is unmounted#### Methods
this.refresh(callback?) - Trigger re-execution
`javascript
this.refresh(); // Simple refresh
this.refresh(() => count++); // With state update (v0.7+)
`
this.schedule(callback?) - Execute after DOM is rendered
`javascript
// el is whatever the component returns Node/Text/HTMLElement/null, an array of dom nodes, etc
this.schedule((el) => {
console.log("Component rendered", el.innerHTML);
});
`
this.after(callback?) - Execute after DOM is live
`javascript
// this runs after the DOM nodes have finally entered the DOM
// this is where you put things like autofocus
this.after((el) => {
console.log(el.isConnected); // true
});
`
this.cleanup(callback?) - Execute on unmount
`javascript
function* Component() {
const interval = setInterval(() => this.refresh(), 1000);
this.cleanup(() => clearInterval(interval)); for ({} of this) {
yield
Tick;
}
}
`
this.addEventListener(type, listener, options?) - Listen to events
`javascript
this.addEventListener("click", (e) => console.log("Clicked!"));
`
this.dispatchEvent(event) - Dispatch events
`javascript
this.dispatchEvent(new CustomEvent("mybuttonclick", {
bubbles: true,
detail: {id: props.id}
}));
`
this.provide(key, value) / this.consume(key) - Context API
`javascript
// Provider
function* ThemeProvider() {
this.provide("theme", "dark");
for ({} of this) {
yield this.props.children;
}
}// Consumer
function ThemedButton() {
const theme = this.consume("theme");
return ;
}
`#### Iteration
for ({} of this) - Render loop (sync)
`javascript
function* Component() {
for ({} of this) {
yield {this.props.message};
}
}
`
for await ({} of this) - Async render loop for racing trees
`javascript
async function* AsyncComponent() {
for await ({} of this) {
// Multiple yields race - whichever completes first shows
yield ;
yield ;
}
}
`---
$3
key - Unique identifier for reconciliation
`javascript
{items.map(item => - {item.name}
)}
`
ref - Access rendered DOM element
`javascript
`
copy - Prevent/control re-rendering
`javascript
// Boolean: prevent rendering when truthy
{el.value} // string: copy specific props
// Copy all except value
// Copy only class and id
// Copy children
`
hydrate - Control SSR hydration
`javascript
// Skip hydration
// Force hydration
// Hydrate all except value
`
class - String or object (v0.7+)
`javascript
btn: true,
'btn-active': isActive,
'btn-disabled': isDisabled
}} />
`
style - CSS string or object
`javascript
`
innerHTML - Raw HTML string (⚠️ XSS risk)
`javascript
`Event Props - Lowercase event handlers
`javascript
`Prop Naming - HTML-friendly names supported
`javascript
// Instead of className and htmlFor
`---
$3
- Render children without wrapper
`javascript
import {Fragment} from "@b9g/crank";
Child 1
Child 2
// Or use: <>...>
// The Fragment tag is the empty string
`
- Prevent element re-rendering
`javascript
import {Copy} from "@b9g/crank";function memo(Component) {
return function* Wrapped(props) {
yield ;
for (const newProps of this) {
if (equals(props, newProps)) {
yield ; // Reuse previous render
} else {
yield ;
}
props = newProps;
}
};
}
`
- Render into different DOM node
`javascript
import {Portal} from "@b9g/crank";const modalRoot = document.getElementById("modal-root");
function Modal({children}) {
return (
{children}
);
}
`
- Insert raw HTML or DOM nodes
`javascript
import {Raw} from "@b9g/crank";function MarkdownViewer({markdown}) {
const html = marked(markdown);
return
;
}// Or insert DOM node
`
- Explicit text node creation (v0.7+)
`javascript
import {Text} from "@b9g/crank";
// Access Text nodes in lifecycle
function* Component() {
this.schedule((node) => {
if (node instanceof Text) {
console.log("Text node:", node);
}
});
for ({} of this) {
yield "Text content"; // Becomes a Text node
}
}
`---
$3
lazy(loader) - Lazy-load components
`javascript
import {lazy} from "@b9g/crank/async";const LazyComponent = lazy(() => import("./MyComponent.js"));
Loading...
}>
`
Suspense - Declarative loading states
`javascript
import {Suspense} from "@b9g/crank/async";Loading...
SuspenseList - Coordinate multiple async components
`javascript
import {SuspenseList} from "@b9g/crank/async";
Loading 1...
---
$3
Mount - Code before first
yield
`javascript
function* Component() {
console.log("Mounting...");
const interval = setInterval(() => this.refresh(), 1000); for ({} of this) {
yield
Tick;
} clearInterval(interval); // Cleanup
}
`Update - Code inside render loop
`javascript
function* Component() {
for ({} of this) {
console.log("Updated with:", this.props);
yield {this.props.message};
}
}
`Cleanup - Code after loop or via
this.cleanup()
`javascript
function* Component() {
const interval = setInterval(() => this.refresh(), 1000);
this.cleanup(() => clearInterval(interval)); for ({} of this) {
yield
Tick;
}
}
`---
$3
Higher-Order Components
`javascript
function withLogger(Component) {
return function* WrappedComponent(props) {
console.log("Rendering with:", props);
for ({} of this) {
yield ;
}
};
}
`Hooks
`javascript
function useInterval(ctx, callback, delay) {
let interval = setInterval(callback, delay);
ctx.cleanup(() => clearInterval(interval));
return (newDelay) => {
delay = newDelay;
clearInterval(interval);
interval = setInterval(callback, delay);
};
}
`Context Extensions (⚠️ Prefer hooks over global extensions)
`javascript
import {Context} from "@b9g/crank";Context.prototype.setInterval = function(callback, delay) {
const interval = setInterval(callback, delay);
this.cleanup(() => clearInterval(interval));
};
// Use in components
function* Timer() {
let seconds = 0;
this.setInterval(() => this.refresh(() => seconds++), 1000);
for ({} of this) {
yield
Seconds: {seconds};
}
}
`Racing Components
`javascript
async function* DataComponent({url}) {
for await ({url} of this) {
// This is the equivalent of calling
// renderer.render( , document.body);
// renderer.render(, document.body);
// but in a component.
yield ;
yield ;
}
}
`Components race to render. Useful for fallback states.
---
$3
`typescript
import type {Context} from "@b9g/crank";
import {ComponentProps} from "@b9g/crank"; // v0.7+// Component with typed props
interface Props {
name: string;
age?: number;
}
function Greeting({name, age}: Props) {
return
Hello {name}, age {age};
}// Generator with typed context
function* Greeting(this: Context, {name}: {name: string}) {
for ({name} of this) {
yield Hello {name};
}
}
// Extract component props type
function Button({variant}: {variant: "primary" | "secondary"}) {
return ;
}
type ButtonProps = ComponentProps;
``For comprehensive guides and documentation, visit crank.js.org