diff --git a/README.md b/README.md index bb109676..fee265c9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ - - - +[![use-http logo][3]][5]
@@ -875,10 +873,19 @@ Does your company use use-http? Consider sponsoring the project to fund new feat

+Browser Support +--------------- + +If you need support for IE, you will need to add additional polyfills. The React docs suggest [these polyfills][4], but from [this issue][2] we have found it to work fine with the [`react-app-polyfill`]. If you have any updates to this browser list, please submit a PR! + +| [IE / Edge]()
Edge | [Firefox]()
Firefox | [Chrome]()
Chrome | [Safari]()
Safari | [Opera]()
Opera | +| --------- | --------- | --------- | --------- | --------- | +| 12+ | last 2 versions| last 2 versions| last 2 versions| last 2 versions | + Feature Requests/Ideas ---------------------- -If you have feature requests, let's talk about them in [this issue](https://github.com/alex-cory/use-http/issues/13)! +If you have feature requests, [submit an issue][1] to let us know what you would like to see! Todos ------ @@ -893,7 +900,6 @@ Todos - snapshot test resources: [swr](https://github.com/zeit/swr/blob/master/test/use-swr.test.tsx#L1083), [react-apollo-hooks](https://github.com/trojanowski/react-apollo-hooks/blob/master/src/__tests__/useQuery-test.tsx#L218) - basic test resources: [fetch-suspense](https://github.com/CharlesStover/fetch-suspense/blob/master/tests/create-use-fetch.test.ts), [@testing-library/react-hooks suspense PR](https://github.com/testing-library/react-hooks-testing-library/pull/35/files) - [ ] maybe add translations [like this one](https://github.com/jamiebuilds/unstated-next) -- [ ] add browser support to docs [1](https://github.com/godban/browsers-support-badges) [2](https://gist.github.com/danbovey/b468c2f810ae8efe09cb5a6fac3eaee5) (currently does not support ie 11) - [ ] maybe add contributors [all-contributors](https://github.com/all-contributors/all-contributors) - [ ] add sponsors [similar to this](https://github.com/carbon-app/carbon) - [ ] Error handling @@ -908,6 +914,7 @@ Todos - [ ] aborts fetch on unmount - [ ] take a look at how [react-apollo-hooks](https://github.com/trojanowski/react-apollo-hooks) work. Maybe ad `useSubscription` and `const request = useFetch(); request.subscribe()` or something along those lines - [ ] make this a github package + - [see ava packages](https://github.com/orgs/ava/packages) - [ ] get it all working on a SSR codesandbox, this way we can have api to call locally - [ ] make GraphQL examples in codesandbox - [ ] Documentation: @@ -930,16 +937,6 @@ Todos .get() ``` -- [ ] maybe add snake_case -> camelCase option to ``. This would - convert all the keys in the response to camelCase. - Not exactly sure how this syntax should look because what - if you want to have this only go 1 layer deep into the response - object. Or if this is just out of scope for this library. - - ```jsx - - ``` - - [ ] potential option ideas ```jsx @@ -1042,3 +1039,11 @@ const App = () => { } ``` + + +[1]: https://github.com/alex-cory/use-http/issues/new?title=[Feature%20Request]%20YOUR_FEATURE_NAME +[2]: https://github.com/alex-cory/use-http/issues/93#issuecomment-600896722 +[3]: https://github.com/alex-cory/use-http/raw/master/public/dog.png +[4]: https://reactjs.org/docs/javascript-environment-requirements.html +[5]: http://use-http.com +[`react-app-polyfill`]: https://www.npmjs.com/package/react-app-polyfill \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 2b9273cd..ac29d428 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ - +![use-http logo][3]

useFetch

