Async sample
This page demonstrates an example approach declarative UI to cooperate with async operation naturally.
The example uses a help function named series, which leverages Async Generator Function to allow users write multi-step UI smoothly.
Sample application loads specified user info asynchronously then display that as <dl>.
Explicit resource association is required
Signals and Deriveds/Effects are associated using function call stack by design. This design choice has one big drawback: everything must be a synchronous normal function call.
For example, this code does not work as intended due to the changes to $name will not re-run e1.
const $name = signal("Alice");
// 1. global stack = []
const e1 = effect(() => {
// 2. global stack = [e1]
const tid = setTimeout(() => {
// 4. global stack = []
// next loop = outside of `e1`
// When this line is executed, `e1` is popped from global stack.
// Therefore, `$name.get()` would be orphan and changes to `$name` does not
// re-run `e1`.
console.log($name.get());
}, 500);
return () => {
clearTimeout(tid);
};
});
// 3. global stack = []
There are two way to tackle this problem.
Retrieve value early
If you call .get() eagerly (when the call stack is not emptied yet), there is nothing to care about.
// ...
const e1 = effect(() => {
// global stack = [e1]
// Hence `$name` is associated to `e1`
const name = $name.get();
const tid = setTimeout(() => {
console.log(name);
}, 500);
return () => {
clearTimeout(tid);
};
});
Manually specify dependant effect
effect function passes itself as a first argument to the callback.
When Signal.prototype.get() and Derived.prototype.get() saw the first parameter, they treat the first parameter as a dependant instead of the global stack.
By using those API, you can explicitly specify which value re-runs which effect on value changes.
const e1 = effect((ctx) => {
const tid = setTimeout(() => {
// global stack is empty, but `.get(ctx)` refers the first argument
// instead of global stack so `$name` is associated to `e1`.
console.log($name.get(ctx));
}, 500);
return () => {
clearTimeout(tid);
};
});
While this approach is more flexible as it allows values to associate themselves to an effect lazily, this could be more error-prone because it’s easy to forget put the context to the first parameter. I recommend you to carefully evaluate all three approaches before integrating into your app.
The helper function in this example page uses this approach, mainly for demonstrating lazy association.
Alternative
Use asyncDerived. It’s the most straightforward way to represent async procedure.
import { asyncDerived } from "@pocka/ef";
const $myApiResult = asyncDerived(async () => {
const resp = await fetch("/my/api");
if (resp.status !== 200) {
throw new Error(`Unexpected status code (HTTP ${resp.status})`);
}
return resp.json();
});
const myUI = derived(() => {
const myApiResult = $myApiResult.get();
if (!myApiResult.isSettled) {
return el("p", [], ["Fetching"]);
}
if (myApiResult.isRejected) {
return el("p", [], ["ERROR: " + String(myApiResult.error)]);
}
return el("div", [], [doSomethingWithApiData(myApiResult.data)]);
});