Skip to content

Commit

Permalink
feat(hook): also pass action into handler hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Pixeladed committed Oct 22, 2020
1 parent 67bf4f6 commit 793410a
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 37 deletions.
34 changes: 26 additions & 8 deletions src/reducers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AsyncThunk } from '@reduxjs/toolkit';
import { AsyncThunk, AnyAction } from '@reduxjs/toolkit';
import { AsyncState, AsyncStatus, AsyncAdapterOptions } from './types';
import { getDefaultStatus, processStatusWithHook } from './utils';

Expand All @@ -12,7 +12,10 @@ export const createPendingHandler = (options: AsyncAdapterOptions) => <
ThunkApiConfig
>(
asyncThunk: AsyncThunk<Returned, ThunkArg, ThunkApiConfig>
) => (state: Partial<AsyncState<Data>>) => {
) => (
state: Partial<AsyncState<Data>>,
action: ReturnType<typeof asyncThunk['pending']>
) => {
if (!state.status) {
state.status = {};
}
Expand All @@ -27,7 +30,11 @@ export const createPendingHandler = (options: AsyncAdapterOptions) => <
loaded: false,
loading: true,
};
const newStatus = processStatusWithHook(baseStatus, options.onPending);
const newStatus = processStatusWithHook(
action,
baseStatus,
options.onPending
);

state.status[typePrefix] = newStatus;
};
Expand All @@ -42,7 +49,10 @@ export const createFulfilledHandler = (options: AsyncAdapterOptions) => <
ThunkApiConfig
>(
asyncThunk: AsyncThunk<Returned, ThunkArg, ThunkApiConfig>
) => (state: Partial<AsyncState<Data>>) => {
) => (
state: Partial<AsyncState<Data>>,
action: ReturnType<typeof asyncThunk['fulfilled']>
) => {
if (!state.status) {
state.status = {};
}
Expand All @@ -58,7 +68,11 @@ export const createFulfilledHandler = (options: AsyncAdapterOptions) => <
loading: false,
lastLoaded: new Date().toISOString(),
};
const newStatus = processStatusWithHook(baseStatus, options.onFulfilled);
const newStatus = processStatusWithHook(
action,
baseStatus,
options.onFulfilled
);

state.status[typePrefix] = newStatus;
};
Expand Down Expand Up @@ -92,7 +106,11 @@ export const createRejectedHandler = (options: AsyncAdapterOptions) => <
loaded: false,
loading: false,
};
const newStatus = processStatusWithHook(baseStatus, options.onRejected);
const newStatus = processStatusWithHook(
action,
baseStatus,
options.onRejected
);

state.status[typePrefix] = newStatus;
};
Expand All @@ -107,14 +125,14 @@ export const createResetHandler = (options: AsyncAdapterOptions) => <
ThunkApiConfig
>(
asyncThunk: AsyncThunk<Returned, ThunkArg, ThunkApiConfig>
) => (state: Partial<AsyncState<Data>>) => {
) => (state: Partial<AsyncState<Data>>, action: AnyAction) => {
if (!state.status) {
state.status = {};
}

const { typePrefix } = asyncThunk;
const baseStatus = getDefaultStatus(typePrefix);
const newStatus = processStatusWithHook(baseStatus, options.onReset);
const newStatus = processStatusWithHook(action, baseStatus, options.onReset);
state.status[typePrefix] = newStatus;
};

Expand Down
36 changes: 30 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { SerializedError } from '@reduxjs/toolkit';
import { SerializedError, AsyncThunk, AnyAction } from '@reduxjs/toolkit';

export interface AsyncAdapterOptions {
usePayloadAsError?: boolean;
onPending?: HandlerHook;
onFulfilled?: HandlerHook;
onRejected?: HandlerHook;
onReset?: HandlerHook;
onPending?: PendingHandlerHook;
onFulfilled?: FulfilledHandlerHook;
onRejected?: RejectedHandlerHook;
onReset?: ResetHandlerHook;
}

export interface AsyncState<T> {
Expand All @@ -21,4 +21,28 @@ export interface AsyncStatus {
lastLoaded: string | undefined;
}

export type HandlerHook = <I extends AsyncStatus, O extends I>(status: I) => O;
export type PendingHandlerHook = <I extends AsyncStatus, O extends I>(
action: ReturnType<AsyncThunk<any, any, any>['pending']>,
status: I
) => O;

export type FulfilledHandlerHook = <I extends AsyncStatus, O extends I>(
action: ReturnType<AsyncThunk<any, any, any>['fulfilled']>,
status: I
) => O;

export type RejectedHandlerHook = <I extends AsyncStatus, O extends I>(
action: ReturnType<AsyncThunk<any, any, any>['rejected']>,
status: I
) => O;

export type ResetHandlerHook = <I extends AsyncStatus, O extends I>(
action: AnyAction,
status: I
) => O;

export type HandlerHook =
| PendingHandlerHook
| FulfilledHandlerHook
| RejectedHandlerHook
| ResetHandlerHook;
5 changes: 3 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ export const getMatchingSerializedError = (error: Error) => {
};

export const processStatusWithHook = (
action: Parameters<HandlerHook>[0],
status: AsyncStatus,
hook?: HandlerHook
hook: HandlerHook | undefined | null
) => {
if (hook) {
return hook(status);
return (hook as Function)(action, status);
} else {
return status;
}
Expand Down
23 changes: 15 additions & 8 deletions test/reducers/handleFulfilled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ describe('handleFulfilled', () => {
it('adds a new status if none exists', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
status: {},
};

adapter.handleFulfilled(thunk)(state);
adapter.handleFulfilled(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]).toBeTruthy();
});

it('should reset error field', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
Expand All @@ -33,13 +35,14 @@ describe('handleFulfilled', () => {
},
};

adapter.handleFulfilled(thunk)(state);
adapter.handleFulfilled(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]?.error).toBe(undefined);
});

