Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/docs/api-reference.recho.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* - echo(...values) - Echo values inline with your code as comments (https://recho.dev/notebook/docs/api-echo)
* - echo.clear() - Clear the output of the current block (https://recho.dev/notebook/docs/api-echo-clear)
* - invalidation() - Promise that resolves before re-running the current block (https://recho.dev/notebook/docs/api-invalidation)
* - recho.state(value) - Create reactive state variables for mutable values (https://recho.dev/notebook/docs/api-state)
* - recho.inspect(value[, options]) - Format values for inspection (https://recho.dev/notebook/docs/api-inspect)
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
72 changes: 72 additions & 0 deletions app/docs/api-state.recho.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @title recho.state(value)
*/

/**
* ============================================================================
* = recho.state(value) =
* ============================================================================
*
* Creates a reactive state variable that can be mutated over time. This is
* similar to React's useState hook and enables mutable reactive values that
* automatically trigger re-evaluation of dependent blocks when changed.
*
* @param {any} value - The initial state value.
* @returns {[any, Function, Function]} A tuple containing:
* - state: The reactive state value that can be read directly
* - setState: Function to update the state (accepts value or updater function)
* - getState: Function to get the current state value
*/

// Basic counter that increments after 1 second
const [count1, setCount1] = recho.state(0);

setTimeout(() => {
setCount1(count1 => count1 + 1);
}, 1000);

//➜ 1
echo(count1);

// Timer that counts down from 10
const [timer, setTimer] = recho.state(10);

{
const interval = setInterval(() => {
setTimer(t => {
if (t <= 0) {
clearInterval(interval);
return 0;
}
return t - 1;
});
}, 1000);

invalidation.then(() => clearInterval(interval));
}

//➜ 8
echo(`Time remaining: ${timer}s`);

// State can be updated with a direct value
const [message, setMessage] = recho.state("Hello");

setTimeout(() => {
setMessage("Hello, World!");
}, 2000);

//➜ "Hello, World!"
echo(message);

// Multiple states can be used together
const [firstName, setFirstName] = recho.state("John");
const [lastName, setLastName] = recho.state("Doe");

setTimeout(() => {
setFirstName("Jane");
setLastName("Smith");
}, 1500);

//➜ "Jane Smith"
echo(`${firstName} ${lastName}`);

4 changes: 4 additions & 0 deletions app/docs/nav.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export const docsNavConfig = [
type: "page",
slug: "api-invalidation",
},
{
type: "page",
slug: "api-state",
},
{
type: "page",
slug: "api-inspect",
Expand Down
1 change: 1 addition & 0 deletions runtime/stdlib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export {now} from "./now.js";
export {interval} from "./interval.js";
export {inspect, Inspector} from "./inspect.js";
export * from "../controls/index.js";
export {state} from "./state.js";
37 changes: 37 additions & 0 deletions runtime/stdlib/observe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Derived from Observable Notebook Kit's observe.
// https://github.com/observablehq/notebook-kit/blob/main/src/runtime/stdlib/generators/observe.ts

export async function* observe(initialize) {
let resolve = undefined;
let value = undefined;
let stale = false;

const dispose = initialize((x) => {
value = x;
if (resolve) {
resolve(x);
resolve = undefined;
} else {
stale = true;
}
return x;
});

if (dispose != null && typeof dispose !== "function") {
throw new Error(
typeof dispose === "object" && "then" in dispose && typeof dispose.then === "function"
? "async initializers are not supported"
: "initializer returned something, but not a dispose function",
);
}

try {
while (true) {
yield stale ? ((stale = false), value) : new Promise((_) => (resolve = _));
}
} finally {
if (dispose != null) {
dispose();
}
}
}
26 changes: 26 additions & 0 deletions runtime/stdlib/state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Derived from Observable Notebook Kit's mutable and mutator.
// https://github.com/observablehq/notebook-kit/blob/main/src/runtime/stdlib/mutable.ts
import {observe} from "./observe.js";

// Mutable returns a generator with a value getter/setting that allows the
// generated value to be mutated. Therefore, direct mutation is only allowed
// within the defining cell, but the cell can also export functions that allows
// other cells to mutate the value as desired.
function Mutable(value) {
let change = undefined;
const mutable = observe((_) => {
change = _;
if (value !== undefined) change(value);
});
return Object.defineProperty(mutable, "value", {
get: () => value,
set: (x) => ((value = x), void change?.(value)),
});
}

export function state(value) {
const state = Mutable(value);
const setState = (x) => (typeof x === "function" ? (state.value = x(state.value)) : (state.value = x));
const getState = () => state.value;
return [state, setState, getState];
}
1 change: 1 addition & 0 deletions test/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {mandelbrotSet} from "./mandelbrot-set.js";
export {matrixRain} from "./matrix-rain.js";
export {jsDocString} from "./js-doc-string.js";
export {commentLink} from "./comment-link.js";
export {mutable} from "./mutable.js";
export {syntaxError3} from "./syntax-error3.js";
export {syntaxError4} from "./syntax-error4.js";
export {nonCallEcho} from "./non-call-echo.js";
Expand Down
8 changes: 8 additions & 0 deletions test/js/mutable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const mutable = `const [a, setA] = recho.state(0);

setTimeout(() => {
setA((a) => a + 1);
}, 1000);

echo(a);
`;
1 change: 1 addition & 0 deletions test/stdlib.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ it("should export expected functions from stdlib", () => {
expect(stdlib.toggle).toBeDefined();
expect(stdlib.number).toBeDefined();
expect(stdlib.radio).toBeDefined();
expect(stdlib.state).toBeDefined();
});