Components

Basic Components #

Host elements use lowercase tags like <div> and render as their HTML equivalents. Components are functions referenced with capitalized tags — the capitalization tells the JSX compiler to treat the tag as an identifier, not a string.

The simplest kind is a function component. The function receives props as its first argument and its return value is rendered as children.

import {renderer} from "@b9g/crank/dom";
function Greeting({name}) {
return <div>Hello, {name}</div>;
}

renderer.render(<Greeting name="World" />, document.body);

Component children #

Children passed between tags appear as props.children. The component must place them in the returned tree — otherwise they won’t render.

import {renderer} from "@b9g/crank/dom";

function Details({summary, children}) {
return (
<details>
<summary>{summary}</summary>
{children}
</details>
);
}

renderer.render(
<Details summary="Greeting">
<div>Hello world</div>
</Details>,
document.body,
);

The type of children is unknown, e.g. it could be an array, an element, or whatever else the caller passes in.

Stateful Components #

Crank uses generator functions for stateful components. A generator pauses at each yield and resumes where it left off when you call next():

function *fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}

const fib = fibonacci();
fib.next(); // {value: 0}
fib.next(); // {value: 1}
fib.next(); // {value: 1}
fib.next(); // {value: 2}

The variables a and b persist between calls — the generator’s scope is its state. Crank components work the same way: yield JSX instead of numbers, and the framework calls next() when the component re-renders:

import {renderer} from "@b9g/crank/dom";

function *Counter() {
let count = 0;
while (true) {
count++;
yield (
<div>
This component has updated {count} time{count !== 1 && "s"}.
</div>
);
}
}

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

However, while (true) is fragile — a missing yield freezes the page. A better pattern iterates over the component’s context (this), which yields fresh props on each render:

import {renderer} from "@b9g/crank/dom";

function *Counter() {
let count = 0;
for ({} of this) {
count++;
yield (
<div>
This component has updated {count} time{count !== 1 && "s"}.
</div>
);
}
}

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

The for...of loop cannot infinite-loop (it blocks at yield), and it automatically receives new props on each re-render.

Why for ({} of this)? #

The destructuring in for ({name} of this) reassigns the parameter variables from the function head, keeping them current without declaring new ones:

When a component has no props, for ({} of this) advances the iterator with an empty destructuring pattern.

Self-Updating Components with refresh() #

Components update themselves with the refresh() method:

import {renderer} from "@b9g/crank/dom";

function *Timer() {
let seconds = 0;
const interval = setInterval(() => {
seconds++;
this.refresh();
}, 1000);

for ({} of this) {
yield <p>{seconds} second{seconds !== 1 && "s"}</p>;
}

clearInterval(interval);
}

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

The refresh() Callback Pattern #

refresh() accepts a callback that runs before re-rendering, so you can’t forget to pair mutation with refresh:

import {renderer} from "@b9g/crank/dom";

function *Timer({message = "Seconds elapsed:"}) {
let seconds = 0;
const interval = setInterval(() => this.refresh(() => seconds++), 1000);

for ({message} of this) {
yield (
<div>{message} {seconds}</div>
);
}

clearInterval(interval);
}

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

For event handlers: const onclick = () => this.refresh(() => count++);

Alternative Context Syntax #

The context is also passed as the second parameter, for arrow functions or if you prefer not to use this:

function *Timer({message}, ctx) {
let seconds = 0;
const interval = setInterval(() => ctx.refresh(() => seconds++), 1000);

for ({message} of ctx) {
yield (
<div>{message} {seconds}</div>
);
}

clearInterval(interval);
}

Default Props #

Use JavaScript’s default value syntax in the destructuring pattern:

import {renderer} from "@b9g/crank/dom";
function Greeting({name="World"}) {
return <div>Hello, {name}</div>;
}

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

For generator components, you should make sure that you use the same default value in both the parameter list and the loop. A mismatch in the default values for a prop between these two positions may cause surprising behavior.

import {renderer} from "@b9g/crank/dom";
function *Greeting({name="World"}) {
yield <div>Hello, {name}</div>;
for ({name="World"} of this) {
yield <div>Hello again, {name}</div>;
}
}

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