it('should set loading to false', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
Expand All @@ -54,13 +57,14 @@ describe('handleFulfilled', () => {
},
};

adapter.handleFulfilled(thunk)(state);
adapter.handleFulfilled(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]?.loading).toBe(false);
});

it('should set loaded to true', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
Expand All @@ -75,13 +79,14 @@ describe('handleFulfilled', () => {
},
};

adapter.handleFulfilled(thunk)(state);
adapter.handleFulfilled(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]?.loaded).toBe(true);
});

it('should update lastLoaded field', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
Expand All @@ -96,7 +101,7 @@ describe('handleFulfilled', () => {
},
};

adapter.handleFulfilled(thunk)(state);
adapter.handleFulfilled(thunk)(state, action);
expect(
Date.parse(state.status?.[thunk.typePrefix]?.lastLoaded!)
).toBeTruthy();
Expand All @@ -105,26 +110,28 @@ describe('handleFulfilled', () => {
it('creates a status state object is none exist', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: Partial<AsyncState<{}>> = {
data: {},
};

adapter.handleFulfilled(thunk)(state);
adapter.handleFulfilled(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]).toBeTruthy();
});

it('calls the onFulfilled handler hook', () => {
const trap = jest.fn(status => status);
const trap = jest.fn((_, status) => status);
const adapter = createAsyncAdapter({ onFulfilled: trap });
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
status: {},
};

adapter.handleFulfilled(thunk)(state);
adapter.handleFulfilled(thunk)(state, action);
expect(trap).toHaveBeenCalled();
});
});
23 changes: 15 additions & 8 deletions test/reducers/handlePending.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ describe('handlePending', () => {
it('adds a new status if none exists', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
status: {},
};

adapter.handlePending(thunk)(state);
adapter.handlePending(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]).toBeTruthy();
});

it('should reset error field', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
Expand All @@ -33,13 +35,14 @@ describe('handlePending', () => {
},
};

adapter.handlePending(thunk)(state);
adapter.handlePending(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]?.error).toBe(undefined);
});

it('should set loading to true', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
Expand All @@ -54,13 +57,14 @@ describe('handlePending', () => {
},
};

adapter.handlePending(thunk)(state);
adapter.handlePending(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]?.loading).toBe(true);
});

it('should set loaded to false', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
Expand All @@ -75,14 +79,15 @@ describe('handlePending', () => {
},
};

adapter.handlePending(thunk)(state);
adapter.handlePending(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]?.loaded).toBe(false);
});

it('should not change lastLoaded field', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const lastLoaded = new Date().toISOString();
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
Expand All @@ -97,33 +102,35 @@ describe('handlePending', () => {
},
};

adapter.handlePending(thunk)(state);
adapter.handlePending(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]?.lastLoaded).toBe(lastLoaded);
});

it('creates a status state object is none exist', () => {
const adapter = createAsyncAdapter();
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: Partial<AsyncState<{}>> = {
data: {},
};

adapter.handlePending(thunk)(state);
adapter.handlePending(thunk)(state, action);
expect(state.status?.[thunk.typePrefix]).toBeTruthy();
});

it('calls the onPending handler hook', () => {
const trap = jest.fn(status => status);
const trap = jest.fn((_, status) => status);
const adapter = createAsyncAdapter({ onPending: trap });
const thunk = createAsyncThunk('thunk', () => {});
const action = { type: 'mock' } as any;

const state: AsyncState<{}> = {
data: {},
status: {},
};

adapter.handlePending(thunk)(state);
adapter.handlePending(thunk)(state, action);
expect(trap).toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion test/reducers/handleRejected.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe('handleRejected', () => {
});

it('calls the onRejected handler hook', () => {
const trap = jest.fn(status => status);
const trap = jest.fn((_, status) => status);
const adapter = createAsyncAdapter({ onRejected: trap });
const thunk = createAsyncThunk('thunk', () => {});
const error = new Error();
Expand Down
Loading

0 comments on commit 793410a

Please sign in to comment.