---
title: "Fetch JSON"
description: "Load JSON with fetch in a Firelight component: Fable.Fetch's fetch, a typed view of the JSON, cancellation with the task's AbortSignal, and loading, error and retry states with Firelight.Task."
---

# Fetch JSON

Build a component that loads a list from a JSON file with `fetch`, shows a loading message, the list or the error, and can load it again.

- [MDN: Using the Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)

Tick the box to ask for a file that isn't there, and the error appears. Untick it, or press Load
again, to load the books.

```fsharp
module Snippets.FetchJson

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

/// A book as the server sends it. Nothing checks that the JSON has these fields.
type Book =
    abstract title: string
    abstract author: string
    abstract year: int

let getBooks (url: string) (signal: AbortSignal) =
    promise {
        // Fails for a response that isn't ok, such as a 404, as well as for no response.
        let! response = fetch url [ Signal signal ]
        return! response.json<Book[]> ()
    }

let private bookList (books: Book[]) =
    html $"""<ul>{books |> Array.map (fun b -> html $"<li><cite>{b.title}</cite>, {b.author} ({b.year})</li>")}</ul>"""

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

    /// Set when leaving the page cancelled a request, so coming back can make it again.
    let mutable interrupted = false

    let books =
        LitTask(
            this,
            TaskConfig(
                TaskFunction(fun (args: string[]) options ->
                    match args with
                    // U2.Case2, not !^: nothing here says if the task's result is the books or the promise.
                    | [| url |] -> U2.Case2(getBooks url options.signal)
                    // Never happens, as args holds one URL, but a match covers every length.
                    | _ -> initialState
                ),
                // A relative URL, next to this page. Yours would be your API's address.
                args = fun () -> [| if this.broken then "no-such-file.json" else "books.json" |]
            )
        )

    static member properties =
        PropertyDeclarations.create [ "broken", PropertyDeclaration<bool>(state = true) ]

    static member styles =
        css
            $$"""
        :host { display: grid; gap: 0.5rem; justify-items: start; }
        p, ul { margin: 0; }
        button { font: inherit; padding: 0.3rem 0.9rem; }
        """

    member val broken = false with get, set

    override this.disconnectedCallback() =
        base.disconnectedCallback ()
        // A task doesn't stop when its component leaves the page; abort the request ourselves.
        if books.status = TaskStatus.PENDING then
            interrupted <- true
            books.abort ()

    override this.connectedCallback() =
        base.connectedCallback ()

        if interrupted then
            interrupted <- false
            books.run () |> ignore

    override this.render() =
        let status =
            StatusRenderer(
                pending = (fun () -> html $"""<p role="status">Loading books…</p>"""),
                complete = bookList,
                error = fun e -> html $"""<p role="alert">The books didn't load. {(unbox<exn> e).Message}</p>"""
            )

        html
            $"""
        <label>
            <input type="checkbox" .checked={this.broken} @change={Ev.checked' (fun on -> this.broken <- on)}>
            Ask for a file that isn't there
        </label>
        <button @click={fun _ -> books.run () |> ignore}>Load again</button>
        {books.render status}"""

defineElement<BookList> "my-book-list"
```

```html
<my-book-list></my-book-list>
```

Like every `LitTask` demo, this one isn't prerendered: the task starts as the component first
updates in the browser, so its first render there shows "Loading" where the build's showed nothing.

## How it works

The request uses `fetch` from [Fable.Fetch](https://github.com/fable-compiler/fable-fetch), the
[Fable](https://fable.io/) binding for the browser's Fetch API. Firelight.Task depends on it, so
`open Fetch` is all the module needs.

- **The request.** `getBooks` is a `promise { }` that awaits, with `let!`, the promises `fetch`
  and `json` return. The `promise` builder is Fable.Promise's, which also comes with Firelight.Task.
  The browser's `fetch` only fails when there's no response at all, and a 404 or a 500 arrives as
  an ordinary response; Fable.Fetch's `fetch` also fails for those, with a message such as
  "404 Not Found for URL …". Use `fetchUnsafe` to read such a response yourself.
- **The JSON.** `Book` is an interface over the parsed objects, so `b.title` compiles to plain
  property access. It types the fields but checks nothing: if the server sends something else,
  the mistake shows up wherever a field is used. When you don't control the server, decode the
  JSON with a library such as [Thoth.Json](https://thoth-org.github.io/Thoth.Json/), as the
  [Kanban sample](https://github.com/roboz0r/Firelight/blob/main/sample/Kanban/Persistence.fs) does.
- **Cancelling.** [`LitTask`](https://roboz0r.github.io/Firelight/packages/task/) passes each run an `AbortSignal`, Fable.Fetch's
  type, and the request passes it on with `Signal signal`. When the arguments change while a
  request is running, the task aborts the signal, and the browser drops the old request.
- **The states.** `StatusRenderer` takes a function for each status: a loading message, the list,
  or the error. Its error is an `obj`, since JavaScript can throw anything; here it's always the
  `Error` that `fetch` or `json` made, so `unbox<exn>` reads its message.
- **Loading again.** `books.run ()` runs the task again with the same arguments, for an error
  that may not happen twice, such as a dropped connection. The button stays on the page in every
  state, so pressing it from the keyboard doesn't lose the focus.
- **Leaving the page.** A task doesn't abort its run when its component is removed, so
  `disconnectedCallback` calls `books.abort ()` if a request is still running. An element can be
  removed and added back, as when a list moves it, so `connectedCallback` runs the interrupted
  request again.

The address is relative, so the file is found next to this page. Point it at your API instead.

## With Elmish

In an app with an Elmish loop, start the same request from a command instead, with
`Cmd.OfPromise.either`, and keep the loading and error states in the model.
[App architecture: Loading fast](https://roboz0r.github.io/Firelight/guides/architecture/#loading-fast-without-server-rendering) shows
one.

## Related

- [Search as you type](https://roboz0r.github.io/Firelight/cookbook/debounced-search/): a task that waits for the user to stop typing.
- [Using JavaScript libraries: Typed bindings](https://roboz0r.github.io/Firelight/guides/js-libraries/#typed-bindings) for
  interfaces over JavaScript objects, such as `Book`.
