Skip to content

Commit

Permalink
test: useDispatch hook
Browse files Browse the repository at this point in the history
  • Loading branch information
iamogbz committed Sep 15, 2020
1 parent d619f2d commit ea41eaa
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 32 deletions.
15 changes: 12 additions & 3 deletions src/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ function createUnimplemented(objectName?: string): (m: string) => () => never {
};
}

export function createContextWithValue<S, T extends string, P>(
value: Partial<ContextValue<S, T, P>>,
): Context<S, T, P> {
return React.createContext<ContextValue<S, T, P>>({
dispatch: (a) => a,
getState: () => value.state,
reducer: (s) => s,
...value,
} as ContextValue<S, T, P>);
}

export function createContext<S, T extends string, P>(
rootReducer: Reducer<S, T, P>,
preloadedState: S,
Expand All @@ -19,10 +30,8 @@ export function createContext<S, T extends string, P>(
global = false,
): Context<S, T, P> {
const unimplemented = createUnimplemented(`Context(${displayName ?? ""})`);
const Context = React.createContext<ContextValue<S, T, P>>({
dispatch: (a) => a,
const Context = createContextWithValue({
enhancer,
getState: () => preloadedState,
reducer: rootReducer,
state: preloadedState,
[SymbolObservable]: unimplemented(SymbolObservable.toString()),
Expand Down
17 changes: 11 additions & 6 deletions src/hooks/useDispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ import { bindActionCreator } from "../utils/bindActionCreators";
import { GlobalContext } from "..";

export function useDispatch<S, T extends string, P>(
actionCreator: ActionCreator<T, P>,
actionCreator: Nullable<ActionCreator<T, P>>,
Context?: Context<S, T, P>,
): (...args: P[]) => void {
const { dispatch } = React.useContext(Context ?? GlobalContext);
// eslint-disable-next-line react-hooks/exhaustive-deps
return React.useCallback(bindActionCreator(actionCreator, dispatch), [
actionCreator,
dispatch,
]);
const boundActionCreator = React.useMemo(
() => bindActionCreator(actionCreator, dispatch),
[actionCreator, dispatch],
);
if (!boundActionCreator) {
throw new Error(
`Unable to bind action creator "${actionCreator}" to disptch`,
);
}
return React.useCallback(boundActionCreator, [boundActionCreator]);
}
4 changes: 2 additions & 2 deletions src/hooks/useSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import * as React from "react";
import { GlobalContext } from "..";

export function useSelector<S, R, T extends string, P>(
selector: Selector<S, R, T, P> | undefined,
selector: Nullable<Selector<S, R, T, P>>,
Context?: Context<S, T, P>,
): R | undefined {
): Nullable<R> {
const { state } = React.useContext(Context ?? GlobalContext);
return React.useMemo(
function select() {
Expand Down
25 changes: 9 additions & 16 deletions src/utils/bindActionCreators.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// github.com/reduxjs/redux/blob/208d7f1/src/bindActionCreators.ts

export function bindActionCreator<T extends string, P>(
actionCreator: ActionCreator<T, P>,
actionCreator: Nullable<ActionCreator<T, P>>,
dispatch: ContextDispatch<T, P>,
): ActionDispatcher<T, P> {
): Nullable<ActionDispatcher<T, P>> {
if (typeof actionCreator !== "function") return;
return function dispatchAction(...args: P[]): void {
dispatch(actionCreator(...args));
};
Expand All @@ -15,23 +16,18 @@ export function bindActionCreator<T extends string, P>(
* may be invoked directly.
*/
export function bindActionCreators<T extends string, P>(
actionCreator: ActionCreator<T, P>,
dispatch: ContextDispatch<T, P>,
): ActionDispatcher<T, P>;

export function bindActionCreators<T extends string, P>(
actionCreator: ActionCreator<T, P>,
actionCreator: Nullable<ActionCreator<T, P>>,
dispatch: ContextDispatch<T, P>,
): ActionDispatcher<T, P>;

export function bindActionCreators<T extends string, P, S>(
actionCreators: ActionCreatorMapping<T, P, S>,
actionCreators: Nullable<ActionCreatorMapping<T, P, S>>,
dispatch: ContextDispatch<T, P>,
): ActionDispatcherMapping<T, P>;

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function bindActionCreators(
actionCreators: ActionCreator | ActionCreatorMapping,
actionCreators: Nullable<ActionCreator | ActionCreatorMapping>,
dispatch: ContextDispatch,
) {
if (typeof actionCreators === "function") {
Expand All @@ -49,12 +45,9 @@ export function bindActionCreators(

const boundActionCreators: ActionDispatcherMapping = {};
for (const key in actionCreators) {
const actionCreator = actionCreators[key];
if (typeof actionCreator === "function") {
boundActionCreators[key] = bindActionCreator(
actionCreator,
dispatch,
);
const bound = bindActionCreator(actionCreators[key], dispatch);
if (bound) {
boundActionCreators[key] = bound;
}
}
return boundActionCreators;
Expand Down
2 changes: 1 addition & 1 deletion src/utils/combineSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function combineSelectors<
>(
duckName: N,
selectors?: SelectorMapping<S, R, T, P, Q>,
): SelectorMapping<S, R, T, P, Q> | undefined {
): Nullable<SelectorMapping<S, R, T, P, Q>> {
if (!selectors) return;
const duckSelectors = {} as SelectorMapping<S, R, T, P, Q>;
for (const s of Object.keys(selectors) as Q[]) {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function isFunction<F>(maybeFunction: F | unknown): maybeFunction is F {

function asMapDispatchToPropsFn<S, T extends string, P, I>(
actionCreators?: ActionCreatorMapping<T, P, S>,
): MapDispatchToProps<T, P, I> | undefined {
): Nullable<MapDispatchToProps<T, P, I>> {
return (
actionCreators &&
function mapDispatchToProps(
Expand Down
44 changes: 44 additions & 0 deletions tests/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { renderHook } from "@testing-library/react-hooks";
import { useDispatch } from "src";
import { createContextWithValue } from "src/createContext";

describe("useDispatch", () => {
it("uses dispatch from Context", () => {
const dispatch = jest.fn();
const context = createContextWithValue<unknown, string, string>({
dispatch,
});
const ACTION_TYPE = "ACTION_TYPE" as const;
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const actionCreator = (payload?: string) => ({
type: ACTION_TYPE,
payload,
});
const { result } = renderHook(() =>
useDispatch(actionCreator, context),
);
const arg0 = "Some argument";
result.current(arg0);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith({
type: ACTION_TYPE,
payload: arg0,
});
});

it.each`
actionCreator
${"non function"}
${null}
${undefined}
`("fails to use dispatch with actionCreator", ({ actionCreator }) => {
const dispatch = jest.fn();
const context = createContextWithValue<unknown, string, string>({
dispatch,
});
const { result } = renderHook(() =>
useDispatch(actionCreator, context),
);
expect(() => result.current).toThrow("Unable to bind action creator");
});
});
8 changes: 7 additions & 1 deletion tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ describe("e2e", (): void => {
});
const mapDispatchToProps = {
increment: rootDuck.actions.counter.increment,
init: null,
};
const staticMergeProps = <A, B, C, D>(
stateProps: A,
Expand Down Expand Up @@ -211,7 +212,12 @@ describe("e2e", (): void => {
const MockComponent = jest.fn(DumbComponent);
const ConnectedComponent = connectGlobal(
mapStateToProps,
mapDispatchToProps,
(dispatch) => ({
increment: bindActionCreators(
rootDuck.actions.counter.increment,
dispatch,
),
}),
staticMergeProps,
{ pure },
)(MockComponent);
Expand Down
4 changes: 2 additions & 2 deletions typings/duck.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type ActionCreatorMapping<
P = unknown,
S = unknown,
C extends string = T /* Action creator mapping keys */
> = Record<C, ActionCreator<T, P, S>>;
> = Record<C, Nullable<ActionCreator<T, P, S>>>;

type ActionDispatcher<T extends string, P = unknown> = (...args: P[]) => void;

Expand Down Expand Up @@ -90,5 +90,5 @@ type RootDuck<
initialState: Record<N, S>;
names: Set<N>;
reducer: Reducer<Record<N, S>, T, P>;
selectors: Record<N, SelectorMapping<S, R, T, P, Q> | undefined>;
selectors: Record<N, Nullable<SelectorMapping<S, R, T, P, Q>>>;
};
1 change: 1 addition & 0 deletions typings/utils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type Nullable<T> = T | null | undefined;

0 comments on commit ea41eaa

Please sign in to comment.