Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(request): export _request function #21

Merged
merged 8 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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