Skip to content

Feat(useResource): resetOnReq option #4

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

Draft
wants to merge 3 commits into
base: feat-async-req
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const [createRequest, { hasPending, cancel }] = useRequest(
| options.instance | `AxiosInstance` | Customize the Axios instance of the current item |
| options.getResponseItem | function | custom returns the value of `data`(index 0). |
| options.asyncReq | boolean | Control the return value of the request |
| options.resetOnReq | boolean | Sets the state to initialState before executing the promise |

```ts
// js
Expand Down
24 changes: 18 additions & 6 deletions src/useResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export type UseResourceOptions<
defaultState?: RequestState<T>;
/** Control the return value of the request */
asyncReq?: A;
/** Sets the state to initialState before executing the promise. */
resetOnReq?: boolean;
};

function getDefaultStateLoading<T extends Request>(
Expand All @@ -69,17 +71,27 @@ function getDefaultStateLoading<T extends Request>(
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" };
| { type: "reset" | "start"; reset?: boolean };

function getNextState<T extends Request>(
state: RequestState<T>,
action: Action<T>,
init?: RequestState<T>,
): RequestState<T> {
const response = action.type === "success" ? action.response : state.response;
let _data = state.data;
let _response = state.response;
if (action.type === "success") {
_data = action.data;
_response = action.response;
}
if ((action.type === "start" || action.type === "reset") && action.reset) {
_data = init?.data;
_response = init?.response;
}

return {
data: action.type === "success" ? action.data : state.data,
response,
data: _data,
response: _response,
error: action.type === "error" ? action.error : undefined,
isLoading: action.type === "start",
};
Expand Down Expand Up @@ -115,7 +127,7 @@ export function useResource<T extends Request, A extends boolean = false>(

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

dispatch({ type: "start" });
dispatch({ type: "start", reset: options?.resetOnReq });
ready()
.then(([data, response]) => {
dispatch({ type: "success", data, response });
Expand All @@ -135,7 +147,7 @@ export function useResource<T extends Request, A extends boolean = false>(
const { ready } = createRequest(...args);

try {
dispatch({ type: "start" });
dispatch({ type: "start", reset: options?.resetOnReq });
const [data, response] = await ready();
dispatch({ type: "success", data, response });
return [data, response];
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ComputedRef, Ref } from "vue";
import { isReactive, readonly, ref, unref } from "vue";

type Reducer<S, A> = (prevState: S, action: A) => S;
type Reducer<S, A> = (prevState: S, action: A, initSate?: Partial<S>) => S;
type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any>
? S
: never;
Expand All @@ -23,7 +23,7 @@ export function useReducer<R extends Reducer<any, any>>(
): [Readonly<Ref<ReducerState<R>>>, (action: ReducerAction<R>) => void] {
const state = ref(initialArg);
const dispatch = (action: ReducerAction<R>) => {
state.value = reducer(state.value, action);
state.value = reducer(state.value, action, initialArg);
};

return [readonly(state) as Readonly<Ref<ReducerState<R>>>, dispatch];
Expand Down
131 changes: 130 additions & 1 deletion tests/useResource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ describe("useResource", () => {
);
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBe("false");

wrapper.get('[data-t-id="add"]').trigger("click");
await wrapper.get('[data-t-id="add"]').trigger("click");
expect(wrapper.get('[data-t-id="res.data"]').text()).toBe(
JSON.stringify(MOCK_DATA_USER_LIST.find((i) => i.id === "1")),
);
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBe("true");

await flushPromises();
expect(wrapper.get('[data-t-id="res.data"]').text()).toBe(
Expand Down Expand Up @@ -255,6 +259,131 @@ describe("useResource", () => {
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBe("false");
});

test("options: resetOnReq-true", async () => {
const Component = defineComponent({
setup() {
const id = ref("1");
const params = computed(() => ({ id: unref(id) }));
const reqFn = getAPIFuncs(true).user.get;
const [res] = useResource(reqFn, [params], {
onCompleted: (d, r, a) => {
expectTypeOf(d).toEqualTypeOf<MockDataUserItem | undefined>();
expectTypeOf(r).toEqualTypeOf<AxiosResponse<MockDataUserItem>>();
expectTypeOf(a).toEqualTypeOf<Parameters<typeof reqFn>>();

const _item = MOCK_DATA_USER_LIST.find((i) => i.id === unref(id));
expect(d).toStrictEqual(_item);
expect(r.data).toStrictEqual(_item);
expect(a).toStrictEqual([{ id: unref(id) }]);
},
resetOnReq: true,
});

const onAdd = () => {
id.value = String(Number(unref(id)) + 1);
};

return () =>
h("div", [
h("button", { "data-t-id": "add", onClick: onAdd }, "add"),
h(
"div",
{ "data-t-id": "res.data" },
JSON.stringify(res.value.data),
),
h("div", { "data-t-id": "res.isLoading" }, res.value.isLoading),
]);
},
});

const wrapper = mount(Component);

expect(wrapper.get('[data-t-id="res.data"]').text()).toBe("");
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBeTruthy();

await flushPromises();
expect(wrapper.get('[data-t-id="res.data"]').text()).toBe(
JSON.stringify(MOCK_DATA_USER_LIST.find((i) => i.id === "1")),
);
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBe("false");

await wrapper.get('[data-t-id="add"]').trigger("click");

expect(wrapper.get('[data-t-id="res.data"]').text()).toBe("");
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBe("true");

await flushPromises();
expect(wrapper.get('[data-t-id="res.data"]').text()).toBe(
JSON.stringify(MOCK_DATA_USER_LIST.find((i) => i.id === "2")),
);
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBe("false");
});
test("options: resetOnReq-true (with defaultState)", async () => {
const INIT_VALUE: MockDataUserItem = { id: "xxx", name: "xxx" };
const Component = defineComponent({
setup() {
const id = ref("1");
const params = computed(() => ({ id: unref(id) }));
const reqFn = getAPIFuncs(true).user.get;
const [res] = useResource(reqFn, [params], {
onCompleted: (d, r, a) => {
expectTypeOf(d).toEqualTypeOf<MockDataUserItem | undefined>();
expectTypeOf(r).toEqualTypeOf<AxiosResponse<MockDataUserItem>>();
expectTypeOf(a).toEqualTypeOf<Parameters<typeof reqFn>>();

const _item = MOCK_DATA_USER_LIST.find((i) => i.id === unref(id));
expect(d).toStrictEqual(_item);
expect(r.data).toStrictEqual(_item);
expect(a).toStrictEqual([{ id: unref(id) }]);
},
resetOnReq: true,
defaultState: { data: INIT_VALUE },
});

const onAdd = () => {
id.value = String(Number(unref(id)) + 1);
};

return () =>
h("div", [
h("button", { "data-t-id": "add", onClick: onAdd }, "add"),
h(
"div",
{ "data-t-id": "res.data" },
JSON.stringify(res.value.data),
),
h("div", { "data-t-id": "res.isLoading" }, res.value.isLoading),
]);
},
});

const wrapper = mount(Component);

expect(wrapper.get('[data-t-id="res.data"]').text()).toBe(
JSON.stringify(INIT_VALUE),
);
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBeTruthy();

await flushPromises();
expect(wrapper.get('[data-t-id="res.data"]').text()).toBe(
JSON.stringify(MOCK_DATA_USER_LIST.find((i) => i.id === "1")),
);
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBe("false");

await wrapper.get('[data-t-id="add"]').trigger("click");

expect(wrapper.get('[data-t-id="res.data"]').text()).toBe(
JSON.stringify(INIT_VALUE),
);
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBe("true");

await flushPromises();
expect(wrapper.get('[data-t-id="res.data"]').text()).toBe(
JSON.stringify(MOCK_DATA_USER_LIST.find((i) => i.id === "2")),
);
expect(wrapper.get('[data-t-id="res.isLoading"]').text()).toBe("false");
});

test("reactive parameter", async () => {
const Component = defineComponent({
setup() {
Expand Down
11 changes: 9 additions & 2 deletions tests/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, test, expect } from "vitest";
import { describe, test, expect, expectTypeOf } from "vitest";
import type { Ref } from "vue";
import { isRef, ref, reactive, unref } from "vue";

import { unrefs, useReducer, hasReactive } from "../src/utils";
Expand Down Expand Up @@ -42,7 +43,8 @@ describe("useReducer", () => {

test("return - dispatch", () => {
const [state, dispatch] = useReducer(
(s: { num: number }, action: { type: "add" | "sub" }) => {
(s: { num: number }, action: { type: "add" | "sub" }, init: any) => {
expect(init.num).toBe(0);
const _type = action?.type;
if (_type === "add") {
return { num: s.num + 1 };
Expand All @@ -63,6 +65,11 @@ describe("useReducer", () => {
dispatch({ type: "sub" });
expect(state.value.num).toBe(2);

expectTypeOf(state).toEqualTypeOf<Readonly<Ref<{ num: number }>>>();
expectTypeOf(dispatch).toEqualTypeOf<
(action: { type: "add" | "sub" }) => void
>();

try {
dispatch(undefined as any);
} catch (error) {
Expand Down