Skip to content

Commit 6237784

Browse files
authored
Add recho.state(value) (#114)
* Add mutable * Expose __Mutator__ * Add Observable Notebook Kit attribution * Rename to recho.state * Add docs * Update state order
1 parent 03acf3c commit 6237784

File tree

9 files changed

+151
-0
lines changed

9 files changed

+151
-0
lines changed

app/docs/api-reference.recho.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* - echo(...values) - Echo values inline with your code as comments (https://recho.dev/notebook/docs/api-echo)
2121
* - echo.clear() - Clear the output of the current block (https://recho.dev/notebook/docs/api-echo-clear)
2222
* - invalidation() - Promise that resolves before re-running the current block (https://recho.dev/notebook/docs/api-invalidation)
23+
* - recho.state(value) - Create reactive state variables for mutable values (https://recho.dev/notebook/docs/api-state)
2324
* - recho.inspect(value[, options]) - Format values for inspection (https://recho.dev/notebook/docs/api-inspect)
2425
*
2526
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

app/docs/api-state.recho.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @title recho.state(value)
3+
*/
4+
5+
/**
6+
* ============================================================================
7+
* = recho.state(value) =
8+
* ============================================================================
9+
*
10+
* Creates a reactive state variable that can be mutated over time. This is
11+
* similar to React's useState hook and enables mutable reactive values that
12+
* automatically trigger re-evaluation of dependent blocks when changed.
13+
*
14+
* @param {any} value - The initial state value.
15+
* @returns {[any, Function, Function]} A tuple containing:
16+
* - state: The reactive state value that can be read directly
17+
* - setState: Function to update the state (accepts value or updater function)
18+
* - getState: Function to get the current state value
19+
*/
20+
21+
// Basic counter that increments after 1 second
22+
const [count1, setCount1] = recho.state(0);
23+
24+
setTimeout(() => {
25+
setCount1(count1 => count1 + 1);
26+
}, 1000);
27+
28+
//➜ 1
29+
echo(count1);
30+
31+
// Timer that counts down from 10
32+
const [timer, setTimer] = recho.state(10);
33+
34+
{
35+
const interval = setInterval(() => {
36+
setTimer(t => {
37+
if (t <= 0) {
38+
clearInterval(interval);
39+
return 0;
40+
}
41+
return t - 1;
42+
});
43+
}, 1000);
44+
45+
invalidation.then(() => clearInterval(interval));
46+
}
47+
48+
//➜ 8
49+
echo(`Time remaining: ${timer}s`);
50+
51+
// State can be updated with a direct value
52+
const [message, setMessage] = recho.state("Hello");
53+
54+
setTimeout(() => {
55+
setMessage("Hello, World!");
56+
}, 2000);
57+
58+
//➜ "Hello, World!"
59+
echo(message);
60+
61+
// Multiple states can be used together
62+
const [firstName, setFirstName] = recho.state("John");
63+
const [lastName, setLastName] = recho.state("Doe");
64+
65+
setTimeout(() => {
66+
setFirstName("Jane");
67+
setLastName("Smith");
68+
}, 1500);
69+
70+
//➜ "Jane Smith"
71+
echo(`${firstName} ${lastName}`);
72+

app/docs/nav.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export const docsNavConfig = [
5959
type: "page",
6060
slug: "api-invalidation",
6161
},
62+
{
63+
type: "page",
64+
slug: "api-state",
65+
},
6266
{
6367
type: "page",
6468
slug: "api-inspect",

runtime/stdlib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export {now} from "./now.js";
33
export {interval} from "./interval.js";
44
export {inspect, Inspector} from "./inspect.js";
55
export * from "../controls/index.js";
6+
export {state} from "./state.js";

runtime/stdlib/observe.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Derived from Observable Notebook Kit's observe.
2+
// https://github.com/observablehq/notebook-kit/blob/main/src/runtime/stdlib/generators/observe.ts
3+
4+
export async function* observe(initialize) {
5+
let resolve = undefined;
6+
let value = undefined;
7+
let stale = false;
8+
9+
const dispose = initialize((x) => {
10+
value = x;
11+
if (resolve) {
12+
resolve(x);
13+
resolve = undefined;
14+
} else {
15+
stale = true;
16+
}
17+
return x;
18+
});
19+
20+
if (dispose != null && typeof dispose !== "function") {
21+
throw new Error(
22+
typeof dispose === "object" && "then" in dispose && typeof dispose.then === "function"
23+
? "async initializers are not supported"
24+
: "initializer returned something, but not a dispose function",
25+
);
26+
}
27+
28+
try {
29+
while (true) {
30+
yield stale ? ((stale = false), value) : new Promise((_) => (resolve = _));
31+
}
32+
} finally {
33+
if (dispose != null) {
34+
dispose();
35+
}
36+
}
37+
}

runtime/stdlib/state.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Derived from Observable Notebook Kit's mutable and mutator.
2+
// https://github.com/observablehq/notebook-kit/blob/main/src/runtime/stdlib/mutable.ts
3+
import {observe} from "./observe.js";
4+
5+
// Mutable returns a generator with a value getter/setting that allows the
6+
// generated value to be mutated. Therefore, direct mutation is only allowed
7+
// within the defining cell, but the cell can also export functions that allows
8+
// other cells to mutate the value as desired.
9+
function Mutable(value) {
10+
let change = undefined;
11+
const mutable = observe((_) => {
12+
change = _;
13+
if (value !== undefined) change(value);
14+
});
15+
return Object.defineProperty(mutable, "value", {
16+
get: () => value,
17+
set: (x) => ((value = x), void change?.(value)),
18+
});
19+
}
20+
21+
export function state(value) {
22+
const state = Mutable(value);
23+
const setState = (x) => (typeof x === "function" ? (state.value = x(state.value)) : (state.value = x));
24+
const getState = () => state.value;
25+
return [state, setState, getState];
26+
}

test/js/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {mandelbrotSet} from "./mandelbrot-set.js";
1414
export {matrixRain} from "./matrix-rain.js";
1515
export {jsDocString} from "./js-doc-string.js";
1616
export {commentLink} from "./comment-link.js";
17+
export {mutable} from "./mutable.js";
1718
export {syntaxError3} from "./syntax-error3.js";
1819
export {syntaxError4} from "./syntax-error4.js";
1920
export {nonCallEcho} from "./non-call-echo.js";

test/js/mutable.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const mutable = `const [a, setA] = recho.state(0);
2+
3+
setTimeout(() => {
4+
setA((a) => a + 1);
5+
}, 1000);
6+
7+
echo(a);
8+
`;

test/stdlib.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ it("should export expected functions from stdlib", () => {
88
expect(stdlib.toggle).toBeDefined();
99
expect(stdlib.number).toBeDefined();
1010
expect(stdlib.radio).toBeDefined();
11+
expect(stdlib.state).toBeDefined();
1112
});

0 commit comments

Comments
 (0)