---
title: "Firelight.Task"
description: "Run async work from a Lit component and render its pending, complete and error states. F# bindings for @lit/task."
---

# Firelight.Task

Run async work from a component and render its pending, complete and error states.

- [Lit docs: Async tasks](https://lit.dev/docs/data/task/)
- [NuGet 0.3.0](https://www.nuget.org/packages/Firelight.Task/0.3.0)
- [Source](https://github.com/roboz0r/Firelight/tree/main/src/Firelight.Task)

## Why use it

Loading data in a component takes more than making the request. You need a loading state, the
result or the error, a new request when an input changes, and a way to ignore an older request
that finishes after a newer one.

`LitTask` handles all of that. Give it a function that returns a promise and an
`args` function: the task runs whenever the arguments change, renders the component
again as its status changes, and `render` picks the template for the current status. The
task function also receives an `AbortSignal` you can pass to `fetch`, so requests that are no
longer needed get cancelled ([Cancelling requests](https://roboz0r.github.io/Firelight/packages/task/#cancelling-requests)). The F# type is called
`LitTask` so it doesn't clash with .NET's `Task`.

## Example

Each button changes the task's argument, which starts a new request. Product 3 fails, to show the
error state. Clear sets the argument to an empty id, for which the task function returns
`initialState`: the task goes back to its initial state, which `render` shows with its `initial`
template, and no request starts.

<p class="notice">
  <code>args</code> must return a plain JavaScript array. <a href="https://fable.io/">Fable</a> compiles numeric arrays such as
  <code>int[]</code> to typed arrays (<code>Int32Array</code>), which <code>@lit/task</code> rejects,
  so this example uses string ids. Arrays of strings, records or <code>obj</code> are fine.
</p>

```fsharp
module Snippets.ProductTask

open Fable.Core
open Firelight
open Firelight.Task
open type Firelight.Lit

// Stands in for a real request, such as a fetch to your API.
let fetchProduct (id: string) : JS.Promise<string> =
    promise {
        do! Promise.sleep 800

        if id = "3" then
            failwith "Product 3 is out of stock"

        return $"Product {id} is in stock"
    }

let private status =
    StatusRenderer(
        initial = (fun () -> html $"<p>Pick a product.</p>"),
        pending = (fun () -> html $"<p>Loading…</p>"),
        complete = (fun (text: string) -> html $"<p>{text}</p>"),
        error = (fun error -> html $"<p class='error'>{error}</p>")
    )

[<AttachMembers>]
type ProductView() as this =
    inherit LitElement()

    // Product ids are strings: @lit/task needs a plain JS array of args, and Fable compiles int[] to an Int32Array.
    let mutable productId = "1"

    // Runs again whenever productId changes, and re-renders as the request goes
    // from pending to complete or error. With no product, initialState puts the task
    // back in its initial state, without a request. (args always holds one id, but a
    // match must cover every length, so the other lengths go there too.)
    let product =
        LitTask(
            this,
            TaskConfig(
                TaskFunction(fun (args: string[]) _ ->
                    match args with
                    // U2.Case2, not !^: nothing here says if the task's result is the string or the promise.
                    | [| id |] when id <> "" -> U2.Case2(fetchProduct id)
                    | _ -> initialState),
                args = (fun () -> [| productId |])
            )
        )

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 0.5rem; justify-items: start; }
        button { font: inherit; padding: 0.4rem 0.9rem; cursor: pointer; }
        .error { color: #c92a2a; }
        """

    member this.Show(id: string) =
        productId <- id
        this.requestUpdate ()

    member this.ProductButton(id: string) =
        html $"""<button @click={fun _ -> this.Show id}>Product {id}</button>"""

    override this.render() =
        html
            $"""
        <div>{[ "1"; "2"; "3" ] |> List.map this.ProductButton} <button @click={fun _ -> this.Show ""}>Clear</button></div>
        {product.render status |> Option.defaultValue (html $"")}"""

defineElement<ProductView> "my-product-view"
```

```html
<my-product-view></my-product-view>
```

`fetchProduct` builds its promise with `promise { }`, from Fable.Promise, which comes with
Firelight.Task through Fable.Fetch; reference Fable.Promise in your project when your code uses
it. The task function takes the id out of `args` with the pattern `[| id |]`, and wraps the
promise in `U2.Case2`: `!^` would need the task's result type, which nothing in this lambda gives it
([Tasks](https://roboz0r.github.io/Firelight/from-lit/tasks/#loading-when-arguments-change) has the details).

## Cancelling requests

The task function's second argument has one member, `signal`. The task aborts it when the run is
no longer wanted: its arguments changed and a newer run started, or you called `abort`. Pass it to
the request, and the browser cancels the request.

Its type is Fable.Fetch's `AbortSignal`. Firelight.Task depends on
[Fable.Fetch](https://github.com/fable-compiler/fable-fetch), the binding for the browser's
`fetch`, so with `open Fetch` the signal goes straight to `fetch`'s `Signal` option:

```fsharp
open Fable.Core.JsInterop
open Fetch
open Firelight.Task

let loadText (url: string) (signal: AbortSignal) =
    promise {
        let! response = fetch url [ Signal signal ]
        return! response.text ()
    }

let loadPage (args: string[]) (options: TaskFunctionOptions) : TaskResult<string> =
    match args with
    | [| url |] -> !^(loadText url options.signal)
    | _ -> initialState
```

Pass it as `TaskFunction loadPage`.

The value is the browser's own `AbortSignal` object, so any other API that takes a signal accepts
it too. In a binding of your own, such as a hand-written `fetch`, type the parameter as Fable.Fetch's
`AbortSignal`:

```fsharp
open Fable.Core
open Fable.Core.JsInterop
open Firelight.Task

[<Global>]
let fetch (url: string, init: {| signal: Fetch.Types.AbortSignal |}) : JS.Promise<obj> = jsNative

let loadPage (args: string[]) (options: TaskFunctionOptions) : TaskResult<obj> =
    match args with
    | [| url |] -> !^(fetch (url, {| signal = options.signal |}))
    | _ -> initialState
```

Where another library's binding has an `AbortSignal` type of its own, convert at the call with
`unbox options.signal`, which compiles to nothing.

Work that isn't a request can call `options.signal.throwIfAborted ()` after each wait, as
[Search as you type](https://roboz0r.github.io/Firelight/cookbook/debounced-search/) does, or read `options.signal.aborted`.

## Install

```sh
dotnet add package Firelight.Task
npm install @lit/task
```

The first command also installs Fable.Fetch.
