diff --git a/README.md b/README.md index 7cb8b05b..6f9d5d60 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,25 @@ const MyComponent = () => { const headers = { Accept: "application/json" } const { data, error, isLoading, run } = useFetch("/api/example", { headers }, options) // This will setup a promiseFn with a fetch request and JSON deserialization. + + // you can later call `run` with an optional callback argument to + // last-minute modify the `init` parameter that is passed to `fetch` + function clickHandler() { + run(init => ({ + ...init, + headers: { + ...init.headers, + authentication: "...", + }, + })) + } + + // alternatively, you can also just use an object that will be spread over `init`. + // please note that this is not deep-merged, so you might override properties present in the + // original `init` parameter + function clickHandler2() { + run({ body: JSON.stringify(formValues) }) + } } ``` @@ -655,6 +674,14 @@ chainable alternative to the `onResolve` / `onReject` callbacks. Runs the `deferFn`, passing any arguments provided as an array. +When used with `useFetch`, `run` has a different signature: + +> `function(init: Object | (init: Object) => Object): void` + +This runs the `fetch` request using the provided `init`. If it's an object it will be spread over the default `init` +(`useFetch`'s 2nd argument). If it's a function it will be invoked with the default `init` and should return a new +`init` object. This way you can either extend or override the value of `init`, for example to set request headers. + #### `reload` > `function(): void` diff --git a/examples/with-typescript/src/App.tsx b/examples/with-typescript/src/App.tsx index 4205a66d..4789fb9c 100644 --- a/examples/with-typescript/src/App.tsx +++ b/examples/with-typescript/src/App.tsx @@ -9,6 +9,7 @@ import Async, { } from "react-async" import DevTools from "react-async-devtools" import "./App.css" +import { FetchHookExample } from "./FetchHookExample" const loadFirstName: PromiseFn = ({ userId }) => fetch(`https://reqres.in/api/users/${userId}`) @@ -50,6 +51,7 @@ class App extends Component { {data => <>{data}} + ) diff --git a/examples/with-typescript/src/FetchHookExample.tsx b/examples/with-typescript/src/FetchHookExample.tsx new file mode 100644 index 00000000..a0829994 --- /dev/null +++ b/examples/with-typescript/src/FetchHookExample.tsx @@ -0,0 +1,52 @@ +import * as React from "react" +import { useFetch } from "react-async" + +export function FetchHookExample() { + const result = useFetch<{ token: string }>("https://reqres.in/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }) + const { run } = result + + return ( + <> +

with fetch hook:

+ + + +
+ Status: +
+ {result.isInitial && "initial"} + {result.isLoading && "loading"} + {result.isRejected && "rejected"} + {result.isResolved && `token: ${result.data.token}`} + + ) +} diff --git a/examples/with-typescript/src/index.css b/examples/with-typescript/src/index.css index cee5f348..a2b91dc6 100644 --- a/examples/with-typescript/src/index.css +++ b/examples/with-typescript/src/index.css @@ -1,14 +1,12 @@ body { margin: 0; padding: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } diff --git a/packages/react-async/src/index.d.ts b/packages/react-async/src/index.d.ts index c0eff598..70f16106 100644 --- a/packages/react-async/src/index.d.ts +++ b/packages/react-async/src/index.d.ts @@ -226,6 +226,21 @@ export function useFetch( input: RequestInfo, init?: RequestInit, options?: FetchOptions -): AsyncState +): AsyncInitialWithout<"run", T> & FetchRun + +// unfortunately, we cannot just omit K from AsyncInitial as that would unbox the Discriminated Union +type AsyncInitialWithout, T> = + | Omit, K> + | Omit, K> + | Omit, K> + | Omit, K> + +type FetchRun = { + run(overrideInit: (init: RequestInit) => RequestInit): void + run(overrideInit: Partial): void + run(ignoredEvent: React.SyntheticEvent): void + run(ignoredEvent: Event): void + run(): void +} export default Async diff --git a/packages/react-async/src/useAsync.js b/packages/react-async/src/useAsync.js index e2510b0f..b2c14509 100644 --- a/packages/react-async/src/useAsync.js +++ b/packages/react-async/src/useAsync.js @@ -156,9 +156,8 @@ const useAsync = (arg1, arg2) => { const parseResponse = (accept, json) => res => { if (!res.ok) return Promise.reject(res) - if (json === false) return res - if (json === true || accept === "application/json") return res.json() - return res + if (typeof json === "boolean") return json ? res.json() : res + return accept === "application/json" ? res.json() : res } const useAsyncFetch = (input, init, { defer, json, ...options } = {}) => { @@ -166,13 +165,23 @@ const useAsyncFetch = (input, init, { defer, json, ...options } = {}) => { const headers = input.headers || (init && init.headers) || {} const accept = headers["Accept"] || headers["accept"] || (headers.get && headers.get("accept")) const doFetch = (input, init) => globalScope.fetch(input, init).then(parseResponse(accept, json)) - const isDefer = defer === true || ~["POST", "PUT", "PATCH", "DELETE"].indexOf(method) - const fn = defer === false || !isDefer ? "promiseFn" : "deferFn" - const identity = JSON.stringify({ input, init }) + const isDefer = + typeof defer === "boolean" ? defer : ["POST", "PUT", "PATCH", "DELETE"].indexOf(method) !== -1 + const fn = isDefer ? "deferFn" : "promiseFn" + const identity = JSON.stringify({ input, init, isDefer }) const state = useAsync({ ...options, [fn]: useCallback( - (_, props, ctrl) => doFetch(input, { signal: ctrl ? ctrl.signal : props.signal, ...init }), + (arg1, arg2, arg3) => { + const [override, signal] = arg3 ? [arg1[0], arg3.signal] : [undefined, arg2.signal] + if (typeof override === "object" && "preventDefault" in override) { + // Don't spread Events or SyntheticEvents + return doFetch(input, { signal, ...init }) + } + return typeof override === "function" + ? doFetch(input, { signal, ...override(init) }) + : doFetch(input, { signal, ...init, ...override }) + }, [identity] // eslint-disable-line react-hooks/exhaustive-deps ), }) diff --git a/packages/react-async/src/useAsync.spec.js b/packages/react-async/src/useAsync.spec.js index ccf3a104..b6501578 100644 --- a/packages/react-async/src/useAsync.spec.js +++ b/packages/react-async/src/useAsync.spec.js @@ -202,4 +202,52 @@ describe("useFetch", () => { await Promise.resolve() expect(json).toHaveBeenCalled() }) + + test("calling `run` with a method argument allows to override `init` parameters", () => { + const component = ( + + {({ run }) => ( + + )} + + ) + const { getByText } = render(component) + expect(globalScope.fetch).not.toHaveBeenCalled() + fireEvent.click(getByText("run")) + expect(globalScope.fetch).toHaveBeenCalledWith( + "/test", + expect.objectContaining({ method: "POST", signal: abortCtrl.signal, body: '{"name":"test"}' }) + ) + }) + + test("calling `run` with an object as argument allows to override `init` parameters", () => { + const component = ( + + {({ run }) => } + + ) + const { getByText } = render(component) + expect(globalScope.fetch).not.toHaveBeenCalled() + fireEvent.click(getByText("run")) + expect(globalScope.fetch).toHaveBeenCalledWith( + "/test", + expect.objectContaining({ method: "POST", signal: abortCtrl.signal, body: '{"name":"test"}' }) + ) + }) + + test("passing `run` directly as a click handler will not spread the event over init", () => { + const component = ( + + {({ run }) => } + + ) + const { getByText } = render(component) + expect(globalScope.fetch).not.toHaveBeenCalled() + fireEvent.click(getByText("run")) + expect(globalScope.fetch).toHaveBeenCalledWith("/test", expect.any(Object)) + expect(globalScope.fetch).not.toHaveBeenCalledWith( + "/test", + expect.objectContaining({ preventDefault: expect.any(Function) }) + ) + }) }) diff --git a/yarn.lock b/yarn.lock index 50c93d49..e9364881 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3670,13 +3670,6 @@ ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228" - integrity sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q== - dependencies: - type-fest "^0.5.2" - ansi-gray@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" @@ -5311,13 +5304,6 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - cli-spinners@^1.1.0: version "1.3.1" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" @@ -6914,11 +6900,6 @@ emoji-regex@^7.0.1, emoji-regex@^7.0.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -7897,13 +7878,6 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -figures@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.0.0.tgz#756275c964646163cc6f9197c7a0295dbfd04de9" - integrity sha512-HKri+WoWoUgr83pehn/SIgLOMZ9nAWC6dcGj26RY2R4F50u4+RTUz0RCrUlOV3nKRAICW1UGzyb+kcX2qK1S/g== - dependencies: - escape-string-regexp "^1.0.5" - file-entry-cache@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" @@ -9344,21 +9318,21 @@ inquirer@^3.0.6, inquirer@^3.3.0: through "^2.3.6" inquirer@^6.2.0, inquirer@^6.2.1, inquirer@^6.4.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.1.tgz#8bfb7a5ac02dac6ff641ac4c5ff17da112fcdb42" - integrity sha512-uxNHBeQhRXIoHWTSNYUFhQVrHYFThIt6IVo2fFmSe8aBwdR3/w6b58hJpiL/fMukFkvGzjg+hSxFtwvVmKZmXw== + version "6.5.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" + integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== dependencies: - ansi-escapes "^4.2.1" + ansi-escapes "^3.2.0" chalk "^2.4.2" - cli-cursor "^3.1.0" + cli-cursor "^2.1.0" cli-width "^2.0.0" external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.15" - mute-stream "0.0.8" + figures "^2.0.0" + lodash "^4.17.12" + mute-stream "0.0.7" run-async "^2.2.0" rxjs "^6.4.0" - string-width "^4.1.0" + string-width "^2.1.0" strip-ansi "^5.1.0" through "^2.3.6" @@ -9622,11 +9596,6 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - is-function@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" @@ -11330,9 +11299,9 @@ mem@^4.0.0, mem@^4.3.0: p-is-promise "^2.0.0" memoize-one@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.0.tgz#ce7af291c0e2fe041b709cac5e8c7b198c994286" - integrity sha512-p3tPVJNrjOkJ0vk0FRn6yv898qlQZct1rsQAXuwK9X5brNVajPv/y13ytrUByzSS8olyzeqCLX8BKEWmTmXa1A== + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== memoizerific@^1.11.3: version "1.11.3" @@ -11891,9 +11860,9 @@ minimist@~0.0.1: integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= minipass@^2.2.1, minipass@^2.3.5: - version "2.3.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" - integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== + version "2.4.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.4.0.tgz#38f0af94f42fb6f34d3d7d82a90e2c99cd3ff485" + integrity sha512-6PmOuSP4NnZXzs2z6rbwzLJu/c5gdzYg1mRI/WIYdx45iiX7T+a4esOzavD6V/KmBzAaopFSTZPZcUx73bqKWA== dependencies: safe-buffer "^5.1.2" yallist "^3.0.0" @@ -12022,7 +11991,7 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -mute-stream@0.0.8, mute-stream@~0.0.4: +mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== @@ -15689,14 +15658,6 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -15773,9 +15734,9 @@ rollup-pluginutils@^2.8.1: estree-walker "^0.6.1" rollup@^1.1.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.20.0.tgz#3799e4cd2f48c0a068d87af575956d8d72c9447a" - integrity sha512-zW80j9RSJ0VV0VOxP1i7cF279+IlAaD49Ihwqb87PDR0555Fvk10HKmh2yUtXCdBb37bELuhHWZTJc4uoCo8Vw== + version "1.20.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.20.1.tgz#fc66f356c5afbd7c62434f1e7a53a1e7da5a2b32" + integrity sha512-8DV8eWLq84fbJFRqkjWg8BWX4NTTdHpx9bxjmTl/83z54o6Ygo1OgUDjJGFq/xe5i0kDspnbjzw2V+ZPXD/BrQ== dependencies: "@types/estree" "0.0.39" "@types/node" "^12.7.2" @@ -16661,15 +16622,6 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" - integrity sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^5.2.0" - string.prototype.matchall@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-3.0.1.tgz#5a9e0b64bcbeb336aa4814820237c2006985646d" @@ -17376,7 +17328,7 @@ type-fest@^0.3.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== -type-fest@^0.5.0, type-fest@^0.5.2: +type-fest@^0.5.0: version "0.5.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==