@@ -821,6 +821,7 @@ useFetch(options) Who's using use-http? ===================== +
@@ -836,11 +837,23 @@ Who's using use-http?
+Browser Support +=============== + +If you need support for IE, you will need to add additional polyfills. The React docs suggest [these polyfills][4], but from [this issue][2] we have found it to work fine with the [`react-app-polyfill`]. If you have any updates to this browser list, please submit a PR! + +| [IE / Edge]()
Edge | [Firefox]()
Firefox | [Chrome]()
Chrome | [Safari]()
Safari | [Opera]()
Opera | +| --------- | --------- | --------- | --------- | --------- | +| 12+ | last 2 versions| last 2 versions| last 2 versions| last 2 versions | + Feature Requests/Ideas ====================== -If you have feature requests, let's talk about them in [this issue](https://github.com/alex-cory/use-http/issues/13)! - +``` + +--> + +[1]: https://github.com/alex-cory/use-http/issues/new?title=[Feature%20Request]%20YOUR_FEATURE_NAME +[2]: https://github.com/alex-cory/use-http/issues/93#issuecomment-600896722 +[3]: https://github.com/alex-cory/use-http/raw/master/public/dog.png +[4]: https://reactjs.org/docs/javascript-environment-requirements.html +[`react-app-polyfill`]: https://www.npmjs.com/package/react-app-polyfill \ No newline at end of file diff --git a/src/__tests__/useFetch.test.tsx b/src/__tests__/useFetch.test.tsx index c34ac377..dc180fe1 100644 --- a/src/__tests__/useFetch.test.tsx +++ b/src/__tests__/useFetch.test.tsx @@ -698,6 +698,7 @@ describe('useFetch - BROWSER - suspense', (): void => { afterEach((): void => { fetch.resetMocks() cleanup() + test.cleanup() }) @@ -939,7 +940,6 @@ describe('useFetch - BROWSER - persistence', (): void => { expect(fetch).toHaveBeenCalledTimes(1) }) - it('should have `cache` in the return of useFetch', async (): Promise => { const { result } = renderHook( () => useFetch({ url: 'https://persist.com', persist: true }, []) @@ -953,6 +953,7 @@ describe('useFetch - BROWSER - persistence', (): void => { }) it('should error if passing wrong cachePolicy with persist: true', async (): Promise => { + try { const { result } = renderHook( () => useFetch({ url: 'https://persist.com', persist: true, cachePolicy: NO_CACHE }, []) @@ -960,7 +961,7 @@ describe('useFetch - BROWSER - persistence', (): void => { expect(result.current.error).toBe(undefined) } catch (err) { expect(err.name).toBe('Invariant Violation') - expect(err.message).toBe(`You cannot use option 'persist' with cachePolicy: no-cache πŸ™…β€β™‚οΈ`) + expect(err.message).toBe('You cannot use option \'persist\' with cachePolicy: no-cache πŸ™…β€β™‚οΈ') } try { @@ -970,7 +971,7 @@ describe('useFetch - BROWSER - persistence', (): void => { expect(result.current.error).toBe(undefined) } catch (err) { expect(err.name).toBe('Invariant Violation') - expect(err.message).toBe(`You cannot use option 'persist' with cachePolicy: network-only πŸ™…β€β™‚οΈ`) + expect(err.message).toBe('You cannot use option \'persist\' with cachePolicy: network-only πŸ™…β€β™‚οΈ') } }) }) diff --git a/src/doFetchArgs.ts b/src/doFetchArgs.ts index 77ace3cb..267bcff1 100644 --- a/src/doFetchArgs.ts +++ b/src/doFetchArgs.ts @@ -88,7 +88,7 @@ export default async function doFetchArgs ( // TODO: see if `Object.entries` is supported for IE // TODO: if the body is a file, and this is a large file, it might exceed the size - // limit of the key size in the Map + // limit of the key size. Potential solution: base64 the body // used to tell if a request has already been made const responseID = Object.entries({ url, method, body: options.body || '' }) .map(([key, value]) => `${key}:${value}`).join('||') diff --git a/src/storage/localStorage.ts b/src/storage/localStorage.ts index e3294159..81040f08 100644 --- a/src/storage/localStorage.ts +++ b/src/storage/localStorage.ts @@ -12,32 +12,26 @@ const getCache = () => { } } const getLocalStorage = ({ cacheLife }: { cacheLife: number }): Cache => { - // there isn't state here now, but will be eventually - - const remove = async (name: string) => { + const isExpired = (responseID: string) => { const cache = getCache() - delete cache[name] - localStorage.setItem(cacheName, JSON.stringify(cache)) + const { expiration, response } = (cache[responseID] || {}) + const expired = expiration> 0 && expiration < Date.now() + if (expired) remove(responseID) + return expired || !response } - const has = async (responseID: string): Promise => { + const remove = async (...responseIDs: string[]) => { const cache = getCache() - return !!(cache[responseID] && cache[responseID].response) + responseIDs.forEach(id => delete cache[id]) + localStorage.setItem(cacheName, JSON.stringify(cache)) } - const get = async (responseID: string): Promise => { - const cache = getCache() - if (!cache[responseID]) { - return - } - - const { expiration, response: { body, headers, status, statusText } } = cache[responseID] as any - if (expiration < Date.now()) { - delete cache[responseID] - localStorage.setItem(cacheName, JSON.stringify(cache)) - return - } + const has = async (responseID: string) => !isExpired(responseID) + const get = async (responseID: string) => { + if (isExpired(responseID)) return + const cache = getCache() + const { body, headers, status, statusText } = cache[responseID].response return new Response(body, { status, statusText, @@ -47,9 +41,8 @@ const getLocalStorage = ({ cacheLife }: { cacheLife: number }): Cache => { const set = async (responseID: string, response: Response): Promise => { const cache = getCache() - const responseObject = await serializeResponse(response) cache[responseID] = { - response: responseObject, + response: await serializeResponse(response), expiration: Date.now() + cacheLife } localStorage.setItem(cacheName, JSON.stringify(cache)) @@ -59,7 +52,13 @@ const getLocalStorage = ({ cacheLife }: { cacheLife: number }): Cache => { localStorage.setItem(cacheName, JSON.stringify({})) } - return { get, set, has, delete: remove, clear } + return Object.defineProperties(getCache(), { + get: { value: get, writable: false }, + set: { value: set, writable: false }, + has: { value: has, writable: false }, + delete: { value: remove, writable: false }, + clear: { value: clear, writable: false } + }) } export default getLocalStorage diff --git a/src/storage/memoryStorage.ts b/src/storage/memoryStorage.ts index 1bcf87f8..6ebfeb98 100644 --- a/src/storage/memoryStorage.ts +++ b/src/storage/memoryStorage.ts @@ -1,34 +1,44 @@ import { Cache } from '../types' -const inMemoryStorage = new Map() -const getMemoryStorage = ({ cacheLife }: { cacheLife: number }): Cache => ({ - async get(name: string) { - const item = inMemoryStorage.get(name) as Response | undefined - if (!item) return - - const expiration = inMemoryStorage.get(`${name}:ts`) - if (expiration && expiration> 0 && expiration < Date.now()) { - inMemoryStorage.delete(name) - inMemoryStorage.delete(`${name}:ts`) - return +var inMemoryStorage: any = {} +var getMemoryStorage = ({ cacheLife }: { cacheLife: number }): Cache => { + const isExpired = (responseID: string) => { + const expiration = inMemoryStorage[`${responseID}:ts`] + const expired = expiration> 0 && expiration < Date.now() + if (expired) remove(responseID) + return expired + } + + const get = async (responseID: string) => { + if (isExpired(responseID)) return + return inMemoryStorage[responseID] as Response + } + + const set = async (responseID: string, res: Response) => { + inMemoryStorage[responseID] = res + inMemoryStorage[`${responseID}:ts`] = cacheLife> 0 ? Date.now() + cacheLife : 0 + } + + const has = async (responseID: string) => !isExpired(responseID) + + const remove = async (...responseIDs: string[]) => { + for (const responseID of responseIDs) { + delete inMemoryStorage[responseID] + delete inMemoryStorage[`${responseID}:ts`] } + } - return item - }, - async set(name: string, data: Response) { - inMemoryStorage.set(name, data) - inMemoryStorage.set(`${name}:ts`, cacheLife> 0 ? Date.now() + cacheLife : 0) - }, - async has(name: string) { - return inMemoryStorage.has(name) - }, - async delete(name: string) { - inMemoryStorage.delete(name) - inMemoryStorage.delete(`${name}:ts`) - }, - async clear() { - return inMemoryStorage.clear() + const clear = async () => { + inMemoryStorage = {} } -}) + + return Object.defineProperties(inMemoryStorage, { + get: { value: get, writable: false, configurable: true }, + set: { value: set, writable: false, configurable: true }, + has: { value: has, writable: false, configurable: true }, + delete: { value: remove, writable: false, configurable: true }, + clear: { value: clear, writable: false, configurable: true } + }) +} export default getMemoryStorage diff --git a/src/types.ts b/src/types.ts index edc19f72..446335ca 100644 --- a/src/types.ts +++ b/src/types.ts @@ -159,6 +159,7 @@ export type Interceptors = { response?: (response: Res) => Res } +// this also holds the response keys. It mimics js Map export type Cache = { get: (name: string) => Promise set: (name: string, data: Response) => Promise diff --git a/src/useFetch.ts b/src/useFetch.ts index 91bdc995..1f8239dd 100644 --- a/src/useFetch.ts +++ b/src/useFetch.ts @@ -51,6 +51,7 @@ function useFetch (...args: UseFetchArgs): UseFetch { const hasMore = useRef(true) const suspenseStatus = useRef('pending') const suspender = useRef>() + const mounted = useRef(false) const [loading, setLoading] = useState(defaults.loading) const forceUpdate = useReducer(() => ({}), [])[1] @@ -78,7 +79,7 @@ function useFetch (...args: UseFetchArgs): UseFetch { interceptors.request ) - if (!suspense) setLoading(true) + if (!suspense && mounted.current) setLoading(true) error.current = undefined if (response.isCached && cachePolicy === CACHE_FIRST) { @@ -86,11 +87,11 @@ function useFetch (...args: UseFetchArgs): UseFetch { res.current = response.cached as Res res.current.data = await tryGetData(response.cached, defaults.data) data.current = res.current.data as TData - if (!suspense) setLoading(false) + if (!suspense && mounted.current) setLoading(false) return data.current } catch (err) { error.current = err - setLoading(false) + if (mounted.current) setLoading(false) } } @@ -134,7 +135,7 @@ function useFetch (...args: UseFetchArgs): UseFetch { controller.current = undefined } - if (!suspense) setLoading(false) + if (!suspense && mounted.current) setLoading(false) return data.current } // end of doFetch() @@ -182,12 +183,14 @@ function useFetch (...args: UseFetchArgs): UseFetch { // onMount/onUpdate useEffect((): any => { + mounted.current = true if (Array.isArray(dependencies)) { const methodName = requestInit.method || HTTPMethod.GET const methodLower = methodName.toLowerCase() as keyof ReqMethods const req = request[methodLower] as NoArgs req() } + return () => mounted.current = false // TODO: need [request] in dependency array. Causing infinite loop though. // eslint-disable-next-line react-hooks/exhaustive-deps }, dependencies)

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /