Skip to content

Commit

Permalink
Merge pull request #21 from axios-use/feat-custom-request
Browse files Browse the repository at this point in the history
Feat(request): export `_request` function
  • Loading branch information
wangcch authored Oct 20, 2023
2 parents 67d01dc + 7b51cb3 commit 0c49f44
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 61 deletions.
11 changes: 11 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
"overrides": [
{
"files": ["**/tests/**/*"],
"rules": {
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off"
}
}
],
"settings": {
"react": {
"pragma": "React",
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,24 @@ const usersRes = await axios(api.getUsers());
const userRes = await axios(api.getUserInfo("ID001"));
```

custom response type. (if you change the response's return value. like axios.interceptors.response)

```ts
import { request, _request } from "@axios-use/react";
const [reqState] = useResource(() => request<DataType>({ url: `/users` }), []);
// AxiosResponse<DataType>
reqState.response;
// DataType
reqState.data;

// custom response type
const [reqState] = useResource(() => _request<MyWrapper<DataType>>({ url: `/users` }), []);
// MyWrapper<DataType>
reqState.response;
// MyWrapper<DataType>["data"]. maybe `undefined` type.
reqState.data;
```

#### createRequestError

The `createRequestError` normalizes the error response. This function is used internally as well. The `isCancel` flag is returned, so you don't have to call **axios.isCancel** later on the promise catch block.
Expand Down
18 changes: 18 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,24 @@ const usersRes = await axios(api.getUsers());
const userRes = await axios(api.getUserInfo("ID001"));
```

自定义 response 类型. (如果你有手动修改 response 数据的需求。 axios.interceptors.response)

```ts
import { request, _request } from "@axios-use/react";
const [reqState] = useResource(() => request<DataType>({ url: `/users` }), []);
// AxiosResponse<DataType>
reqState.response;
// DataType
reqState.data;

// 自定义 response 类型
const [reqState] = useResource(() => _request<MyWrapper<DataType>>({ url: `/users` }), []);
// MyWrapper<DataType>
reqState.response;
// MyWrapper<DataType>["data"]. maybe `undefined` type.
reqState.data;
```

#### createRequestError

`createRequestError` 用于规范错误响应(该函数也默认在内部调用)。 `isCancel` 标志被返回,因此也不必在 promise catch 块上调用 **axios.isCancel**
Expand Down
2 changes: 1 addition & 1 deletion src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface Cache<T = any> {
clear(): void;
}

const SLASHES_REGEX = /^\/|\/$/g;
const SLASHES_REGEX = /(?:^\/)|(?:\/$)/g;

export const defaultCacheKeyGenerator = <T = any, D = any>(
config: Resource<T, D>,
Expand Down
91 changes: 68 additions & 23 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,56 @@ export type AxiosRestResponse<D = any> = Omit<
"data"
>;

export interface Resource<TPayload, D = any> extends AxiosRequestConfig<D> {
payload?: TPayload;
export interface Resource<
T = AxiosResponse,
D = any,
K1 extends keyof T = never,
K2 extends keyof T[K1] = never,
K3 extends keyof T[K1][K2] = never,
> extends AxiosRequestConfig<D> {
_payload?: T;
_payload_item?: [K3] extends [never]
? [K2] extends [never]
? [K1] extends [never]
? T extends AxiosResponse<infer DD> | { data?: infer DD }
? DD
: undefined
: T[K1]
: T[K1][K2]
: T[K1][K2][K3];
}

export type Request<T = any, D = any> = (...args: any[]) => Resource<T, D>;
export type Request<
T = any,
D = any,
K1 extends keyof T = any,
K2 extends keyof T[K1] = any,
K3 extends keyof T[K1][K2] = any,
> = (...args: any[]) => Resource<T, D, K1, K2, K3>;

export type Payload<T extends Request> = ReturnType<T>["payload"];
export type BodyData<T extends Request> = ReturnType<T>["data"];
type _AnyKeyValue<T, K> = K extends keyof T ? T[K] : any;

export type Payload<T extends Request, Check = false> = Check extends true
? _AnyKeyValue<ReturnType<T>, "_payload_item">
: T extends Request<AxiosResponse>
? Exclude<_AnyKeyValue<ReturnType<T>, "_payload">, undefined>
: _AnyKeyValue<ReturnType<T>, "_payload">;
export type BodyData<T extends Request> = _AnyKeyValue<ReturnType<T>, "data">;
/** @deprecated No longer use. Use `BodyData` instead */
export type CData<T extends Request> = BodyData<T>;

export interface RequestFactory<T extends Request> {
(...args: Parameters<T>): {
cancel: Canceler;
ready: () => Promise<[Payload<T>, AxiosResponse<Payload<T>, BodyData<T>>]>;
};
}
export type RequestFactory<T extends Request> = (...args: Parameters<T>) => {
cancel: Canceler;
ready: () => Promise<readonly [Payload<T, true>, Payload<T>]>;
};

export interface RequestDispatcher<T extends Request> {
(...args: Parameters<T>): Canceler;
}
export type RequestDispatcher<T extends Request> = (
...args: Parameters<T>
) => Canceler;

// Normalize the error response returned from our hooks
/**
* Normalize the error response returned from `@axios-use/vue`
*/
export interface RequestError<
T = any,
D = any,
Expand All @@ -48,19 +75,37 @@ export interface RequestError<
}

export type RequestCallbackFn<T extends Request> = {
onCompleted?: (
data: Payload<T>,
response: AxiosResponse<Payload<T>, BodyData<T>>,
) => void;
onError?: (err?: RequestError<Payload<T>, BodyData<T>>) => void;
/**
* A callback function that's called when your request successfully completes with zero errors.
* This function is passed the request's result `data` and `response`.
*/
onCompleted?: (data: Payload<T, true>, response: Payload<T>) => void;
/**
* A callback function that's called when the request encounters one or more errors.
* This function is passed an `RequestError` object that contains either a networkError object or a `AxiosError`, depending on the error(s) that occurred.
*/
onError?: (err: RequestError<Payload<T>, BodyData<T>>) => void;
};

export function request<T, D = any>(
config: AxiosRequestConfig<D>,
): Resource<T, D> {
/**
* For TypeScript type deduction
*/
export function _request<
T,
D = any,
K1 extends keyof T = never,
K2 extends keyof T[K1] = never,
K3 extends keyof T[K1][K2] = never,
>(config: AxiosRequestConfig<D>): Resource<T, D, K1, K2, K3> {
return config;
}

/**
* For TypeScript type deduction
*/
export const request = <T = any, D = any>(config: AxiosRequestConfig<D>) =>
_request<AxiosResponse<T, D>, D>(config);

export function createRequestError<
T = any,
D = any,
Expand Down
18 changes: 7 additions & 11 deletions src/requestContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from "react";
import React, { createContext, useMemo } from "react";
import type { PropsWithChildren } from "react";
import { createContext } from "react";
import type { AxiosInstance } from "axios";

import type { RequestError } from "./request";
Expand Down Expand Up @@ -41,16 +40,13 @@ export const RequestProvider = <T,>(
...rest
} = props;

const providerValue = useMemo(
() => ({ instance, cache, cacheKey, cacheFilter, customCreateReqError }),
[cache, cacheFilter, cacheKey, customCreateReqError, instance],
);

return (
<RequestContext.Provider
value={{
instance,
cache,
cacheKey,
cacheFilter,
customCreateReqError,
}}
{...rest}>
<RequestContext.Provider value={providerValue} {...rest}>
{children}
</RequestContext.Provider>
);
Expand Down
17 changes: 11 additions & 6 deletions src/useRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
CancelTokenSource,
Canceler,
CancelToken,
AxiosResponse,
AxiosInstance,
} from "axios";
import axios from "axios";
Expand All @@ -14,6 +13,7 @@ import type {
Request,
Payload,
BodyData,
RequestError,
} from "./request";
import { createRequestError } from "./request";
import { RequestContext } from "./requestContext";
Expand Down Expand Up @@ -72,11 +72,14 @@ export function useRequest<T extends Request>(
setSources((prevSources) => [...prevSources, source]);
}
return axiosInstance({ ...config, cancelToken: source.token })
.then((response: AxiosResponse<Payload<T>, BodyData<T>>) => {
.then((response) => {
removeCancelToken(source.token);

onCompletedRef.current?.(response.data, response);
return [response.data, response];
onCompletedRef.current?.(
response.data as Payload<T, true>,
response as Payload<T>,
);
return [response.data, response as Payload<T>] as const;
})
.catch((err: AxiosError<Payload<T>, BodyData<T>>) => {
removeCancelToken(source.token);
Expand All @@ -85,10 +88,12 @@ export function useRequest<T extends Request>(
? customCreateReqError(err)
: createRequestError(err);

onErrorRef.current?.(error);
onErrorRef.current?.(
error as RequestError<Payload<T>, BodyData<T>>,
);

throw error;
}) as Promise<[Payload<T>, AxiosResponse<BodyData<T>>]>;
});
};

return {
Expand Down
33 changes: 17 additions & 16 deletions src/useResource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useCallback, useContext, useReducer, useMemo } from "react";
import type { Canceler, AxiosResponse } from "axios";
import type { Canceler } from "axios";
import { useRequest } from "./useRequest";
import type {
Payload,
Expand All @@ -22,19 +22,19 @@ import { useDeepMemo, useMountedState, useRefFn, getStrByFn } from "./utils";
const REQUEST_CLEAR_MESSAGE =
"A new request has been made before completing the last one";

type RequestState<TRequest extends Request> = {
data?: Payload<TRequest>;
response?: AxiosResponse<BodyData<TRequest>>;
error?: RequestError<Payload<TRequest>, BodyData<TRequest>>;
type RequestState<T extends Request> = {
data?: Payload<T, true>;
response?: Payload<T>;
error?: RequestError<Payload<T>, BodyData<T>>;
isLoading?: boolean;

/** @deprecated Use `response` instead */
other?: AxiosResponse<BodyData<TRequest>>;
other?: Payload<T>;
};

export type UseResourceResult<TRequest extends Request> = [
RequestState<TRequest> & { cancel: Canceler },
RequestDispatcher<TRequest>,
export type UseResourceResult<T extends Request> = [
RequestState<T> & { cancel: Canceler },
RequestDispatcher<T>,
() => Canceler | undefined,
];

Expand Down Expand Up @@ -62,14 +62,14 @@ function getDefaultStateLoading<T extends Request>(
return undefined;
}

type Action<T, D = any> =
| { type: "success"; data: T; response: AxiosResponse<D> }
| { type: "error"; error: RequestError<T, D> }
type Action<T extends Request> =
| { type: "success"; data: Payload<T, true>; response: Payload<T> }
| { type: "error"; error: RequestError<Payload<T>, BodyData<T>> }
| { type: "reset" | "start" };

function getNextState<TRequest extends Request>(
state: RequestState<TRequest>,
action: Action<Payload<TRequest>, BodyData<TRequest>>,
action: Action<TRequest>,
): RequestState<TRequest> {
const response = action.type === "success" ? action.response : state.response;

Expand Down Expand Up @@ -134,9 +134,10 @@ export function useResource<T extends Request>(
);
}, [RequestConfig.cacheKey, fnOptions, options?.cacheKey, requestCache]);
const cacheData = useMemo(() => {
return requestCache && cacheKey && typeof requestCache.get === "function"
? requestCache.get(cacheKey) ?? undefined
: undefined;
if (requestCache && cacheKey && typeof requestCache.get === "function") {
return (requestCache.get(cacheKey) as Payload<T, true>) ?? undefined;
}
return undefined;
}, [cacheKey, requestCache]);

const [createRequest, { clear }] = useRequest(fn, {
Expand Down
Loading

0 comments on commit 0c49f44

Please sign in to comment.