Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,7 @@ createReducer<TState, TRootAction>(initialState)
.handleAction([actionCreator1, actionCreator2, ...actionCreatorN], reducer)
.handleType(type, reducer)
.handleType([type1, type2, ...typeN], reducer)
.defaultHandler(reducer)
```

Examples:
Expand Down Expand Up @@ -805,6 +806,28 @@ const counterReducer = createReducer<State, Action>(0)
.handleType('INCREMENT', (state, _) => state + 1);
```

**Default handler:**
```ts
type State = boolean;
type Action =
| { type: 'REQUEST_START' }
| { type: 'REQUEST_SUCCESS' }
| { type: 'REQUEST_FAILURE' };

const loadingReducer = createReducer<State, Action>(false)
.handleType('REQUEST_START', () => true)
.defaultHandler(() => false);
```

The default handler runs for actions that do not have an explicit handler. It
does not run for Redux initialization actions unless you pass `true` as the
second argument:
```ts
const reducer = createReducer<State, Action>(false)
.handleType('REQUEST_START', () => true)
.defaultHandler(() => false, true);
```

**Extend or compose reducers - every operation is completely typesafe:**
```ts
const newCounterReducer = createReducer<State, Action>(0)
Expand Down
16 changes: 15 additions & 1 deletion src/__snapshots__/create-reducer.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ exports[`Type refinement checks action (type) should match snapshot 3`] = `"T.Em

exports[`Type refinement checks action (type) should match snapshot 4`] = `"T.EmptyAction<\\"foo4\\">"`;

exports[`Type refinement checks reducer.handlers (type) should match snapshot 1`] = `"Record<\\"foo1\\" | \\"foo2\\" | \\"foo3\\" | \\"foo4\\", (state: { foo: string | null; }, action: T.PayloadAction<\\"foo1\\", string> | T.PayloadAction<\\"foo2\\", string> | T.EmptyAction<\\"foo3\\"> | T.EmptyAction<\\"foo4\\">) => { foo: string | null; }>"`;
exports[`Type refinement checks action (type) should match snapshot 5`] = `"T.EmptyAction<\\"foo5\\">"`;

exports[`Type refinement checks reducer.handlers (type) should match snapshot 1`] = `"Record<\\"foo1\\" | \\"foo2\\" | \\"foo3\\" | \\"foo4\\", (state: { foo: string | null; }, action: T.PayloadAction<\\"foo1\\", string> | T.PayloadAction<\\"foo2\\", string> | T.EmptyAction<\\"foo3\\"> | T.EmptyAction<\\"foo4\\"> | T.EmptyAction<\\"foo5\\">) => { foo: string | null; }>"`;

exports[`Type refinement checks reducerDefaultResult (type) should match snapshot 1`] = `"{ foo: string | null; }"`;

exports[`Type refinement checks reducerResult (type) should match snapshot 1`] = `"{ foo: string | null; }"`;

Expand All @@ -26,6 +30,10 @@ exports[`Type refinement checks state (type) should match snapshot 3`] = `"{ foo

exports[`Type refinement checks state (type) should match snapshot 4`] = `"{ foo: string | null; }"`;

exports[`Type refinement checks state (type) should match snapshot 5`] = `"{ foo: string | null; }"`;

exports[`With Action Creators action (type) should match snapshot 1`] = `"T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">"`;

exports[`With Action Creators counterReducer1.handlers (type) should match snapshot 1`] = `"Record<\\"ADD\\" | \\"INCREMENT\\", (state: number, action: T.PayloadAction<\\"ADD\\", number> | T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">) => number>"`;

exports[`With Action Creators counterReducer2.handlers (type) should match snapshot 1`] = `"Record<\\"ADD\\" | \\"INCREMENT\\", (state: number, action: T.PayloadAction<\\"ADD\\", number> | T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">) => number>"`;
Expand All @@ -34,10 +42,16 @@ exports[`With Action Creators counterReducer3.handlers (type) should match snaps

exports[`With Action Creators counterReducer4.handlers (type) should match snapshot 1`] = `"Record<\\"INCREMENT\\", (state: number, action: T.PayloadAction<\\"ADD\\", number> | T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">) => number>"`;

exports[`With Action Creators counterReducer5.handlers (type) should match snapshot 1`] = `"Record<\\"ADD\\", (state: number, action: T.PayloadAction<\\"ADD\\", number> | T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">) => number>"`;

exports[`With Action Types action (type) should match snapshot 1`] = `"T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">"`;

exports[`With Action Types counterReducer1.handlers (type) should match snapshot 1`] = `"Record<\\"ADD\\" | \\"INCREMENT\\", (state: number, action: T.PayloadAction<\\"ADD\\", number> | T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">) => number>"`;

exports[`With Action Types counterReducer2.handlers (type) should match snapshot 1`] = `"Record<\\"ADD\\" | \\"INCREMENT\\", (state: number, action: T.PayloadAction<\\"ADD\\", number> | T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">) => number>"`;

