DEV Community

Cover image for Implementing fetchSignal, async data as signals for Mithtril
artydev
artydev

Posted on

Implementing fetchSignal, async data as signals for Mithtril

Here is an elegant way of fetching data.

export function useFetch(url, options = {}) {
    const data = signal(null)
    const loading = signal(true)
    const error = signal(null)

    const { select, ...fetchOptions } = options

    let controller
    let active = true

    const run = () => {
        controller?.abort()
        controller = new AbortController()

        batch(() => {
            loading(true)
            error(null)
        })

        fetch(url, { ...fetchOptions, signal: controller.signal })
            .then(r => {
                if (!r.ok) {
                    throw new Error(`${r.status} ${r.statusText}`)
                }
                return r.json()
            })
            .then(json => {
                if (!active) return
                batch(() => {
                    data(select ? select(json) : json)
                    loading(false)
                })
            })
            .catch(err => {
                if (!active || err.name === "AbortError") return
                batch(() => {
                    error(err.message)
                    loading(false)
                })
            })
    }

    run()

    return {
        data,
        loading,
        error,
        refetch: run,
        cancel: () => {
            active = false
            controller?.abort()
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Ans its usage:

const Products = {
    oninit(vnode) {
        vnode.state.fetch = useFetch(
            "https://dummyjson.com/products",
            {
                select: r => r.products
            }
        )
    },

    onremove(vnode) {
        vnode.state.fetch.cancel()
    },

    view(vnode) {
        const { data, loading, error } = vnode.state.fetch

        if (loading()) return m("p", "Loading...")
        if (error()) return m("p.error", error())

        return m(".products",
            data().map(p =>
                m(".product", [
                    m("h3", p.title),
                    m("p", `$${p.price}`)
                ])
            )
        )
    }
}

m.mount(document.body, Products)
Enter fullscreen mode Exit fullscreen mode

You can test it here: fetchSignal

Notice what’s missing:

no lifecycle juggling

no redraw logic

no state synchronization

The UI simply reads signals.

Top comments (0)