Skip to content

Instantly share code, notes, and snippets.

@grampelberg
Created August 16, 2020 22:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grampelberg/0456008e56f8f3ed0cf20a70b628b479 to your computer and use it in GitHub Desktop.
Save grampelberg/0456008e56f8f3ed0cf20a70b628b479 to your computer and use it in GitHub Desktop.
import _merge from 'lodash/merge'
import { useEffect, useState } from 'react'
import { BehaviorSubject, combineLatest, interval, of, Subject } from 'rxjs'
import { fromFetch } from 'rxjs/fetch'
import {
filter,
map,
materialize,
pairwise,
pluck,
shareReplay,
switchMap,
tap,
} from 'rxjs/operators'
class FetchError extends Error {
constructor(response, body, ...params) {
super(...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, FetchError)
}
this.name = 'FetchError'
_merge(this, response)
try {
this.json = JSON.parse(body)
} catch (err) {
this.text = body
}
}
}
const isComplete = (n) => n.kind === 'C'
const cache = {}
const fetchData = (url) => {
if (url in cache) return cache[url]
const loading = new BehaviorSubject(false).pipe(
pairwise(),
map(([old]) => !old),
)
loading.subscribe()
const data = new Subject().pipe(
tap(loading),
switchMap((url) =>
fromFetch(url, {
selector: async (resp) => _merge(resp, { text: await resp.text() }),
}).pipe(
map((resp) => {
if (!resp.ok) {
throw new FetchError(
resp,
resp.text,
`Unable to fetch: ${resp.url}`,
)
}
return JSON.parse(resp.text)
}),
materialize(),
filter((i) => !isComplete(i)),
),
),
tap(loading),
shareReplay(1),
)
const refresh = combineLatest(of(url), interval(1000)).pipe(
pluck(0),
tap(data),
)
data.subscribe()
data.next(url)
cache[url] = { data, loading, refresh }
return cache[url]
}
// TODO: test caching + loading behavior
// TODO: test caching + refresh
// TODO: should refresh be configurable?
// TODO: is there a way to merge all these subscriptions into a single unsubscribe?
// TODO: is there a way to subscribe to everything *and* do next all at once?
// TODO: the fetcher should probably get split out into its own epic
// TODO: work with a list of URLs and partial results
// TODO: get streaming results to work eg. watch=true
const useFetch = (url, refresh = false) => {
const [data, setData] = useState(undefined)
const [error, setError] = useState(undefined)
const [loading, setLoading] = useState(true)
useEffect(() => {
const { loading, data, refresh: startRefresh } = fetchData(url)
const sub = loading.subscribe(setLoading).add(
data.subscribe((ev) => {
setData(ev.value)
setError(ev.error)
}),
)
if (refresh) {
sub.add(startRefresh.subscribe())
}
return () => sub.unsubscribe()
}, [refresh, url])
return { loading, error, data }
}
export default useFetch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment