Task API
Declared by <huxerui/task.h>.
Task<T> and Task<void>
Section titled “Task<T> and Task<void>”Move-only, lazy coroutine return types. A task starts when launched or awaited. Only another HuxerUI task may co_await it. Exceptions are retained and rethrown on resume; a non-void task must co_return a value.
TaskScope
Section titled “TaskScope”UseTaskScope() returns the current component’s scope. Launch(Task<void>&&) and the factory overload return TaskHandle. Outstanding executions are canceled when the owning scope is destroyed.
TaskHandle
Section titled “TaskHandle”Copyable weak cancellation handle. Cancel() is safe after completion or prior cancellation.
Delay(std::chrono::duration<double>) -> Task<void> schedules resumption through the owning UI task system. Standard chrono duration literals are re-exported in huxerui.
Complete scoped-task example
Section titled “Complete scoped-task example”#include <huxerui/huxerui.h>
using namespace huxerui;
Task<void> Refresh(State<std::string> status) { status = "Loading"; co_await Delay(std::chrono::milliseconds(100)); status = "Ready";}
[[huxerui::composable]]View RefreshButton(State<std::string> status) { auto tasks = UseTaskScope(); return Button("Refresh").OnClick([tasks, status] { tasks.Launch(Refresh(status)); });}TaskScope owns launched work; destroying its component scope cancels outstanding executions before captured state can outlive that scope.
CPU work and external callbacks
Section titled “CPU work and external callbacks”RunWorker(function, arguments...) creates a lazy task with owned, decayed arguments. It runs on a bounded worker pool and resumes the awaiting HuxerUI task on its UI thread. Never read/write State, compose Views, or access UI-affine platform objects inside the worker.
#include <huxerui/huxerui.h>
#include <algorithm>#include <string>
using namespace huxerui;
Task<void> Normalize(std::string input, State<std::string> output) { output = co_await RunWorker([](std::string value) { value.erase(std::remove(value.begin(), value.end(), '\r'), value.end()); return value; }, input);}Launch from a user action or committed Lifecycle, never during composition. For large inputs, choose an ownership transfer deliberately instead of copying buffers repeatedly.
TaskScope::Post(callback) is for an external thread’s callback entering the UI task scope. Queued work is discarded if the scope closes. Awaited Delay, HTTP, Files, and RunWorker already restore UI execution and need no extra Post.
WorkerSequence::Run(function, arguments...) serializes calls sharing the sequence. Its function receives std::stop_token first. It guarantees order, not a dedicated thread; active cancellation is cooperative and the next call waits for the previous call and its capture cleanup to finish. A worker must inspect its token to stop lengthy processing.
Cancelling pending work can prevent execution; it cannot forcibly interrupt arbitrary active C++. Thread-worker facilities may be unavailable in a Web build without worker support; report that capability instead of blocking the browser thread.
Cancellation is not a catchable completion
Section titled “Cancellation is not a catchable completion”TaskHandle::Cancel() destroys suspended coroutine state on the owning UI thread; it does not resume the coroutine with a cancellation exception. Code after the current co_await will not run merely to reset a loading flag. Let the action that cancels work also publish the intended UI state, or let a replacement request own that state. Use RAII for resources whose cleanup must happen when the coroutine frame is destroyed.
Destroying a TaskHandle does not cancel the task. Store it when an action or Lifecycle cleanup needs explicit cancellation; otherwise TaskScope owns it until completion or scope teardown. An empty/already-completed handle is inert. A Task itself is move-only and single-consumer: do not await the same operation twice or use it as a reusable future.
Replace work when an input changes
Section titled “Replace work when an input changes”This complete component starts only after a committed dependency change. Cleanup cancels the previous delayed lookup before the next one starts:
#include <huxerui/huxerui.h>
#include <chrono>#include <string>
using namespace huxerui;
[[huxerui::composable]]View DelayedLookup() { auto query = UseState(TextEditingValue::FromText("")); auto result = UseState<std::string>("Enter a query"); auto tasks = UseTaskScope(); const std::string text = query->text; Lifecycle([tasks, result, text] { result = text.empty() ? "Enter a query" : "Waiting..."; auto request = tasks.Launch([result, text]() -> Task<void> { if (text.empty()) { co_return; } co_await Delay(std::chrono::milliseconds{300}); result = "Result for: " + text; }); return [request] { request.Cancel(); }; }, text); return Column { TextField(query) .Label("Query") .OnChanged([query](const TextEditingValue& next) { query = next; }), Text(result), }.With(Spacing(8.0F));}The dependency is text, so moving the caret alone does not restart a lookup. For real HTTP, replace the Delay and simulated result with Send and handle both transport errors and application status. Scope cancellation prevents the old HuxerUI continuation from publishing a result; it cannot undo a server-side mutation that already happened.
Exceptions and ownership at launch
Section titled “Exceptions and ownership at launch”Launch(Task<void>&&) transfers a named coroutine result. Launch(factory) owns the callable until its coroutine finishes, avoiding a dangling capturing-coroutine lambda. Do not immediately invoke a temporary capturing coroutine lambda and pass its result as though its closure would live forever.
Launching during composition, after a scope closes, or with an empty Task is a logic error. Fallible application work should catch and present expected exceptions inside the launched operation; do not use an unobserved launch as an error-reporting strategy. Service APIs may also validate synchronously before returning their lazy Task, so put construction of a fallible request inside the intended error boundary.

