Comparing version 0.3.7 to 0.3.8
@@ -141,2 +141,3 @@ import { ReactNode } from 'react'; | ||
cacheLife?: number; | ||
suspense?: boolean; | ||
} | ||
@@ -143,0 +144,0 @@ export declare type Options = CustomOptions & Omit<RequestInit, 'body'> & { |
@@ -69,3 +69,3 @@ "use strict"; | ||
var initialURL = customOptions.url, path = customOptions.path, interceptors = customOptions.interceptors, timeout = customOptions.timeout, retries = customOptions.retries, onTimeout = customOptions.onTimeout, onAbort = customOptions.onAbort, onNewData = customOptions.onNewData, perPage = customOptions.perPage, cachePolicy = customOptions.cachePolicy, // 'cache-first' by default | ||
cacheLife = customOptions.cacheLife; | ||
cacheLife = customOptions.cacheLife, suspense = customOptions.suspense; | ||
var isServer = use_ssr_1.default().isServer; | ||
@@ -79,3 +79,6 @@ var controller = react_1.useRef(); | ||
var hasMore = react_1.useRef(true); | ||
var suspenseStatus = react_1.useRef('pending'); | ||
var suspender = react_1.useRef(); | ||
var _b = react_1.useState(defaults.loading), loading = _b[0], setLoading = _b[1]; | ||
var forceUpdate = react_1.useReducer(function () { return ({}); }, [])[1]; | ||
var makeFetch = react_1.useCallback(function (method) { | ||
@@ -95,4 +98,6 @@ var doFetch = function (routeOrBody, body) { return __awaiter(_this, void 0, void 0, function () { | ||
_a = _c.sent(), url = _a.url, options = _a.options, response = _a.response; | ||
if (!suspense) | ||
setLoading(true); | ||
error.current = undefined; | ||
if (!(response.isCached && cachePolicy === CACHE_FIRST)) return [3 /*break*/, 5]; | ||
setLoading(true); | ||
if (!response.isExpired) return [3 /*break*/, 2]; | ||
@@ -109,3 +114,4 @@ cache.delete(response.id); | ||
data.current = res.current.data; | ||
setLoading(false); | ||
if (!suspense) | ||
setLoading(false); | ||
return [2 /*return*/, data.current]; | ||
@@ -121,4 +127,2 @@ case 4: | ||
return [2 /*return*/, data.current]; | ||
setLoading(true); | ||
error.current = undefined; | ||
timer = timeout > 0 && setTimeout(function () { | ||
@@ -172,9 +176,37 @@ timedout.current = true; | ||
case 11: | ||
setLoading(false); | ||
if (!suspense) | ||
setLoading(false); | ||
return [2 /*return*/, data.current]; | ||
} | ||
}); | ||
}); }; | ||
}); }; // end of doFetch() | ||
if (suspense) { | ||
return function () { | ||
var args = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
args[_i] = arguments[_i]; | ||
} | ||
return __awaiter(_this, void 0, void 0, function () { | ||
var newData; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
suspender.current = doFetch.apply(void 0, args).then(function (newData) { | ||
suspenseStatus.current = 'success'; | ||
return newData; | ||
}, function () { | ||
suspenseStatus.current = 'error'; | ||
}); | ||
forceUpdate(); | ||
return [4 /*yield*/, suspender.current]; | ||
case 1: | ||
newData = _a.sent(); | ||
return [2 /*return*/, newData]; | ||
} | ||
}); | ||
}); | ||
}; | ||
} | ||
return doFetch; | ||
}, [isServer, onAbort, requestInit, initialURL, path, interceptors, cachePolicy, perPage, timeout, cacheLife, onTimeout, defaults.data, onNewData]); | ||
}, [isServer, onAbort, requestInit, initialURL, path, interceptors, cachePolicy, perPage, timeout, cacheLife, onTimeout, defaults.data, onNewData, forceUpdate, suspense]); | ||
var post = react_1.useCallback(makeFetch(types_1.HTTPMethod.POST), [makeFetch]); | ||
@@ -211,3 +243,4 @@ var del = react_1.useCallback(makeFetch(types_1.HTTPMethod.DELETE), [makeFetch]); | ||
value: function () { | ||
var clonedResponse = ('clone' in res.current ? res.current.clone() : {}); | ||
var _a; | ||
var clonedResponse = ('clone' in res.current ? res.current.clone() : (_a = {}, _a[field] = function () { console.error("You haven't made a http request yet"); }, _a)); | ||
return clonedResponse[field](); | ||
@@ -222,3 +255,3 @@ }, | ||
react_1.useEffect(function () { | ||
if (dependencies && Array.isArray(dependencies)) { | ||
if (Array.isArray(dependencies)) { | ||
var methodName = requestInit.method || types_1.HTTPMethod.GET; | ||
@@ -237,3 +270,14 @@ var methodLower = methodName.toLowerCase(); | ||
react_1.useEffect(function () { return request.abort; }, []); | ||
return Object.assign([request, response, loading, error.current], __assign({ request: request, response: response }, request)); | ||
var final = Object.assign([request, response, loading, error.current], __assign({ request: request, response: response }, request)); | ||
if (suspense && suspender.current) { | ||
if (isServer) | ||
throw new Error('Suspense on server side is not yet supported! 🙅♂️'); | ||
switch (suspenseStatus.current) { | ||
case 'pending': | ||
throw suspender.current; | ||
case 'error': | ||
throw error.current; | ||
} | ||
} | ||
return final; | ||
} | ||
@@ -240,0 +284,0 @@ exports.useFetch = useFetch; |
@@ -15,2 +15,3 @@ import { OptionsMaybeURL, NoUrlOptions, Flatten, CachePolicies, Interceptors, OverwriteGlobalOptions } from './types'; | ||
cacheLife: number; | ||
suspense: boolean; | ||
}; | ||
@@ -37,2 +38,3 @@ requestInit: RequestInit; | ||
cacheLife: number; | ||
suspense: boolean; | ||
}; | ||
@@ -39,0 +41,0 @@ requestInit: { |
@@ -33,3 +33,4 @@ "use strict"; | ||
cachePolicy: types_1.CachePolicies.CACHE_FIRST, | ||
cacheLife: 0 | ||
cacheLife: 0, | ||
suspense: false | ||
}, | ||
@@ -104,2 +105,3 @@ requestInit: { headers: {} }, | ||
var cacheLife = useField('cacheLife', urlOrOptions, optionsNoURLs); | ||
var suspense = useField('suspense', urlOrOptions, optionsNoURLs); | ||
var loading = react_1.useMemo(function () { | ||
@@ -151,3 +153,4 @@ if (utils_1.isObject(urlOrOptions)) | ||
cachePolicy: cachePolicy, | ||
cacheLife: cacheLife | ||
cacheLife: cacheLife, | ||
suspense: suspense | ||
}, | ||
@@ -154,0 +157,0 @@ requestInit: requestInit, |
{ | ||
"name": "use-http", | ||
"version": "0.3.7", | ||
"version": "0.3.8", | ||
"homepage": "http://use-http.com", | ||
@@ -19,3 +19,3 @@ "main": "dist/index.js", | ||
"devDependencies": { | ||
"@testing-library/react": "^9.2.0", | ||
"@testing-library/react": "^10.0.0", | ||
"@testing-library/react-hooks": "^3.0.0", | ||
@@ -22,0 +22,0 @@ "@types/fetch-mock": "^7.2.3", |
177
README.md
@@ -81,2 +81,3 @@ <a href="http://use-http.com"> | ||
- Built in caching | ||
- Suspense<sup>(experimental)</sup> support | ||
@@ -142,3 +143,3 @@ Usage | ||
<details><summary><b>Basic Usage (no managed state) <code>useFetch</code></b></summary> | ||
<details><summary><b>Basic Usage (auto managed state) <code>useFetch</code></b></summary> | ||
@@ -171,4 +172,5 @@ This fetch is run `onMount/componentDidMount`. The last argument `[]` means it will run `onMount`. If you pass it a variable like `[someVariable]`, it will run `onMount` and again whenever `someVariable` changes values (aka `onUpdate`). **If no method is specified, GET is the default** | ||
<details open><summary><b>Basic Usage (no managed state) with <code>Provider</code></b></summary> | ||
<details open><summary><b>Basic Usage (auto managed state) with <code>Provider</code></b></summary> | ||
```js | ||
@@ -205,2 +207,80 @@ import useFetch, { Provider } from 'use-http' | ||
<details open><summary><b>Suspense Mode (auto managed state)</b></summary> | ||
```js | ||
import useFetch, { Provider } from 'use-http' | ||
function Todos() { | ||
const { data: todos } = useFetch({ | ||
path: '/todos', | ||
data: [], | ||
suspense: true // can put it in 2 places. Here or in Provider | ||
}, []) // onMount | ||
return todos.map(todo => <div key={todo.id}>{todo.title}</div>) | ||
} | ||
function App() { | ||
const options = { | ||
suspense: true | ||
} | ||
return ( | ||
<Provider url='https://example.com' options={options}> | ||
<Suspense fallback='Loading...'> | ||
<Todos /> | ||
</Suspense> | ||
</Provider> | ||
) | ||
} | ||
``` | ||
[![Edit Basic Example](https://codesandbox.io/static/img/play-codesandbox.svg)]() | ||
</details> | ||
<details open><summary><b>Suspense Mode (managed state)</b></summary> | ||
Can put `suspense` in 2 places. Either `useFetch` (A) or `Provider` (B). | ||
```js | ||
import useFetch, { Provider } from 'use-http' | ||
function Todos() { | ||
const [todos, setTodos] = useState([]) | ||
// A. can put `suspense: true` here | ||
const { get, response } = useFetch({ data: [], suspense: true }) | ||
const loadInitialTodos = async () => { | ||
const todos = await get('/todos') | ||
if (response.ok) setTodos(todos) | ||
} | ||
const mounted = useRef(false) | ||
useEffect(() => { | ||
if (mounted.current) return | ||
mounted.current = true | ||
loadInitialTodos() | ||
}, []) | ||
return todos.map(todo => <div key={todo.id}>{todo.title}</div>) | ||
} | ||
function App() { | ||
const options = { | ||
suspense: true // B. can put `suspense: true` here too | ||
} | ||
return ( | ||
<Provider url='https://example.com' options={options}> | ||
<Suspense fallback='Loading...'> | ||
<Todos /> | ||
</Suspense> | ||
</Provider> | ||
) | ||
} | ||
``` | ||
[![Edit Basic Example](https://codesandbox.io/static/img/play-codesandbox.svg)]() | ||
</details> | ||
<div align="center"> | ||
@@ -273,5 +353,5 @@ <br> | ||
<details open><summary><b>Destructured <code>useFetch</code></b></summary> | ||
⚠️ The `response` object cannot be destructured! (at least not currently) ️️⚠️ | ||
⚠️ Do not destructure the `response` object! Technically you can do it, but if you need to access the `response.ok` from, for example, within a component's onClick handler, it will be a stale value for `ok` where it will be correct for `response.ok`. ️️⚠️ | ||
```js | ||
@@ -283,5 +363,3 @@ var [request, response, loading, error] = useFetch('https://example.com') | ||
request, | ||
// the `response` is everything you would expect to be in a normal response from an http request with the `data` field added. | ||
// ⚠️ The `response` object cannot be destructured! (at least not currently) ️️⚠️ | ||
response, | ||
response, // 🚨 Do not destructure the `response` object! | ||
loading, | ||
@@ -294,10 +372,32 @@ error, | ||
patch, | ||
delete // don't destructure `delete` though, it's a keyword | ||
del, // <- that's why we have this (del). or use `request.delete` | ||
mutate, // GraphQL | ||
query, // GraphQL | ||
delete // don't destructure `delete` though, it's a keyword | ||
del, // <- that's why we have this (del). or use `request.delete` | ||
mutate, // GraphQL | ||
query, // GraphQL | ||
abort | ||
} = useFetch('https://example.com') | ||
// 🚨 Do not destructure the `response` object! | ||
// 🚨 This just shows what fields are available in it. | ||
var { | ||
ok, | ||
status, | ||
headers, | ||
data, | ||
type, | ||
statusText, | ||
url, | ||
body, | ||
bodyUsed, | ||
redirected, | ||
// methods | ||
json, | ||
text, | ||
formData, | ||
blob, | ||
arrayBuffer, | ||
clone | ||
} = response | ||
var { | ||
loading, | ||
@@ -685,2 +785,3 @@ error, | ||
| --------------------- | --------------------------------------------------------------------------|------------- | | ||
| `suspense` | Enables React Suspense mode. [example]() | false | | ||
| `cachePolicy` | These will be the same ones as Apollo's [fetch policies](https://www.apollographql.com/docs/react/api/react-apollo/#optionsfetchpolicy). Possible values are `cache-and-network`, `network-only`, `cache-only`, `no-cache`, `cache-first`. Currently only supports **`cache-first`** or **`no-cache`** | `cache-first` | | ||
@@ -704,2 +805,5 @@ | `cacheLife` | After a successful cache update, that cache data will become stale after this duration | `0` | | ||
// enables React Suspense mode | ||
suspense: true, // defaults to `false` | ||
// Cache responses to improve speed and reduce amount of requests | ||
@@ -783,2 +887,8 @@ // Only one request to the same endpoint will be initiated unless cacheLife expires for 'cache-first'. | ||
- [ ] suspense | ||
- [ ] triggering it from outside the `<Suspense />` component. | ||
- add `.read()` to `request` | ||
- or make it work with just the `suspense: true` option | ||
- both of these options need to be thought out a lot more^ | ||
- [ ] tests for this^ (triggering outside) | ||
- [ ] maybe add translations [like this one](https://github.com/jamiebuilds/unstated-next) | ||
@@ -788,2 +898,4 @@ - [ ] 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) | ||
- [ ] add sponsors [similar to this](https://github.com/carbon-app/carbon) | ||
- [ ] Error handling | ||
- [ ] if calling `response.json()` and there is no response yet | ||
- [ ] tests | ||
@@ -799,5 +911,3 @@ - [ ] doFetchArgs tests for `response.isExpired` | ||
- [ ] make this a github package | ||
- [ ] Make work with React Suspense [current example WIP](https://codesandbox.io/s/7ww5950no0) | ||
- [ ] get it all working on a SSR codesandbox, this way we can have api to call locally | ||
- [ ] make GraphQL work with React Suspense | ||
- [ ] make GraphQL examples in codesandbox | ||
@@ -838,4 +948,2 @@ - [ ] Documentation: | ||
const request = useFetch({ | ||
// enabled React Suspense mode | ||
suspense: false, | ||
// allows caching to persist after page refresh | ||
@@ -850,6 +958,12 @@ persist: true, // false by default | ||
cache: new Map(), | ||
interceptors: { | ||
// I think it's more scalable/clean to have this as an object. | ||
// What if we only need the `route` and `options`? | ||
request: async ({ options, url, path, route }) => {}, | ||
response: ({ response }) => {} | ||
}, | ||
// can retry on certain http status codes | ||
retryOn: [503], | ||
// OR | ||
retryOn(attempt, error, response) { | ||
retryOn({ attempt, error, response }) { | ||
// retry on any network error, or 4xx or 5xx status codes | ||
@@ -862,3 +976,3 @@ if (error !== null || response.status >= 400) { | ||
// This function receives a retryAttempt integer and returns the delay to apply before the next attempt in milliseconds | ||
retryDelay(attempt, error, response) { | ||
retryDelay({ attempt, error, response }) { | ||
// applies exponential backoff | ||
@@ -907,29 +1021,2 @@ return Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000) | ||
<details><summary><b>The Goal With Suspense <sup><strong>(not implemented yet)</strong></sup></b></summary> | ||
```jsx | ||
import React, { Suspense, unstable_ConcurrentMode as ConcurrentMode, useEffect } from 'react' | ||
function WithSuspense() { | ||
const suspense = useFetch('https://example.com') | ||
useEffect(() => { | ||
suspense.read() | ||
}, []) | ||
if (!suspense.data) return null | ||
return <pre>{suspense.data}</pre> | ||
} | ||
function App() ( | ||
<ConcurrentMode> | ||
<Suspense fallback="Loading..."> | ||
<WithSuspense /> | ||
</Suspense> | ||
</ConcurrentMode> | ||
) | ||
``` | ||
</details> | ||
<details><summary><b>GraphQL with Suspense <sup><strong>(not implemented yet)</strong></sup></b></summary> | ||
@@ -936,0 +1023,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
129907
36
1359
1036