Skip to content

Commit

Permalink
Merge branch 'master' into next
Browse files Browse the repository at this point in the history
  • Loading branch information
ghengeveld committed Oct 7, 2019
2 parents 1223710 + 8aa1417 commit 3f51efd
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 70 deletions.
5 changes: 2 additions & 3 deletions README.md
Expand Up @@ -88,11 +88,10 @@ Use it with `fetch`, Axios or other data fetching libraries, even GraphQL.


[abortable fetch]: https://developers.google.com/web/updates/2017/09/abortable-fetch [abortable fetch]: https://developers.google.com/web/updates/2017/09/abortable-fetch


> ## Upgrading to v8 > ## Upgrading to v9
> >
> Version 8 comes with breaking changes. > Version 9 comes with a minor breaking change.
> See [Upgrading](https://docs.react-async.com/installation#upgrading) for details. > See [Upgrading](https://docs.react-async.com/installation#upgrading) for details.
> A [codemod](https://github.com/async-library/react-async/tree/master/codemods) is available.
# Documentation # Documentation


Expand Down
18 changes: 12 additions & 6 deletions docs/state.md
Expand Up @@ -108,9 +108,12 @@ The number of times a promise was started.


> `Promise` > `Promise`
A reference to the internal wrapper promise created when starting a new promise \(either automatically or by invoking `run` / `reload`\). It fulfills or rejects along with the provided `promise` / `promiseFn` / `deferFn`. Useful as a chainable alternative to the `onResolve` / `onReject` callbacks. A reference to the internal wrapper promise created when starting a new promise \(either automatically or by invoking
`run` / `reload`\). It fulfills or rejects along with the provided `promise` / `promiseFn` / `deferFn`. Useful as a
chainable alternative to the `onResolve` / `onReject` callbacks.


Warning! If you chain on `promise`, you MUST provide a rejection handler \(e.g. `.catch(...)`\). Otherwise React will throw an exception and crash if the promise rejects. Warning! If you chain on `promise`, you MUST provide a rejection handler \(e.g. `.catch(...)`\). Otherwise React will
throw an exception and crash if the promise rejects.


## `run` ## `run`


Expand All @@ -120,15 +123,18 @@ Runs the `deferFn`, passing any arguments provided as an array.


When used with `useFetch`, `run` has several overloaded signatures: When used with `useFetch`, `run` has several overloaded signatures:


> `function(resource: String | Resource, init: Object | (init: Object) => Object): void` > `function(override: OverrideParams | (params: OverrideParams) => OverrideParams): void`
>
> `function(init: Object | (init: Object) => Object): void`
> >
> `function(event: SyntheticEvent | Event): void` > `function(event: SyntheticEvent | Event): void`
> >
> `function(): void` > `function(): void`
This way you can run the `fetch` request using the provided `resource` and `init`. `resource` can be omitted. If `init` is 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. Where `type OverrideParams = { resource?: RequestInfo } & Partial<RequestInit>`.

This way you can run the `fetch` request with custom `resource` and `init`. If `override` is an object it will be spread
over the default `resource` and `init` for `fetch`. If it's a function it will be invoked with the params defined with
`useFetch`, and should return an `override` object. This way you can either extend or override the value of `resource`
and `init`, for example to change the URL or set custom request headers.


## `reload` ## `reload`


Expand Down
12 changes: 8 additions & 4 deletions docs/upgrading.md
@@ -1,5 +1,11 @@
# Upgrading # Upgrading


## Upgrade to v9

The rejection value for failed requests with `useFetch` was changed. Previously it was the Response object. Now it's an
Error object with `response` property. If you are using `useFetch` and are using the `error` value, expecting it to be
of type Response, you must now use `error.response` instead.

## Upgrade to v8 ## Upgrade to v8


All standalone helper components were renamed to avoid import naming collision. All standalone helper components were renamed to avoid import naming collision.
Expand All @@ -12,7 +18,8 @@ All standalone helper components were renamed to avoid import naming collision.


> A [codemod](https://github.com/async-library/react-async/tree/master/codemods) is available to automate the upgrade. > A [codemod](https://github.com/async-library/react-async/tree/master/codemods) is available to automate the upgrade.
The return type for `run` was changed from `Promise` to `undefined`. You should now use the `promise` prop instead. This is a manual upgrade. See [`promise`](installation.md#promise-1) for details. The return type for `run` was changed from `Promise` to `undefined`. You should now use the `promise` prop instead. This
is a manual upgrade. See [`promise`](state.md#promise) for details.


## Upgrade to v6 ## Upgrade to v6


Expand All @@ -25,11 +32,8 @@ The return type for `run` was changed from `Promise` to `undefined`. You should
## Upgrade to v4 ## Upgrade to v4


- `deferFn` now receives an `args` array as the first argument, instead of arguments to `run` being spread at the front - `deferFn` now receives an `args` array as the first argument, instead of arguments to `run` being spread at the front

of the arguments list. This enables better interop with TypeScript. You can use destructuring to keep using your of the arguments list. This enables better interop with TypeScript. You can use destructuring to keep using your

existing variables. existing variables.


- The shorthand version of `useAsync` now takes the `options` object as optional second argument. This used to be - The shorthand version of `useAsync` now takes the `options` object as optional second argument. This used to be

`initialValue`, but was undocumented and inflexible. `initialValue`, but was undocumented and inflexible.
8 changes: 4 additions & 4 deletions packages/react-async/src/index.d.ts
Expand Up @@ -227,11 +227,11 @@ type AsyncInitialWithout<K extends keyof AsyncInitial<T>, T> =
| Omit<AsyncFulfilled<T>, K> | Omit<AsyncFulfilled<T>, K>
| Omit<AsyncRejected<T>, K> | Omit<AsyncRejected<T>, K>


type OverrideParams = { resource?: RequestInfo } & Partial<RequestInit>

type FetchRun<T> = { type FetchRun<T> = {
run(overrideResource: RequestInfo, overrideInit: (init: RequestInit) => RequestInit): void run(overrideParams: (params?: OverrideParams) => OverrideParams): void
run(overrideResource: RequestInfo, overrideInit: Partial<RequestInit>): void run(overrideParams: OverrideParams): void
run(overrideInit: (init: RequestInit) => RequestInit): void
run(overrideInit: Partial<RequestInit>): void
run(ignoredEvent: React.SyntheticEvent): void run(ignoredEvent: React.SyntheticEvent): void
run(ignoredEvent: Event): void run(ignoredEvent: Event): void
run(): void run(): void
Expand Down
23 changes: 22 additions & 1 deletion packages/react-async/src/reducer.js
@@ -1,6 +1,27 @@
import { getInitialStatus, getIdleStatus, getStatusProps, statusTypes } from "./status" import { getInitialStatus, getIdleStatus, getStatusProps, statusTypes } from "./status"


export const neverSettle = new Promise(() => {}) // This exists to make sure we don't hold any references to user-provided functions
class NeverSettle extends Promise {
constructor() {
super(() => {}, () => {})
/* istanbul ignore next */
if (Object.setPrototypeOf) {
// Not available in IE 10, but can be polyfilled
Object.setPrototypeOf(this, NeverSettle.prototype)
}
}
finally() {
return this
}
catch() {
return this
}
then() {
return this
}
}

export const neverSettle = new NeverSettle()


export const actionTypes = { export const actionTypes = {
start: "start", start: "start",
Expand Down
26 changes: 16 additions & 10 deletions packages/react-async/src/useAsync.js
Expand Up @@ -168,6 +168,11 @@ const useAsync = (arg1, arg2) => {
export class FetchError extends Error { export class FetchError extends Error {
constructor(response) { constructor(response) {
super(`${response.status} ${response.statusText}`) super(`${response.status} ${response.statusText}`)
/* istanbul ignore next */
if (Object.setPrototypeOf) {
// Not available in IE 10, but can be polyfilled
Object.setPrototypeOf(this, FetchError.prototype)
}
this.response = response this.response = response
} }
} }
Expand All @@ -178,8 +183,6 @@ const parseResponse = (accept, json) => res => {
return accept === "application/json" ? res.json() : res return accept === "application/json" ? res.json() : res
} }


const isResource = value => typeof value === "string" || (typeof value === "object" && value.url)

const useAsyncFetch = (resource, init, { defer, json, ...options } = {}) => { const useAsyncFetch = (resource, init, { defer, json, ...options } = {}) => {
const method = resource.method || (init && init.method) const method = resource.method || (init && init.method)
const headers = resource.headers || (init && init.headers) || {} const headers = resource.headers || (init && init.headers) || {}
Expand All @@ -194,15 +197,17 @@ const useAsyncFetch = (resource, init, { defer, json, ...options } = {}) => {
...options, ...options,
[fn]: useCallback( [fn]: useCallback(
(arg1, arg2, arg3) => { (arg1, arg2, arg3) => {
const [runArgs, signal] = isDefer ? [arg1, arg3.signal] : [[], arg2.signal] const [override, signal] = isDefer ? [arg1[0], arg3.signal] : [undefined, arg2.signal]
const [runResource, runInit] = isResource(runArgs[0]) ? runArgs : [, runArgs[0]] const isEvent = typeof override === "object" && "preventDefault" in override
if (typeof runInit === "object" && "preventDefault" in runInit) { if (!override || isEvent) {
// Don't spread Events or SyntheticEvents return doFetch(resource, { signal, ...init })
return doFetch(runResource || resource, { signal, ...init }) }
if (typeof override === "function") {
const { resource: runResource, ...runInit } = override({ resource, signal, ...init })
return doFetch(runResource || resource, { signal, ...runInit })
} }
return typeof runInit === "function" const { resource: runResource, ...runInit } = override
? doFetch(runResource || resource, { signal, ...runInit(init) }) return doFetch(runResource || resource, { signal, ...init, ...runInit })
: doFetch(runResource || resource, { signal, ...init, ...runInit })
}, },
[identity] // eslint-disable-line react-hooks/exhaustive-deps [identity] // eslint-disable-line react-hooks/exhaustive-deps
), ),
Expand All @@ -211,6 +216,7 @@ const useAsyncFetch = (resource, init, { defer, json, ...options } = {}) => {
return state return state
} }


/* istanbul ignore next */
const unsupported = () => { const unsupported = () => {
throw new Error( throw new Error(
"useAsync requires React v16.8 or up. Upgrade your React version or use the <Async> component instead." "useAsync requires React v16.8 or up. Upgrade your React version or use the <Async> component instead."
Expand Down
50 changes: 8 additions & 42 deletions packages/react-async/src/useAsync.spec.js
Expand Up @@ -203,61 +203,27 @@ describe("useFetch", () => {
expect(json).toHaveBeenCalled() expect(json).toHaveBeenCalled()
}) })


test("calling `run` with a callback as argument allows to override `init` parameters", () => { test("calling `run` with a callback as argument allows to override fetch parameters", () => {
const override = params => ({ ...params, resource: "/bar", body: '{"name":"bar"}' })
const component = ( const component = (
<Fetch input="/test" init={{ method: "POST", body: '{"name":"foo"}' }}> <Fetch input="/foo" init={{ method: "POST", body: '{"name":"foo"}' }}>
{({ run }) => ( {({ run }) => <button onClick={() => run(override)}>run</button>}
<button onClick={() => run(init => ({ ...init, body: '{"name":"bar"}' }))}>run</button>
)}
</Fetch>
)
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":"bar"}' })
)
})

test("calling `run` with an object as argument allows to override `init` parameters", () => {
const component = (
<Fetch input="/test" init={{ method: "POST", body: '{"name":"foo"}' }}>
{({ run }) => <button onClick={() => run({ body: '{"name":"bar"}' })}>run</button>}
</Fetch>
)
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":"bar"}' })
)
})

test("calling `run` with a url allows to override fetch's `resource` parameter", () => {
const component = (
<Fetch input="/foo" options={{ defer: true }}>
{({ run }) => <button onClick={() => run("/bar")}>run</button>}
</Fetch> </Fetch>
) )
const { getByText } = render(component) const { getByText } = render(component)
expect(globalScope.fetch).not.toHaveBeenCalled() expect(globalScope.fetch).not.toHaveBeenCalled()
fireEvent.click(getByText("run")) fireEvent.click(getByText("run"))
expect(globalScope.fetch).toHaveBeenCalledWith( expect(globalScope.fetch).toHaveBeenCalledWith(
"/bar", "/bar",
expect.objectContaining({ signal: abortCtrl.signal }) expect.objectContaining({ method: "POST", signal: abortCtrl.signal, body: '{"name":"bar"}' })
) )
}) })


test("overriding the `resource` can be combined with overriding `init`", () => { test("calling `run` with an object as argument allows to override fetch parameters", () => {
const override = { resource: "/bar", body: '{"name":"bar"}' }
const component = ( const component = (
<Fetch input="/foo" init={{ method: "POST", body: '{"name":"foo"}' }}> <Fetch input="/foo" init={{ method: "POST", body: '{"name":"foo"}' }}>
{({ run }) => ( {({ run }) => <button onClick={() => run(override)}>run</button>}
<button onClick={() => run("/bar", init => ({ ...init, body: '{"name":"bar"}' }))}>
run
</button>
)}
</Fetch> </Fetch>
) )
const { getByText } = render(component) const { getByText } = render(component)
Expand Down
1 change: 1 addition & 0 deletions stories/index.stories.js
@@ -1,3 +1,4 @@
/* eslint-disable react/prop-types */
import React, { Suspense } from "react" import React, { Suspense } from "react"
import { storiesOf } from "@storybook/react" import { storiesOf } from "@storybook/react"


Expand Down

0 comments on commit 3f51efd

Please sign in to comment.