Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: deprecate reducer in favor of actionReducer and streamReducer APIs #558

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export {

export {
reducer,
actionReducer,
streamReducer,
combineReducers,
Reducer,
RegisteredReducer,
Expand Down
20 changes: 15 additions & 5 deletions src/internal/testing/mock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { actionCreator } from '../../actionCreator';
import { reducer } from '../../reducer';
import { actionReducer } from '../../reducer';
import { _namespaceAction } from '../../namespace';

const incrementOne = actionCreator('[increment] one');
Expand All @@ -14,12 +14,17 @@ const throwErrorFn = (): number => {
const ERROR = 'error';
const namespace = 'namespace';

const handleOne = reducer(incrementOne, incrementOneHandler);
const handleMany = reducer(
const handleOne = actionReducer(incrementOne, incrementOneHandler);
const handleMany = actionReducer(
incrementMany,
(accumulator: number, increment) => accumulator + increment
);
const handleManyExplicitTypes = actionReducer<number, number>(
incrementMany,
(accumulator: number, increment) => accumulator + increment
);
const handleDecrementWithError = reducer(decrement, throwErrorFn);

const handleDecrementWithError = actionReducer(decrement, throwErrorFn);

export const incrementMocks = {
error: ERROR,
Expand All @@ -32,7 +37,12 @@ export const incrementMocks = {
handlers: {
incrementOne: incrementOneHandler,
},
reducers: { handleOne, handleMany, handleDecrementWithError },
reducers: {
handleOne,
handleMany,
handleManyExplicitTypes,
handleDecrementWithError,
},
marbles: {
errors: {
e: ERROR,
Expand Down
6 changes: 6 additions & 0 deletions src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export interface ActionCreatorCommon {
*
* This type allows for inferring overlap of the payload type between multiple
* action creators with different payloads.
*
* @deprecated
* v2.6.0 Use ActionCreator type instead
*/
export interface UnknownActionCreatorWithPayload<Payload>
extends ActionCreatorCommon {
Expand All @@ -34,6 +37,9 @@ export interface UnknownActionCreatorWithPayload<Payload>
* This type has payload as an optional argument to the action creator function
* and has return type `UnknownAction`. It's useful when you need to define a
* generic action creator that might create actions with or without actions.
*
* @deprecated
* v2.6.0 Use ActionCreator type instead
*/
export interface UnknownActionCreator extends ActionCreatorCommon {
(payload?: any): UnknownAction;
Expand Down
2 changes: 1 addition & 1 deletion src/reducer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { incrementMocks } from './internal/testing/mock';
const { reducers, actionCreators, handlers } = incrementMocks;
const { actions, words, numbers, errors } = incrementMocks.marbles;
const reducerArray = Object.values(reducers);
const alwaysReset = reducer(
const alwaysReset = reducer<number, any>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the problem with reducers that accept multiple actionCreators

[
actionCreators.incrementOne,
actionCreators.incrementMany,
Expand Down
108 changes: 96 additions & 12 deletions src/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import {
pipe,
} from 'rxjs';
import { filter, map, mergeWith, scan } from 'rxjs/operators';
import {
import type {
UnknownAction,
UnknownActionCreator,
UnknownActionCreatorWithPayload,
VoidPayload,
} from './internal/types';
import type { ActionCreator } from './types/ActionCreator';
import { defaultErrorSubject } from './internal/defaultErrorSubject';
import { ofType } from './operators/operators';
import { isObservableInput } from './isObservableInput';
Expand All @@ -29,7 +30,7 @@ export type Reducer<State, Payload = VoidPayload> = (

type RegisteredActionReducer<State, Payload = any> = Reducer<State, Payload> & {
trigger: {
actions: UnknownActionCreator[];
actions: ActionCreator<Payload>[];
};
};
type RegisteredStreamReducer<State, Payload = any> = Reducer<State, Payload> & {
Expand All @@ -43,12 +44,8 @@ export type RegisteredReducer<State, Payload = any> = Reducer<
Payload
> & {
trigger:
| {
actions: UnknownActionCreator[];
}
| {
source$: Observable<Payload>;
};
| { actions: ActionCreator<Payload>[] }
| { source$: Observable<Payload> };
};

const isActionReducer = <State, Payload>(
Expand All @@ -61,9 +58,89 @@ const isStreamReducer = <State, Payload>(
): reducerFn is RegisteredStreamReducer<State, Payload> =>
'source$' in reducerFn.trigger;

type ObservableLike<T> = Observable<T> | InteropObservable<T>;
/**
* A stream reducer is a stream operator which updates the state of a given stream with the last
* emitted state of another stream, meaning it reduces the state of a given stream over another
* stream.
*
* Another way of looking at it is in terms of an action reducer this avoids creating a new action
* and dispatching it manually on every emit from the source observable. This treats the observable
* state as the action.
*
* ```ts
* // Listen for changes on context$
* streamReducer(
* context$,
* (state, context) => {
* // context changed!
* })
*
* // Listen for specific changes on context$
* streamReducer(
* context$.pipe(
* map(context => ({ id })),
* distinctUntilChanged()
* ),
* (state, contextId) => {
* // contextId changed!
* })
* ```
*
* @param source The observable that the reducer function should be subscribed to, which act as
* the "action" of the reducer.
* @param reducerFn The reducer function with signature:
* (prevState, observableInput) => nextState
* @returns A wrapped reducer function for use with persistedReducedStream, combineReducers etc.
*/
export const streamReducer = <State, EmittedState>(
source: ObservableInput<EmittedState>,
reducerFn: Reducer<State, EmittedState>
): RegisteredReducer<State, EmittedState> => {
const wrappedStreamReducer = (
state: State,
emittedState: EmittedState,
namespace?: string
) => reducerFn(state, emittedState, namespace);

wrappedStreamReducer.trigger = {
source$: from(source),
};

return wrappedStreamReducer;
};

/**
* A action reducer is a stream operator which triggers its reducer when a
* relevant action is dispatched to the action$
*
* Typing your reducer:
* - The _preferred_ way to type this is to let Typescript infer both State and
* Payload from your reducerFn and actionCreator respectively.
* - Alternatively you can provide both type arguments explicitly
* We do not default the payload to void in order to encourage inference.
*
* @param actionCreator The actionCreator that creates the action that should run the reducer
* @param reducerFn The reducer function with signature: (prevState, action) => newState
* @returns A wrapped reducer function for use with persistedReducedStream, combineReducers etc.
*/
export const actionReducer = <State, Payload>(
actionCreator: ActionCreator<Payload>,
reducerFn: Reducer<State, Payload>
): RegisteredReducer<State, Payload> => {
const wrappedActionReducer = (
state: State,
payload: Payload,
namespace?: string
) => reducerFn(state, payload, namespace);

wrappedActionReducer.trigger = {
actions: [actionCreator],
};

type ReducerCreator = {
return wrappedActionReducer;
};

type DeprecatedReducerCreator = {
/**
* Define a reducer for a stream
*
Expand All @@ -76,7 +153,7 @@ type ReducerCreator = {
* called directly as if it was the `reducer` parameter itself.
*/
<State, Payload>(
source$: ObservableLike<Payload>,
source$: Observable<Payload> | InteropObservable<Payload>,
reducer: Reducer<State, Payload>
): RegisteredReducer<State, Payload>;

Expand All @@ -94,6 +171,7 @@ type ReducerCreator = {
* @returns A registered reducer that can be passed into `combineReducers`, or
* called directly as if it was the `reducer` parameter itself.
*/

<State, Payload>(
actionCreator: UnknownActionCreatorWithPayload<Payload>[],
reducer: Reducer<State, Payload>
Expand Down Expand Up @@ -167,12 +245,18 @@ type ReducerCreator = {
): RegisteredReducer<State, VoidPayload>;
};

export const reducer: ReducerCreator = <State>(
sseppola marked this conversation as resolved.
Show resolved Hide resolved
/**
* @deprecated
* v2.6.0, use actionReducer or streamReducer instead.
* If using multi-action reducers you have to split them into individual reducers
*/
export const reducer: DeprecatedReducerCreator = <State>(
trigger: UnknownActionCreator | UnknownActionCreator[] | ObservableInput<any>,
reducerFn: Reducer<State, any>
) => {
const wrapper = (state: State, payload: any, namespace?: string) =>
reducerFn(state, payload, namespace);

if (!Array.isArray(trigger) && isObservableInput(trigger)) {
wrapper.trigger = {
source$: from<ObservableInput<any>>(trigger),
Expand Down