This document provides a high-level introduction to Crank.js, covering its core philosophy, architecture, and fundamental concepts. It explains the "Just JavaScript" approach, the four component types, and the core abstractions that power the framework.
For detailed information about specific subsystems:
Crank.js is a JSX-based web framework that uses standard JavaScript functions, async functions, and generator functions to write components. It is distributed as the @b9g/crank package on NPM, weighing 13.55KB with zero dependencies.
| Property | Value |
|---|---|
| Package Name | @b9g/crank |
| Homepage | https://crank.js.org |
| Repository | https://github.com/bikeshaving/crank |
| Tagline | "The Just JavaScript Framework" |
| Bundle Size | 13.55KB minified |
| Dependencies | Zero |
| License | MIT |
Sources: package.json1-141
Crank's defining characteristic is its non-reactive approach. Unlike React, Vue, Svelte, and Solid, Crank does not use reactive primitives (signals, stores, observables) to automatically update the UI when state changes. Instead, components explicitly call this.refresh() to trigger re-renders.
This explicit refresh model provides what the framework calls executional transparency - you can see exactly when your code runs without hidden reactive graphs or framework-specific scheduling algorithms.
The philosophy contrasts with React's opaque rendering model (double-rendering, hooks dependencies, concurrent mode) and other frameworks' reactive abstractions (Solid's signal graphs, Vue's deep proxies, Svelte's compiler magic). Crank prioritizes making it clear when code executes over automating what causes re-execution.
Key Principle: "Your components are literally just functions that use standard JavaScript control flow."
Sources: docs/blog/2025-08-20-why-be-reactive.md1-372 README.md1-850 CHANGELOG.md36-107
Crank uses a modular export strategy with separate entrypoints for different use cases:
| Module Path | Purpose | Source File | Key Exports |
|---|---|---|---|
@b9g/crank | Core framework | src/crank.ts | createElement, Fragment, Copy, Portal, Raw, Text, Context, Renderer |
@b9g/crank/dom | Browser rendering | src/dom.ts | renderer (DOMRenderer instance) |
@b9g/crank/html | Server-side rendering | src/html.ts | renderer (HTMLRenderer instance) |
@b9g/crank/async | Async utilities | src/async.ts | Suspense, SuspenseList, lazy |
@b9g/crank/standalone | Template tag | src/standalone.ts | jsx, html (no build step) |
@b9g/crank/jsx-runtime | JSX transform | src/jsx-runtime.ts | Automatic JSX runtime |
@b9g/crank/event-target | Event system | src/event-target.ts | EventTarget class |
Package Structure Diagram
Sources: package.json31-114 rollup.config.js54-108
Crank's architecture consists of three primary classes defined in src/crank.ts and several special symbols that form the foundation of the framework:
Core Classes and Their Roles
The Element class in src/crank.ts is the fundamental unit created by createElement() calls (JSX transpilation target). Each element stores its tag and props, along with private properties for rendering state.
| Property | Type | Purpose | Visibility |
|---|---|---|---|
tag | string | symbol | Function | Host element tag, special symbol, or component function | Public |
props | Record<string, any> | Element properties including children | Public |
_node | TNode | undefined | Cached DOM node from renderer | Private |
_children | Element | string | Array | undefined | Cached child elements from previous render | Private |
_ctx | Context | undefined | Component context instance | Private |
_isMounted | boolean | Flag indicating if element is mounted in tree | Private |
Key Distinction: Element (Crank's virtual element class in src/crank.ts) vs Element (DOM's base class from Web APIs). Crank elements are plain JavaScript objects that describe what to render; they are not DOM nodes themselves.
Sources: src/crank.ts docs/blog/2020-10-13-writing-crank-from-scratch.md117-228
The Renderer class in src/crank.ts orchestrates the rendering process using an adapter pattern. It walks element trees, manages state, and delegates environment-specific operations to a RenderAdapter interface.
Renderer Core Methods
| Method | Phase | Signature | Purpose |
|---|---|---|---|
render(children, root, ctx?) | Entry | Returns Promise<TResult> | TResult | Renders element tree into root |
create(data) | Reconciliation | Called by adapter | Creates nodes for host elements |
patch(data) | Reconciliation | Called by adapter | Updates node properties |
arrange(data) | Commit | Called by adapter | Orders children within parent |
remove(data) | Cleanup | Called by adapter | Removes nodes from tree |
Concrete Implementations
The base Renderer class is subclassed for different rendering targets:
| Class | File | Purpose |
|---|---|---|
DOMRenderer | src/dom.ts | Renders to browser DOM nodes |
HTMLRenderer | src/html.ts | Renders to HTML strings for SSR |
Sources: src/crank.ts src/dom.ts src/html.ts
The Context class in src/crank.ts is passed as this to all component functions. It provides the component lifecycle API, event system, and provision/consumption system.
Context API Categories
Context Lifecycle Methods
| Method | When Called | Purpose | Return Value |
|---|---|---|---|
refresh(callback?) | User-triggered | Force component re-execution; callback runs before render | Promise<TResult> | TResult |
schedule(callback?) | After reconciliation, before commit | Access rendered nodes before DOM insertion | Promise<void> | void |
after(callback?) | After commit phase | Run code when nodes are live in DOM | Promise<TResult> | void |
cleanup(callback?) | Before unmount | Release resources, can be async for exit animations | Promise<void> | void |
v0.7 Enhancements: All lifecycle methods support async callbacks. refresh(callback) was added in v0.7 to encapsulate state updates. schedule() and cleanup() can now return promises for async mounting/unmounting.
Sources: src/crank.ts README.md413-497 CHANGELOG.md63-133
Crank supports four component patterns based on JavaScript function types. All component functions receive props as the first parameter and a Context instance as this.
Component Type Decision Matrix
| Component Type | State Preservation | Async Operations | Re-renders | Signature |
|---|---|---|---|---|
| Function | ❌ No | ❌ No | On parent update | function(props, ctx) |
| Generator | ✅ Yes | ❌ No | Via refresh() | function*(props, ctx) |
| Async Function | ❌ No | ✅ Yes | On parent update | async function(props, ctx) |
| Async Generator | ✅ Yes | ✅ Yes | Via refresh() or for await | async function*(props, ctx) |
Component Type Detection: Crank introspects the return value of component functions. If it returns an object with a next() method, it's treated as a generator. If it returns a promise, it's treated as async. The renderer in src/crank.ts handles all four types with unified logic.
Sources: README.md119-245 test/types.tsx1-352 CHANGELOG.md143-224
createElement() FunctionJSX syntax is transpiled to createElement() calls. This function in src/crank.ts is the primary interface between JSX and Crank's internal element representation.
JSX Transpilation Example
createElement Signature
Implementation Details
The function performs several normalizations:
props is nullchildren arguments under props.childrenElement instance with the tag and normalized propsThis unwrapping optimization reduces memory overhead since most elements have 0-1 children.
Sources: src/crank.ts docs/blog/2020-10-13-writing-crank-from-scratch.md83-106 docs/blog/2020-10-13-writing-crank-from-scratch.md217-241
Starting in v0.7, Crank uses a two-phase rendering architecture to coordinate async components and eliminate "tearing" (inconsistent UI states):
Phase 1: Reconciliation - Walk element tree, execute components, wait for async siblings to settle together, build complete element tree. No DOM mutations.
Phase 2: Commit - Atomically apply all DOM mutations, arrange children, fire schedule() and after() callbacks.
Benefits of Two-Phase Rendering
| Benefit | Description |
|---|---|
| Eliminates Tearing | Async siblings render together instead of independently |
| Parallel Hydration | Async components hydrate in parallel rather than sequence |
| Consistent States | UI updates are atomic across the entire tree |
| Predictable Timing | Callbacks fire in well-defined phases |
Sources: CHANGELOG.md118-156 High-level diagrams from prompt
Sources: README.md62-117 package.json31-114
| Feature | Crank | React |
|---|---|---|
| Component State | Local variables in function scope | useState() hook |
| Side Effects | Direct code in generator body | useEffect() hook |
| Async Rendering | Native async/await in components | Suspense with cache requirement |
| Reactivity Model | Explicit refresh() calls | Automatic via state setters |
| Lifecycle | Generator control flow (yield, loops, try/finally) | Lifecycle methods / hooks |
| Dependencies | No dependency tracking needed | Manual dependency arrays |
| Re-render Control | Developer calls refresh() | Framework decides when to render |
| Props Iteration | for...of this in generators | Not available |
| Cleanup | cleanup() callback or code after loop | useEffect() cleanup function |
Crank: State and lifecycle are expressed with JavaScript control flow. Components are transparent about when they execute.
React: State and lifecycle are expressed with framework APIs. Components are opaque about when they execute (double-rendering, strict mode, concurrent features).
Sources: docs/blog/2025-08-20-why-be-reactive.md1-372 docs/blog/2020-04-15-introducing-crank.md1-92 CHANGELOG.md36-107
Crank.js is a JSX framework built on the principle that "JavaScript is already a UI runtime." It uses four function types (function, generator, async, async generator) as components, avoiding reactive abstractions in favor of explicit control via this.refresh(). The core abstractions - Element, Renderer, and Context - provide a clean separation between virtual elements, rendering logic, and component lifecycle.
Next Steps:
Refresh this wiki
This wiki was recently refreshed. Please wait 4 days to refresh again.