exports[`With Action Types counterReducer3.handlers (type) should match snapshot 1`] = `"Record<\\"ADD\\", (state: number, action: T.PayloadAction<\\"ADD\\", number> | T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">) => number>"`;

exports[`With Action Types counterReducer4.handlers (type) should match snapshot 1`] = `"Record<\\"INCREMENT\\", (state: number, action: T.PayloadAction<\\"ADD\\", number> | T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">) => number>"`;

exports[`With Action Types counterReducer5.handlers (type) should match snapshot 1`] = `"Record<\\"ADD\\", (state: number, action: T.PayloadAction<\\"ADD\\", number> | T.EmptyAction<\\"INCREMENT\\"> | T.EmptyAction<\\"DECREMENT\\">) => number>"`;
81 changes: 80 additions & 1 deletion src/create-reducer.spec.snap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ const initialState = 0;
// @dts-jest:pass
Object.keys({ ...emptyReducer.handlers }); // => []

const counterReducer5 = emptyReducer
.handleAction(add, (state, action) => state + action.payload)
.defaultHandler((state, action) => {
// @dts-jest:pass:snap -> T.EmptyAction<"INCREMENT"> | T.EmptyAction<"DECREMENT">
action;

return state - 1;
});

// @dts-jest:pass:snap -> Record<"ADD", (state: number, action: T.PayloadAction<"ADD", number> | T.EmptyAction<"INCREMENT"> | T.EmptyAction<"DECREMENT">) => number>
counterReducer5.handlers;
// @dts-jest:pass
Object.keys({ ...counterReducer5.handlers }); // => [ "ADD"]
// @dts-jest:pass
Object.keys({ ...emptyReducer.handlers }); // => []

{
[
counterReducer1,
Expand All @@ -93,6 +109,8 @@ const initialState = 0;
// @dts-jest:pass
fn(0, add(4)); // => 4
});
counterReducer5(0, decrement());
counterReducer5(0, add(4));
}
}

Expand All @@ -111,6 +129,7 @@ const initialState = 0;
foo2: createAction('foo2')<string>(),
foo3: createAction('foo3')(),
foo4: createAction('foo4')(),
foo5: createAction('foo5')(),
};

type Action = ActionType<typeof actions2>;
Expand Down Expand Up @@ -159,9 +178,20 @@ const initialState = 0;
...state,
foo: 'empty',
};
})
.defaultHandler((state, action) => {
// @dts-jest:pass:snap -> { foo: string | null; }
state;
// @dts-jest:pass:snap -> T.EmptyAction<"foo5">
action;

return {
...state,
foo: 'default',
};
});

// @dts-jest:pass:snap -> Record<"foo1" | "foo2" | "foo3" | "foo4", (state: { foo: string | null; }, action: T.PayloadAction<"foo1", string> | T.PayloadAction<"foo2", string> | T.EmptyAction<"foo3"> | T.EmptyAction<"foo4">) => { foo: string | null; }>
// @dts-jest:pass:snap -> Record<"foo1" | "foo2" | "foo3" | "foo4", (state: { foo: string | null; }, action: T.PayloadAction<"foo1", string> | T.PayloadAction<"foo2", string> | T.EmptyAction<"foo3"> | T.EmptyAction<"foo4"> | T.EmptyAction<"foo5">) => { foo: string | null; }>
reducer.handlers;

{
Expand All @@ -180,6 +210,10 @@ const initialState = 0;
// @dts-jest:pass:snap -> { foo: string | null; }
reducerResult; // => { foo: "empty" }
});

const reducerDefaultResult = reducer(defaultState, actions2.foo5());
// @dts-jest:pass:snap -> { foo: string | null; }
reducerDefaultResult; // => { foo: "default" }
}
}

Expand Down Expand Up @@ -230,6 +264,21 @@ const initialState = 0;
// @dts-jest:pass
Object.keys({ ...reducerTest.handlers }); // => []

const counterReducer5 = reducerTest
.handleType('ADD', (state, action) => state + action.payload)
.defaultHandler((state, action) => {
// @dts-jest:pass:snap -> T.EmptyAction<"INCREMENT"> | T.EmptyAction<"DECREMENT">
action;

return state - 1;
});
// @dts-jest:pass:snap -> Record<"ADD", (state: number, action: T.PayloadAction<"ADD", number> | T.EmptyAction<"INCREMENT"> | T.EmptyAction<"DECREMENT">) => number>
counterReducer5.handlers;
// @dts-jest:pass
Object.keys({ ...counterReducer5.handlers }); // => [ "ADD"]
// @dts-jest:pass
Object.keys({ ...reducerTest.handlers }); // => []

{
[
counterReducer1,
Expand All @@ -254,5 +303,35 @@ const initialState = 0;
// @dts-jest:pass
fn(0, add(4)); // => 4
});
counterReducer5(0, increment());
counterReducer5(0, add(4));
}
}

