Skip to content

Commit

Permalink
fix: support for legacy reducer types
Browse files Browse the repository at this point in the history
  • Loading branch information
iamogbz committed Sep 13, 2020
1 parent 09a5ecd commit b76cd0c
Show file tree
Hide file tree
Showing 11 changed files with 73 additions and 64 deletions.
13 changes: 8 additions & 5 deletions src/components/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ export function Provider<S, T extends string, P>({
[reducerDispatch],
);

const enhanced = React.useMemo<ContextValue<S, T, P>>(() => {
const { enhancer, ...value } = root;
Object.assign(value, { dispatch, getState });
return enhancer?.(value) ?? value;
}, [dispatch, getState, root]);
const enhanced = React.useMemo<ContextValue<S, T, P>>(
function enhance() {
const { enhancer, ...value } = root;
Object.assign(value, { dispatch, getState });
return enhancer?.(value) ?? value;
},
[dispatch, getState, root],
);

React.useEffect(
function initialiseContext() {
Expand Down
2 changes: 1 addition & 1 deletion src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export function createAction<T extends string, P, S>(
type: T,
prepare = (payload?: P): Partial<Action<T, P>> => ({ payload }),
): ActionCreator<T, P, S> {
return (payload): Action<T, P> => {
return function createAction(payload): Action<T, P> {
const partialAction = prepare(payload);
return { ...partialAction, type };
};
Expand Down
4 changes: 2 additions & 2 deletions src/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { setGlobalContext } from "./components/Context";

function createUnimplemented(objectName?: string): (m: string) => () => never {
const prefix = objectName ? `${objectName}.` : "";
return function unimplemented(methodName) {
return (): never => {
return function createUnimplemented(methodName) {
return function unimplemented(): never {
throw new Error(`Unimplemented method: ${prefix}${methodName}`);
};
};
Expand Down
47 changes: 17 additions & 30 deletions src/createDuck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,6 @@ function getNS<N extends string, T extends string, V extends string>(
return `${name}/${actionType}` as V;
}

function reverseMap<A extends string, B extends string>(
map?: Record<A, B>,
): Record<B, Set<A>> {
if (!map) return {} as Record<B, Set<A>>;
const reversedMap = {} as Record<B, Set<A>>;
for (const [mappedActionType, actionType] of getEntries(map)) {
if (!reversedMap[actionType]) {
reversedMap[actionType] = new Set<A>();
}
reversedMap[actionType].add(mappedActionType);
}
return reversedMap;
}

export function createDuck<
S,
P,
Expand All @@ -40,29 +26,30 @@ export function createDuck<
}: {
name: N;
initialState: S;
reducers: ActionReducerMapping<S, U, P>;
reducers: ActionReducerMapping<S, U, P>; // Reducers = { CreatorName: Reducer(State, ActionType, PayloadTypes) }
selectors?: SelectorMapping<S, R, T, P, Q>;
actionMapping?: Record<T, U>;
actionMapping?: Record<T, U>; // ActionMapping = { MappedActionType: ActionType }
}): Duck<S, N, T, P, R, Q, U> {
const mappedActionTypes = reverseMap(actionMapping);
const mappedActionTypes = { ...actionMapping } as Record<T, U>;
const actions = {} as ActionCreatorMapping<T, P, S, U>;
const namedspacedReducers = {} as ActionReducerMapping<S, T, P>;
for (const [actionType, reducer] of getEntries(reducers)) {
for (const [actionType] of getEntries(reducers)) {
const namespacedActionType = getNS<N, U, T>(name, actionType);
actions[actionType] = createAction(namespacedActionType);
const mappedReducer = (reducer as unknown) as Reducer<S, T, P>;
namedspacedReducers[namespacedActionType] = mappedReducer;
if (mappedActionTypes[actionType]) {
for (const mappedActionType of mappedActionTypes[actionType]) {
namedspacedReducers[mappedActionType] = mappedReducer;
}
}
mappedActionTypes[namespacedActionType] = actionType;
}

const reducer: Reducer<S, T, P> = createReducer(
initialState,
namedspacedReducers,
);
const actionReducer = createReducer<S, P, U>(initialState, reducers);
const isMappedActionType = (a?: Action<string, P>): a is Action<T, P> =>
a !== undefined &&
Object.prototype.hasOwnProperty.call(mappedActionTypes, a.type);
function reducer(state: S, action: Action<T, P> | Action<U, P>): S {
return actionReducer(state, {
...action,
type: isMappedActionType(action)
? mappedActionTypes[action.type]
: action.type,
});
}

return {
actions,
Expand Down
22 changes: 10 additions & 12 deletions src/createReducer.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
export function createReducer<
S,
P,
T extends string /* All action types the final reducer supports */,
K extends ActionReducerMapping<
T extends string /* All action types the final reducer supports */
>(
initialState: S,
actionTypeToReducer: ActionReducerMapping<
S,
T,
P
> /* Action types to reducer mapping */
>(
initialState: S,
actionTypeToReducer: K,
> /* Action types to reducer mapping */,
defaultReducer?: Reducer<S, T, P>,
): Reducer<S, T, P> {
const isReducerActionType = (a: Action<string, P>): boolean =>
Boolean(
Object.prototype.hasOwnProperty.call(actionTypeToReducer, a.type),
);
const isReducerActionType = (a?: Action<string, P>): a is Action<T, P> =>
a !== undefined &&
Object.prototype.hasOwnProperty.call(actionTypeToReducer, a.type);

return (state = initialState, action?): S => {
if (action === undefined || !isReducerActionType(action)) {
return function actionReducer(state = initialState, action?): S {
if (!isReducerActionType(action)) {
return defaultReducer?.(state, action) ?? state;
}
return actionTypeToReducer[action.type](state, action);
Expand Down
9 changes: 6 additions & 3 deletions src/hooks/useGetter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import * as React from "react";

export function useGetter<R>(value: R): () => R {
const ref = React.useRef(value);
React.useEffect(() => {
ref.current = value;
}, [value]);
React.useEffect(
function updateRef() {
ref.current = value;
},
[value],
);
return React.useCallback(
function getValue() {
return ref.current;
Expand Down
5 changes: 3 additions & 2 deletions src/utils/applyMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ export function applyMiddleware<S, T extends string, P>(
return function enhancer(
context: ContextValue<S, T, P>,
): ContextValue<S, T, P> {
const dispatchStub: ContextDispatch<T, P> = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function dispatchStub(...args: unknown[]): never {
throw new Error(
"Dispatching while constructing your middleware is not allowed. " +
"Other middleware would not be applied to this dispatch.",
);
};
}

const middlewareAPI: MiddlewareAPI<S, T, P> = {
getState: context.getState,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/combineReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function combineReducers<S, N extends string, T extends string, P>(
state: Record<N, S> = initialState,
action,
): Record<string, S> {
return reducers.reduce((acc, [name, reducer]) => {
return reducers.reduce(function reduce(acc, [name, reducer]) {
acc[name] = reducer(state[name], action);
return acc;
}, {} as Record<N, S>);
Expand Down
24 changes: 20 additions & 4 deletions tests/__mocks__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,38 @@ export function createMocks(): {
increment: jest.MockedFunction<(s: number) => number>;
init: jest.MockedFunction<() => boolean>;
} {
const increment = jest.fn((state): number => state + 1);
const DECREMENT = "decrement";
const INCREMENT = "increment";
const decrement = (state: number): number => state - 1;
const increment = jest.fn((state): number => state + 1);
const counterReducer: (s: number, a?: Action<string>) => number = (
state,
action,
) => {
switch (action?.type) {
case DECREMENT:
return decrement(state);
case INCREMENT:
return increment(state);
default:
return state;
}
};
const counterDuck = createDuck({
name: "counter",
initialState: 0,
reducers: { decrement, increment },
reducers: { [DECREMENT]: counterReducer, [INCREMENT]: counterReducer },
selectors: { get: (state): number => state },
});

const INIT = "init";
const init = jest.fn((): boolean => true);
const initDuck = createDuck({
name: "init",
initialState: false,
reducers: { init },
reducers: { [INIT]: init },
selectors: { get: (state): boolean => state },
actionMapping: { [ActionTypes.INIT]: "init" },
actionMapping: { [ActionTypes.INIT]: INIT },
});

const rootDuck = createRootDuck(counterDuck, initDuck);
Expand Down
4 changes: 2 additions & 2 deletions tests/createReducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe("createReducer", () => {
const initialState = { yes: false };
const reducer = createReducer(initialState, {
[actionType]: (state) => ({ ...state, yes: true }),
});
} as Record<string, Reducer<typeof initialState, string>>);
const state1 = reducer(initialState, unknownAction);
expect(state1).toEqual(initialState);

Expand All @@ -25,7 +25,7 @@ describe("createReducer", () => {
initialState,
{
[actionType]: (state) => ({ ...state, yes: "something" }),
},
} as Record<string, Reducer<typeof initialState, string>>,
(state) => ({ ...state, yes: "nani!!!" }),
);
const state1 = reducer(initialState, unknownAction);
Expand Down
5 changes: 3 additions & 2 deletions typings/duck.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ type Reducer<
type ActionReducerMapping<
S = unknown,
T extends string = string,
P = unknown
> = Record<T, Reducer<S, T, P>>;
P = unknown,
C extends string = T
> = Record<C, Reducer<S, T, P>>;

type Selector<
S = unknown,
Expand Down

0 comments on commit b76cd0c

Please sign in to comment.