diff --git a/README.md b/README.md index b812980..f19a95c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/useResource.ts b/src/useResource.ts index ece032e..9ee19a9 100644 --- a/src/useResource.ts +++ b/src/useResource.ts @@ -51,6 +51,8 @@ export type UseResourceOptions< defaultState?: RequestState; /** Control the return value of the request */ asyncReq?: A; + /** Sets the state to initialState before executing the promise. */ + resetOnReq?: boolean; }; function getDefaultStateLoading( @@ -69,17 +71,27 @@ function getDefaultStateLoading( type Action = | { type: "success"; data: Payload; response: Payload } | { type: "error"; error: RequestError, BodyData> } - | { type: "reset" | "start" }; + | { type: "reset" | "start"; reset?: boolean }; function getNextState( state: RequestState, action: Action, + init?: RequestState, ): RequestState { - 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", }; @@ -115,7 +127,7 @@ export function useResource( const { ready, cancel } = createRequest(...args); - dispatch({ type: "start" }); + dispatch({ type: "start", reset: options?.resetOnReq }); ready() .then(([data, response]) => { dispatch({ type: "success", data, response }); @@ -135,7 +147,7 @@ export function useResource( 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]; diff --git a/src/utils.ts b/src/utils.ts index 7550d46..84d7d1d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import type { ComputedRef, Ref } from "vue"; import { isReactive, readonly, ref, unref } from "vue"; -type Reducer = (prevState: S, action: A) => S; +type Reducer = (prevState: S, action: A, initSate?: Partial) => S; type ReducerState> = R extends Reducer ? S : never; @@ -23,7 +23,7 @@ export function useReducer>( ): [Readonly>>, (action: ReducerAction) => void] { const state = ref(initialArg); const dispatch = (action: ReducerAction) => { - state.value = reducer(state.value, action); + state.value = reducer(state.value, action, initialArg); }; return [readonly(state) as Readonly>>, dispatch]; diff --git a/tests/useResource.test.ts b/tests/useResource.test.ts index 7cfbef6..a7f2ea1 100644 --- a/tests/useResource.test.ts +++ b/tests/useResource.test.ts @@ -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( @@ -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(); + expectTypeOf(r).toEqualTypeOf>(); + expectTypeOf(a).toEqualTypeOf>(); + + 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(); + expectTypeOf(r).toEqualTypeOf>(); + expectTypeOf(a).toEqualTypeOf>(); + + 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() { diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 1cb8221..5937f04 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -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"; @@ -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 }; @@ -63,6 +65,11 @@ describe("useReducer", () => { dispatch({ type: "sub" }); expect(state.value.num).toBe(2); + expectTypeOf(state).toEqualTypeOf>>(); + expectTypeOf(dispatch).toEqualTypeOf< + (action: { type: "add" | "sub" }) => void + >(); + try { dispatch(undefined as any); } catch (error) {