Created
August 16, 2020 22:04
-
-
Save grampelberg/0456008e56f8f3ed0cf20a70b628b479 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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