Async data in Reactor — `UseResource` (single fetch), `UseMutation` (writes + optimistic updates), `UseInfiniteResource` (cursor pagination), and `Pending` (bubble-up fallback). Use when fetching, caching, paginating, or mutating server data.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Async data in Reactor — `UseResource` (single fetch), `UseMutation` (writes + optimistic updates), `UseInfiniteResource` (cursor pagination), and `Pending` (bubble-up fallback). Use when fetching, caching, paginating, or mutating server data.
Async Resources in Reactor
Reactor replaces the UseState + UseEffect + manual-cancellation dance with
three hooks. Reads return AsyncValue<T>, writes return a Mutation
handle. The hook owns the cancellation token, dedups across siblings,
caches results in a shared QueryCache, and drops late results on unmount.
Available on any Component subclass directly; on RenderContext as
extension methods for function components (Memo(ctx => ...) or
RenderEachTime(ctx => ...)).
AsyncValue<T> is a sealed 4-state record: Loading, Data(T Value),
Error(Exception), Reloading(T Previous) (stale-while-revalidate).
Pattern-match with a switch:
classUserCard : Component
{
Element ()
{
user = UseResource(
ct => Api.GetUserAsync(userId, ct),
deps: [] { userId });
user
{
AsyncValue<User>.Loading => TextBlock(),
AsyncValue<User>.Data d => TextBlock(d.Value.Name).Bold(),
AsyncValue<User>.Reloading r => TextBlock(r.Previous.Name).Opacity(),
AsyncValue<User>.Error e => TextBlock(),
_ => Empty()
};
}
}
public
override
Render
var
new
object
return
switch
"Loading…"
0.5
$"Error: {e.Exception.Message}"
Or use user.Match<Element>(loading, data, error, reloading?) — reloading
defaults to data so stale values stay visible.
Rules:
Fetcher must be idempotent (reads only — the analyzer REACTOR_HOOKS_006
warns on names like Post*, Create*, Delete*, Update*, Save*, Send*).
For writes, use UseMutation.
Fetcher must observe ct — return a cancelled task or check
ct.IsCancellationRequested. Cancellation fires on deps change, unmount,
and cache invalidation.
deps must be value-comparable. Don't pass new lambdas or freshly
constructed collections each render — memoize with UseMemo or use
scalar values.
ResourceOptions (all optional):
Option
Default
Meaning
StaleTime
0s
Within this window, return Data without refetching. After, enter Reloading and refetch.
CacheTime
5m
Keep entry in cache this long after zero subscribers.
The returned Mutation is stable across renders. Properties:
IsPending, Error?, LastResult?. Call mutation.RunAsync(input) from
click handlers; call mutation.Reset() to clear error/result.
var (todos, setTodos) = UseState<IReadOnlyList<Todo>>([]);
var addTodo = UseMutation<TodoInput, Todo>(
mutator: (input, ct) => Api.AddTodoAsync(input, ct),
options: new MutationOptions<TodoInput, Todo>(
// Synchronous — lands in the next render before the network call.
OnOptimistic: input =>
setTodos([.. todos, new Todo(input.Title, IsTemporary: true)]),
OnSuccess: (todo, _) =>
setTodos([.. todos.Select(t => t.IsTemporary ? todo : t)]),
OnError: (_, input) =>
setTodos([.. todos.Where(t => t.Title != input.Title)]),
// Any sibling UseResource subscribed to these keys refetches.
InvalidateKeys: ["todos/list"]));
return VStack(
Button("Add", () => _ = addTodo.RunAsync(new TodoInput("New")))
.IsEnabled(!addTodo.IsPending));
OnOptimistic runs synchronously on the caller. If it throws, the mutator
never fires — prevents half-applied state.
InvalidateKeys fires on success only, not on error.
Overlapping RunAsync calls are allowed; each has its own token. Gate
with IsPending if you need serial semantics.
Unmount cancels the mutator's token. OnError is silent on cancellation.
Pull model. The virtualizer calls resource.ItemAt(i) per visible row;
the hook fetches whichever page covers i, dedups, and caches. Unfetched
or in-flight slots return null (render a shimmer).
Loading / Idle / EndOfList / Error(ex) — aggregate of most recent page fetch.
ItemAt(index)
Get item; triggers page fetch if uncached.
EnsureRange(first, last)
Batch prefetch — call from onVisibleRangeChanged.
FetchNext()
Manually fetch the next page.
Retry()
Retry the most recent failed page only.
Refresh()
Invalidate all pages and refetch from page 0 (pull-to-refresh).
Footer UI off LoadState:
commits.LoadState switch
{
LoadState.Loading => ProgressRing(),
LoadState.Idle when !commits.HasMore => TextBlock("End of list"),
LoadState.Error e => Button("Retry", () => commits.Retry()),
_ => Empty()
}
Notes:
Cursor paging is serial — page N-1 must land before page N (the cursor
lives in N-1's payload). For parallel offset paging, use an offset as
the cursor type.
Deps change cancels all in-flight pages, unsubscribes cache keys, and
restarts from page 0.
4. Pending — bubble-up fallback
Render a single fallback for a subtree whose components each load
independent resources:
PendingFactory.Pending(
fallback: TextBlock("Loading dashboard…").Opacity(0.5),
child: VStack(
Component<UserHeader>(), // has UseResource
Component<RecentActivity>(),// has UseInfiniteResource
Component<Stats>() // has UseResource
))
Both trees mount — the child's hooks run in the background.
Fallback shows while any nested resource is in Loading.
Reloading (refetch with data present) does not re-trigger the
fallback. No flash on revalidation.
5. Gotchas
Non-idempotent fetcher (REACTOR_HOOKS_006). The analyzer flags
UseResource(ct => api.PostComment(...)). Reads retry and refetch; use
UseMutation for writes.
Unstable deps.deps: new object[] { userId } allocates a fresh array
each render — the deps-change detector compares element-wise, so scalar
userId is fine, but a fresh new Filter { ... } object is not.
Memoize complex deps:
Don't share CacheKey across unrelated hooks. Auto-key keeps siblings
independent. Explicit CacheKey is for two distant subtrees that should
observe the same entry.
Live-mutation overlay.UseInfiniteResource.Items is server-sourced
and immutable — the hook re-renders when pages land, not when you edit a
row. Keep optimistic edits in a separate Dictionary<int, T> overlay that
takes precedence over Items[i] at read time.
AsyncValue.Match allocates one delegate per arm. In per-row render
paths (inside a virtualized list), prefer a switch expression for
exhaustiveness checking and zero per-call allocation.