describe('defaultHandler', () => {
it('handles actions without an explicit handler', () => {
const reducer = createReducer<number, ActionType<typeof actions>>(0)
.handleAction(add, (state, action) => state + action.payload)
.defaultHandler(state => state - 1);

expect(reducer(0, add(4))).toBe(4);
expect(reducer(0, increment())).toBe(-1);
expect(reducer(0, decrement())).toBe(-1);
});

it('does not run for Redux initialization actions by default', () => {
const reducer = createReducer<number, ActionType<typeof actions>>(0)
.handleAction(add, (state, action) => state + action.payload)
.defaultHandler(() => 100);

expect(reducer(undefined, { type: '@@redux/INIT.test' } as any)).toBe(0);
});

it('can opt into running for Redux initialization actions', () => {
const reducer = createReducer<number, ActionType<typeof actions>>(0)
.handleAction(add, (state, action) => state + action.payload)
.defaultHandler(() => 100, true);

expect(reducer(undefined, { type: '@@redux/INIT.test' } as any)).toBe(100);
});
});
79 changes: 79 additions & 0 deletions src/create-reducer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ const initialState = 0;
// @dts-jest:pass
Object.keys({ ...emptyReducer.handlers }); // => []

const counterReducer5 = emptyReducer
.handleAction(add, (state, action) => state + action.payload)
.defaultHandler((state, action) => {
// @dts-jest:pass:snap
action;

return state - 1;
});

// @dts-jest:pass:snap
counterReducer5.handlers;
// @dts-jest:pass
Object.keys({ ...counterReducer5.handlers }); // => [ "ADD"]
// @dts-jest:pass
Object.keys({ ...emptyReducer.handlers }); // => []

{
[
counterReducer1,
Expand All @@ -93,6 +109,8 @@ const initialState = 0;
// @dts-jest:pass
fn(0, add(4)); // => 4
});
counterReducer5(0, decrement());
counterReducer5(0, add(4));
}
}

Expand All @@ -111,6 +129,7 @@ const initialState = 0;
foo2: createAction('foo2')<string>(),
foo3: createAction('foo3')(),
foo4: createAction('foo4')(),
foo5: createAction('foo5')(),
};

type Action = ActionType<typeof actions2>;
Expand Down Expand Up @@ -159,6 +178,17 @@ const initialState = 0;
...state,
foo: 'empty',
};
})
.defaultHandler((state, action) => {
// @dts-jest:pass:snap
state;
// @dts-jest:pass:snap
action;

return {
...state,
foo: 'default',
};
});

// @dts-jest:pass:snap
Expand All @@ -180,6 +210,10 @@ const initialState = 0;
// @dts-jest:pass:snap
reducerResult; // => { foo: "empty" }
});

const reducerDefaultResult = reducer(defaultState, actions2.foo5());
// @dts-jest:pass:snap
reducerDefaultResult; // => { foo: "default" }
}
}

Expand Down Expand Up @@ -230,6 +264,21 @@ const initialState = 0;
// @dts-jest:pass
Object.keys({ ...reducerTest.handlers }); // => []

const counterReducer5 = reducerTest
.handleType('ADD', (state, action) => state + action.payload)
.defaultHandler((state, action) => {
// @dts-jest:pass:snap
action;

return state - 1;
});
// @dts-jest:pass:snap
counterReducer5.handlers;
// @dts-jest:pass
Object.keys({ ...counterReducer5.handlers }); // => [ "ADD"]
// @dts-jest:pass
Object.keys({ ...reducerTest.handlers }); // => []

{
[
counterReducer1,
Expand All @@ -254,5 +303,35 @@ const initialState = 0;
// @dts-jest:pass
fn(0, add(4)); // => 4
});
counterReducer5(0, increment());
counterReducer5(0, add(4));
}
}

describe('defaultHandler', () => {
it('handles actions without an explicit handler', () => {
const reducer = createReducer<number, ActionType<typeof actions>>(0)
.handleAction(add, (state, action) => state + action.payload)
.defaultHandler(state => state - 1);

expect(reducer(0, add(4))).toBe(4);
expect(reducer(0, increment())).toBe(-1);
expect(reducer(0, decrement())).toBe(-1);
});

it('does not run for Redux initialization actions by default', () => {
const reducer = createReducer<number, ActionType<typeof actions>>(0)
.handleAction(add, (state, action) => state + action.payload)
.defaultHandler(() => 100);

expect(reducer(undefined, { type: '@@redux/INIT.test' } as any)).toBe(0);
});

it('can opt into running for Redux initialization actions', () => {
const reducer = createReducer<number, ActionType<typeof actions>>(0)
.handleAction(add, (state, action) => state + action.payload)
.defaultHandler(() => 100, true);

expect(reducer(undefined, { type: '@@redux/INIT.test' } as any)).toBe(100);
});
});
Loading