diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..2a0dc9a8 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +14.16.0 diff --git a/docs/api/state.md b/docs/api/state.md index 35484184..4e1f43d8 100644 --- a/docs/api/state.md +++ b/docs/api/state.md @@ -9,6 +9,7 @@ These are returned in an object by `useAsync()` or provided by `` as rend - [`startedAt`](#startedat) When the current/last promise was started. - [`finishedAt`](#finishedat) When the last promise was fulfilled or rejected. - [`status`](#status) One of: `initial`, `pending`, `fulfilled`, `rejected`. +- [`statusCode`](#statusCode) The HTTP response status code (`useAsync` only). - [`isInitial`](#isinitial) true when no promise has ever started, or one started but was cancelled. - [`isPending`](#ispending) true when a promise is currently awaiting settlement. Alias: `isLoading` - [`isFulfilled`](#isfulfilled) true when the last promise was fulfilled. Alias: `isResolved` @@ -64,6 +65,12 @@ Tracks when the last promise was resolved or rejected. One of: `initial`, `pending`, `fulfilled`, `rejected`. These are available for import as `statusTypes`. +## `statusCode` + +> `number` + +The HTTP response status code (`useAsync` only). + ## `isInitial` > `boolean` diff --git a/packages/react-async/src/propTypes.ts b/packages/react-async/src/propTypes.ts index b6d837fe..67f64022 100644 --- a/packages/react-async/src/propTypes.ts +++ b/packages/react-async/src/propTypes.ts @@ -14,6 +14,7 @@ const stateObject = startedAt: PropTypes.instanceOf(Date), finishedAt: PropTypes.instanceOf(Date), status: PropTypes.oneOf(["initial", "pending", "fulfilled", "rejected"]), + statusCode: PropTypes.number, isInitial: PropTypes.bool, isPending: PropTypes.bool, isLoading: PropTypes.bool, diff --git a/packages/react-async/src/types.ts b/packages/react-async/src/types.ts index 20cd5d84..872dbc60 100644 --- a/packages/react-async/src/types.ts +++ b/packages/react-async/src/types.ts @@ -108,6 +108,7 @@ export type AsyncFulfilled> = S & { startedAt: Date finishedAt: Date status: "fulfilled" + statusCode: number isInitial: false isPending: false isLoading: false @@ -123,6 +124,7 @@ export type AsyncRejected> = S & { startedAt: Date finishedAt: Date status: "rejected" + statusCode: number isInitial: false isPending: false isLoading: false diff --git a/packages/react-async/src/useAsync.spec.js b/packages/react-async/src/useAsync.spec.js index e6ae75a5..9f98020e 100644 --- a/packages/react-async/src/useAsync.spec.js +++ b/packages/react-async/src/useAsync.spec.js @@ -266,4 +266,24 @@ describe("useFetch", () => { expect(err.message).toEqual("400 Bad Request") expect(err.response).toBe(errorResponse) }) + + test("statusCode parsing", async () => { + const response = { ok: true, status: 205, body: "", json } + globalScope.fetch.mockResolvedValue(response) + let lastStatus, lastStatusCode + const component = ( + + {({ statusCode, status }) => { + lastStatus = status + lastStatusCode = statusCode + + return
+ }} + + ) + render(component) + await sleep(10) + expect(lastStatus).toEqual("fulfilled") + expect(lastStatusCode).toEqual(205) + }) }) diff --git a/packages/react-async/src/useAsync.tsx b/packages/react-async/src/useAsync.tsx index b3eb22d9..1036f271 100644 --- a/packages/react-async/src/useAsync.tsx +++ b/packages/react-async/src/useAsync.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useDebugValue, useEffect, useMemo, useRef, useReducer } from "react" +import React, { useCallback, useDebugValue, useEffect, useMemo, useRef, useReducer, useState } from "react" import globalScope, { MockAbortController, noop } from "./globalScope" import { @@ -264,6 +264,10 @@ interface FetchRun extends Omit, "run"> { run(): void } +type StatusCode = S & { + statusCode?: number +} + type FetchRunArgs = | [(params?: OverrideParams) => OverrideParams] | [OverrideParams] @@ -280,20 +284,24 @@ function isEvent(e: FetchRunArgs[0]): e is Event | React.SyntheticEvent { * @param {RequestInfo} resource * @param {RequestInit} init * @param {FetchOptions} options - * @returns {AsyncState>} + * @returns {AsyncState>} */ function useAsyncFetch( resource: RequestInfo, init: RequestInit, { defer, json, ...options }: FetchOptions = {} -): AsyncState> { +): AsyncState>> { + const [statusCode, setStatusCode] = useState() const method = (resource as Request).method || (init && init.method) const headers: Headers & Record = (resource as Request).headers || (init && init.headers) || {} const accept: string | undefined = headers["Accept"] || headers["accept"] || (headers.get && headers.get("accept")) const doFetch = (input: RequestInfo, init: RequestInit) => - globalScope.fetch(input, init).then(parseResponse(accept, json)) + globalScope.fetch(input, init).then((response) => { + setStatusCode(response.status) + return parseResponse(accept, json)(response) + }) const isDefer = typeof defer === "boolean" ? defer : ["POST", "PUT", "PATCH", "DELETE"].indexOf(method!) !== -1 const fn = isDefer ? "deferFn" : "promiseFn" @@ -327,7 +335,7 @@ function useAsyncFetch( [fn]: isDefer ? deferFn : promiseFn, }) useDebugValue(state, ({ counter, status }) => `[${counter}] ${status}`) - return state + return { ...state, statusCode } } const unsupported = () => {