Skip to content

Feat: getResponseItem options (custom data value) #1

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

Merged
merged 9 commits into from
May 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
61 changes: 43 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ import { useRequest, useResource } from "@axios-use/vue";

### Options (optional)

| config | type | default | explain |
| -------- | ------ | ------- | ------------------------------------------------------------- |
| instance | object | `axios` | Axios instance. You can pass your axios with a custom config. |
| config | type | default | explain |
| --------------- | -------- | --------------- | --------------------------------------------------------------------------------------------------------------------- |
| instance | object | `axios` | Axios instance. You can pass your axios with a custom config. |
| getResponseItem | function | `(r) => r.data` | custom `data` value. The default value is response['data']. [PR#1](https://github.com/axios-use/axios-use-vue/pull/1) |

```ts
import axios from "axios";
Expand All @@ -83,12 +84,13 @@ Vue.use(AxiosUseVue, { instance: axiosInstance });

### useRequest

| option | type | explain |
| ------------------- | --------------- | ------------------------------------------------ |
| fn | function | get AxiosRequestConfig function |
| options.onCompleted | function | This function is passed the query's result data. |
| options.onError | function | This function is passed an `RequestError` object |
| options.instance | `AxiosInstance` | Customize the Axios instance of the current item |
| option | type | explain |
| ----------------------- | --------------- | ------------------------------------------------ |
| fn | function | get AxiosRequestConfig function |
| options.onCompleted | function | This function is passed the query's result data. |
| options.onError | function | This function is passed an `RequestError` object |
| options.instance | `AxiosInstance` | Customize the Axios instance of the current item |
| options.getResponseItem | function | custom returns the value of `data`(index 0). |

```ts
// js
Expand Down Expand Up @@ -136,15 +138,16 @@ const [createRequest, { hasPending, cancel }] = useRequest(

### useResource

| option | type | explain |
| -------------------- | --------------- | ------------------------------------------------------------------- |
| fn | function | get AxiosRequestConfig function |
| parameters | array \| false | `fn` function parameters. effect dependency list |
| options.filter | function | Request filter. if return a falsy value, will not start the request |
| options.defaultState | object | Initialize the state value. `{data, response, error, isLoading}` |
| options.onCompleted | function | This function is passed the query's result data. |
| options.onError | function | This function is passed an `RequestError` object |
| options.instance | `AxiosInstance` | Customize the Axios instance of the current item |
| option | type | explain |
| ----------------------- | --------------- | ------------------------------------------------------------------- |
| fn | function | get AxiosRequestConfig function |
| parameters | array \| false | `fn` function parameters. effect dependency list |
| options.filter | function | Request filter. if return a falsy value, will not start the request |
| options.defaultState | object | Initialize the state value. `{data, response, error, isLoading}` |
| options.onCompleted | function | This function is passed the query's result data. |
| options.onError | function | This function is passed an `RequestError` object |
| options.instance | `AxiosInstance` | Customize the Axios instance of the current item |
| options.getResponseItem | function | custom returns the value of `data`(index 0). |

```ts
// js
Expand Down Expand Up @@ -260,6 +263,8 @@ const [reqState] = useResource(
The `request` function allows you to define the response type coming from it. It also helps with creating a good pattern on defining your API calls and the expected results. It's just an identity function that accepts the request config and returns it. Both `useRequest` and `useResource` extract the expected and annotated type definition and resolve it on the `response.data` field.

```ts
import { request } from "@axios-use/vue";

const api = {
getUsers: () => {
return request<Users>({
Expand All @@ -285,6 +290,26 @@ 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/vue";

const [reqState] = useResource(() => request<DataType>({ url: `/users` }));
// AxiosResponse<DataType>
unref(reqState).response;
// DataType
unref(reqState).data;

// custom response type
const [reqState] = useResource(() => _request<MyWrapper<DataType>>({ url: `/users` }));
// MyWrapper<DataType>
unref(reqState).response;
// MyWrapper<DataType>["data"]. maybe `undefined` type.
// You can use `getResponseItem` to customize the value of `data`
unref(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
15 changes: 9 additions & 6 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { InjectionKey } from "vue";
import { getCurrentInstance, inject } from "vue";

import type { AxiosInstance } from "axios";
import type { AxiosInstance, AxiosResponse } from "axios";
import axios from "axios";

import type { App } from "../demi";
Expand All @@ -12,6 +12,8 @@ const INJECT_INSIDE_WARN_MSG =
export type RequestConfigType = {
/** Axios instance. You can pass your axios with a custom config. */
instance?: AxiosInstance;
/** custom `data` value. @default response['data'] */
getResponseItem?: (res?: any) => unknown;
};

export const AXIOS_USE_VUE_PROVIDE_KEY = Symbol(
Expand Down Expand Up @@ -39,16 +41,17 @@ export const setUseRequestConfig = (app: App, options?: RequestConfigType) => {
}
};

const defaultGetResponseData = (res: AxiosResponse) => res?.data;

export const getUseRequestConfig = (): RequestConfigType &
Required<Pick<RequestConfigType, "instance">> => {
Required<Pick<RequestConfigType, "instance" | "getResponseItem">> => {
const _isInside = Boolean(getCurrentInstance());
if (!_isInside) {
console.warn(INJECT_INSIDE_WARN_MSG);
}

const { instance = axios } = _isInside
? inject<RequestConfigType>(AXIOS_USE_VUE_PROVIDE_KEY, {})
: {};
const { instance = axios, getResponseItem = defaultGetResponseData } =
_isInside ? inject<RequestConfigType>(AXIOS_USE_VUE_PROVIDE_KEY, {}) : {};

return { instance };
return { instance, getResponseItem };
};
62 changes: 38 additions & 24 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,44 @@ import type {
} from "axios";
import axios from "axios";

export type _ResponseDataItemType<T> = T extends AxiosResponse<infer D1>
? D1
: T extends { data: infer D2 } | { data?: infer D2 }
? D2
: undefined;

export interface Resource<T, D = any, W = AxiosResponse>
extends AxiosRequestConfig<D> {
_payload?: W extends AxiosResponse ? AxiosResponse<T, D> : T;
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, W = any> = (
...args: any[]
) => Resource<T, D, W>;
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, Check = false> = Check extends true
? _ResponseDataItemType<ReturnType<T>["_payload"]>
? ReturnType<T>["_payload_item"]
: T extends Request<AxiosResponse>
? NonNullable<ReturnType<T>["_payload"]>
: ReturnType<T>["_payload"];
export type BodyData<T extends Request> = ReturnType<T>["data"];

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

Expand Down Expand Up @@ -57,10 +71,7 @@ export type RequestCallbackFn<T extends Request> = {
* 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: NonNullable<Payload<T>>,
) => void;
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.
Expand All @@ -71,18 +82,21 @@ export type RequestCallbackFn<T extends Request> = {
/**
* For TypeScript type deduction
*/
export function _request<T, D = any, W = false>(
config: AxiosRequestConfig<D>,
): Resource<T, D, W> {
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, D = any, W = AxiosResponse<T, D>>(
config: AxiosRequestConfig<D>,
) => _request<T, D, W>(config);
export const request = <T, D = any>(config: AxiosRequestConfig<D>) =>
_request<AxiosResponse<T, D>, D>(config);

export function createRequestError<
T = any,
Expand Down
13 changes: 10 additions & 3 deletions src/useRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { createRequestError } from "./request";

export type UseRequestOptions<T extends Request> = RequestCallbackFn<T> & {
instance?: AxiosInstance;
/** custom returns the value of `data`(index 0). @default (r) => r?.data */
getResponseItem?: (res?: any) => unknown;
};

export type UseRequestResult<T extends Request> = [
Expand Down Expand Up @@ -73,11 +75,16 @@ export function useRequest<T extends Request>(
sources.value = [...unref(sources), _source];

return _axiosIns({ ..._config, cancelToken: _source.token })
.then((res: NonNullable<Payload<T>>) => {
.then((res) => {
removeCancelToken(_source.token);

onCompleted?.(res?.data, res);
return [res?.data, res] as const;
const _data = (
options?.getResponseItem
? options.getResponseItem(res as Payload<T>)
: requestConfig.getResponseItem(res)
) as Payload<T, true>;
onCompleted?.(_data, res as Payload<T>);
return [_data, res as Payload<T>] as const;
})
.catch((err: AxiosError<Payload<T>, BodyData<T>>) => {
removeCancelToken(_source.token);
Expand Down
17 changes: 8 additions & 9 deletions src/useResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export type UseResourceResult<T extends Request> = [

export type UseResourceOptions<T extends Request> = Pick<
RequestConfigType,
"instance"
"instance" | "getResponseItem"
> &
RequestCallbackFn<T> & {
/** Conditional Fetching */
Expand Down Expand Up @@ -96,6 +96,7 @@ export function useResource<T extends Request>(
onCompleted: options?.onCompleted,
onError: options?.onError,
instance: options?.instance,
getResponseItem: options?.getResponseItem,
});

const [state, dispatch] = useReducer(getNextState, {
Expand All @@ -111,19 +112,17 @@ export function useResource<T extends Request>(

const { ready, cancel } = createRequest(...args);

void (async () => {
try {
dispatch({ type: "start" });
const [data, response] = await ready();

dispatch({ type: "start" });
ready()
.then(([data, response]) => {
dispatch({ type: "success", data, response });
} catch (e) {
})
.catch((e) => {
const error = e as RequestError<Payload<T>, BodyData<T>>;
if (!error.isCancel) {
dispatch({ type: "error", error });
}
}
})();
});

return cancel;
};
Expand Down
43 changes: 42 additions & 1 deletion tests/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe("context", () => {
const vm = getCurrentInstance2();

expect(
(vm.proxy as any)._provided[AXIOS_USE_VUE_PROVIDE_KEY as any]
(vm?.proxy as any)._provided[AXIOS_USE_VUE_PROVIDE_KEY as any]
?.instance,
).toBe(mockAxiosIns);

Expand Down Expand Up @@ -156,3 +156,44 @@ describe("context", () => {
});
});
});

describe("config - getResponseItem", () => {
test("default value", () => {
const Component = defineComponent({
setup() {
const { getResponseItem } = getUseRequestConfig();
expect(getResponseItem).toBeDefined();
expect(getResponseItem({ data: 1 })).toBe(1);
expect(getResponseItem({})).toBeUndefined();
expect(getResponseItem()).toBeUndefined();

return () => h("div");
},
});

mount(Component);
});

test("custom", () => {
const fn = (r) => ({
o: r,
d: r?.data,
msg: r?.message || r?.statusText || r?.status,
});
const Component = defineComponent({
setup() {
const { getResponseItem } = getUseRequestConfig();
expect(getResponseItem).toBeDefined();
expect(getResponseItem({ data: 1 })).toStrictEqual(fn({ data: 1 }));
expect(getResponseItem({})).toStrictEqual(fn({}));
expect(getResponseItem()).toStrictEqual(fn(undefined));

return () => h("div");
},
});

mount(Component, (app) => {
app.use(AxioUseVue, { getResponseItem: fn });
});
});
});
6 changes: 3 additions & 3 deletions tests/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ describe("type checking", () => {
const rq2 = () => _request<DataType2>({});

it("request", () => {
expectTypeOf(rq0()).toEqualTypeOf<Resource<DataType, any, false>>();
expectTypeOf(rq0()).toEqualTypeOf<Resource<DataType, any>>();
expectTypeOf(rq1()).toEqualTypeOf<
Resource<DataType, any, AxiosResponse<DataType, any>>
Resource<AxiosResponse<DataType>, any, "data">
>();

const c0 = null as unknown as Payload<typeof rq0>;
Expand All @@ -84,7 +84,7 @@ describe("type checking", () => {
expectTypeOf(c1).toEqualTypeOf<undefined>();

const c2 = null as unknown as Payload<typeof rq1>;
expectTypeOf(c2).toEqualTypeOf<AxiosResponse<DataType, any> | undefined>();
expectTypeOf(c2).toEqualTypeOf<AxiosResponse<DataType, any>>();
const c3 = null as unknown as Payload<typeof rq1, true>;
expectTypeOf(c3).toEqualTypeOf<DataType | undefined>();

Expand Down
Loading