From bd51e8b0f37bdc2d1279904cec7f8ac63e4c9cba Mon Sep 17 00:00:00 2001 From: Sindre Seppola Date: Tue, 6 Dec 2022 14:27:08 +0100 Subject: [PATCH 1/4] feat: deprecate reducer in favor of actionReducer and streamReducer APIs --- src/reducer.ts | 90 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 19 deletions(-) diff --git a/src/reducer.ts b/src/reducer.ts index 36e0924b..fdaee3c0 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -61,9 +61,7 @@ const isStreamReducer = ( ): reducerFn is RegisteredStreamReducer => 'source$' in reducerFn.trigger; -type ObservableLike = Observable | InteropObservable; - -type ReducerCreator = { +type StreamReducerCreator = { /** * Define a reducer for a stream * @@ -76,10 +74,12 @@ type ReducerCreator = { * called directly as if it was the `reducer` parameter itself. */ ( - source$: ObservableLike, + source$: ObservableInput, reducer: Reducer ): RegisteredReducer; +}; +type ActionReducerCreator = { /** * Define a reducer for multiple actions with overlapping payload * @@ -161,28 +161,80 @@ type ReducerCreator = { * @returns A registered reducer that can be passed into `combineReducers`, or * called directly as if it was the `reducer` parameter itself. */ - ( - actionCreator: UnknownActionCreator, - reducer: Reducer - ): RegisteredReducer; + ( + actionCreator: UnknownActionCreator | UnknownActionCreator[], + reducer: Reducer + ): RegisteredReducer; }; -export const reducer: ReducerCreator = ( - trigger: UnknownActionCreator | UnknownActionCreator[] | ObservableInput, - reducerFn: Reducer +/** + * streamReducer + * A stream reducer is a stream operator which updates the state of a given stream with the last + * emitted state of another stream, it basically reduces the state of a given stream over another + * stream. + * + * @param trigger 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: StreamReducerCreator = ( + trigger: ObservableInput, + reducerFn: Reducer ) => { - const wrapper = (state: State, payload: any, namespace?: string) => + const wrapper = (state: State, payload: Payload, namespace?: string) => reducerFn(state, payload, namespace); + + wrapper.trigger = { + source$: from(trigger), + }; + + return wrapper; +}; + +/** + * actionReducer + * A action reducer is a stream operator which allows to update the state of a given stream based + * on a given action with the payload of that action. + * + * @param trigger One or multiple actions that should trigger 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: ActionReducerCreator = ( + trigger: UnknownActionCreator | UnknownActionCreator[], + reducerFn: Reducer +) => { + const wrapper = (state: State, payload: Payload, namespace?: string) => + reducerFn(state, payload, namespace); + + wrapper.trigger = { + actions: wrapInArray(trigger), + }; + + return wrapper; +}; + +/** + * @deprecated since version 2.4.0 + * Use actionReducer or streamReducer instead + */ +export const reducer: StreamReducerCreator & ActionReducerCreator = < + State, + Payload = any +>( + trigger: + | UnknownActionCreator + | UnknownActionCreator[] + | ObservableInput, + reducerFn: Reducer +) => { if (!Array.isArray(trigger) && isObservableInput(trigger)) { - wrapper.trigger = { - source$: from>(trigger), - }; + return streamReducer(trigger, reducerFn); } else { - wrapper.trigger = { - actions: wrapInArray(trigger), - }; + return actionReducer(trigger, reducerFn); } - return wrapper; }; const ACTION_ORIGIN = Symbol('Action origin'); From f8c1a59c8215ae6e0a90b5495ec8e59a68df07cc Mon Sep 17 00:00:00 2001 From: Sindre Seppola Date: Wed, 7 Dec 2022 09:24:42 +0100 Subject: [PATCH 2/4] fix: renaming based on PR feedback --- src/reducer.ts | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/reducer.ts b/src/reducer.ts index fdaee3c0..f4a1d9d1 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -173,24 +173,27 @@ type ActionReducerCreator = { * emitted state of another stream, it basically reduces the state of a given stream over another * stream. * - * @param trigger The observable that the reducer function should be subscribed to, which act as + * @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: StreamReducerCreator = ( - trigger: ObservableInput, - reducerFn: Reducer +export const streamReducer: StreamReducerCreator = ( + source: ObservableInput, + reducerFn: Reducer ) => { - const wrapper = (state: State, payload: Payload, namespace?: string) => - reducerFn(state, payload, namespace); + const wrappedStreamReducer = ( + state: State, + emittedState: EmittedState, + namespace?: string + ) => reducerFn(state, emittedState, namespace); - wrapper.trigger = { - source$: from(trigger), + wrappedStreamReducer.trigger = { + source$: from(source), }; - return wrapper; + return wrappedStreamReducer; }; /** @@ -198,22 +201,25 @@ export const streamReducer: StreamReducerCreator = ( * A action reducer is a stream operator which allows to update the state of a given stream based * on a given action with the payload of that action. * - * @param trigger One or multiple actions that should trigger the reducer + * @param actionCreator One or multiple actions 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: ActionReducerCreator = ( - trigger: UnknownActionCreator | UnknownActionCreator[], - reducerFn: Reducer +export const actionReducer: ActionReducerCreator = ( + actionCreator: UnknownActionCreator | UnknownActionCreator[], + reducerFn: Reducer ) => { - const wrapper = (state: State, payload: Payload, namespace?: string) => - reducerFn(state, payload, namespace); + const wrappedActionReducer = ( + state: State, + payload: ActionPayload, + namespace?: string + ) => reducerFn(state, payload, namespace); - wrapper.trigger = { - actions: wrapInArray(trigger), + wrappedActionReducer.trigger = { + actions: wrapInArray(actionCreator), }; - return wrapper; + return wrappedActionReducer; }; /** From 9531ca0f3978a4770696e7abba3b0e885c80e1e0 Mon Sep 17 00:00:00 2001 From: Sindre Seppola Date: Thu, 18 May 2023 12:30:57 +0200 Subject: [PATCH 3/4] fix: update with changes from v2.5.0 release --- src/index.ts | 2 + src/internal/types.ts | 6 ++ src/reducer.spec.ts | 2 +- src/reducer.ts | 203 ++++++++++++------------------------------ 4 files changed, 68 insertions(+), 145 deletions(-) diff --git a/src/index.ts b/src/index.ts index c1167d21..99f3a578 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,8 @@ export { export { reducer, + actionReducer, + streamReducer, combineReducers, Reducer, RegisteredReducer, diff --git a/src/internal/types.ts b/src/internal/types.ts index c65ea66d..078ec62c 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -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 extends ActionCreatorCommon { @@ -34,6 +37,9 @@ export interface UnknownActionCreatorWithPayload * 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; diff --git a/src/reducer.spec.ts b/src/reducer.spec.ts index d3b8a666..f149b33b 100644 --- a/src/reducer.spec.ts +++ b/src/reducer.spec.ts @@ -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( [ actionCreators.incrementOne, actionCreators.incrementMany, diff --git a/src/reducer.ts b/src/reducer.ts index f4a1d9d1..786e7dfa 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -1,5 +1,4 @@ import { - InteropObservable, Observable, ObservableInput, OperatorFunction, @@ -8,12 +7,8 @@ import { pipe, } from 'rxjs'; import { filter, map, mergeWith, scan } from 'rxjs/operators'; -import { - UnknownAction, - UnknownActionCreator, - UnknownActionCreatorWithPayload, - VoidPayload, -} from './internal/types'; +import type { UnknownAction, VoidPayload } from './internal/types'; +import type { ActionCreator } from './types/ActionCreator'; import { defaultErrorSubject } from './internal/defaultErrorSubject'; import { ofType } from './operators/operators'; import { isObservableInput } from './isObservableInput'; @@ -29,7 +24,7 @@ export type Reducer = ( type RegisteredActionReducer = Reducer & { trigger: { - actions: UnknownActionCreator[]; + actions: ActionCreator[]; }; }; type RegisteredStreamReducer = Reducer & { @@ -43,12 +38,8 @@ export type RegisteredReducer = Reducer< Payload > & { trigger: - | { - actions: UnknownActionCreator[]; - } - | { - source$: Observable; - }; + | { actions: ActionCreator[] } + | { source$: Observable }; }; const isActionReducer = ( @@ -61,128 +52,44 @@ const isStreamReducer = ( ): reducerFn is RegisteredStreamReducer => 'source$' in reducerFn.trigger; -type StreamReducerCreator = { - /** - * Define a reducer for a stream - * - * @see combineReducers - * @param source$ The stream which will trigger this reducer - * @param reducer The reducer function - * @template `State` - The state the reducer reduces to - * @template `Payload` - The type of values `source$` emits - * @returns A registered reducer that can be passed into `combineReducers`, or - * called directly as if it was the `reducer` parameter itself. - */ - ( - source$: ObservableInput, - reducer: Reducer - ): RegisteredReducer; -}; - -type ActionReducerCreator = { - /** - * Define a reducer for multiple actions with overlapping payload - * - * @see combineReducers - * @param actionCreator The action creator to assign this reducer to and - * extract payload type from - * @param reducer The reducer function - * @template `State` - The state the reducer reduces to - * @template `Payload` - The payload of the action, fed to the reducer together - * with the state. Should be automatically extracted from - * the `actionCreator` parameter - * @returns A registered reducer that can be passed into `combineReducers`, or - * called directly as if it was the `reducer` parameter itself. - */ - ( - actionCreator: UnknownActionCreatorWithPayload[], - reducer: Reducer - ): RegisteredReducer; - - /** - * Define a reducer for multiple actions without overlapping payload - * - * @see combineReducers - * @param actionCreator The action creator to assign this reducer to and - * extract payload type from - * @param reducer The reducer function - * @template `State` - The state the reducer reduces to - * @returns A registered reducer that can be passed into `combineReducers`, or - * called directly as if it was the `reducer` parameter itself. - */ - ( - actionCreator: UnknownActionCreatorWithPayload[], - reducer: Reducer - ): RegisteredReducer; - - /** - * Define a reducer for multiple actions without payloads - * - * @see combineReducers - * @param actionCreator The action creator to assign this reducer to and - * extract payload type from - * @param reducer The reducer function - * @template `State` - The state the reducer reduces to - * @returns A registered reducer that can be passed into `combineReducers`, or - * called directly as if it was the `reducer` parameter itself. - */ - ( - actionCreator: UnknownActionCreator[], - reducer: Reducer - ): RegisteredReducer; - - /** - * Define a reducer for an action with payload - * - * @see combineReducers - * @param actionCreator The action creator to assign this reducer to and - * extract payload type from - * @param reducer The reducer function - * @template `State` - The state the reducer reduces to - * @template `Payload` - The payload of the action, fed to the reducer together - * with the state. Should be automatically extracted from - * the `actionCreator` parameter - * @returns A registered reducer that can be passed into `combineReducers`, or - * called directly as if it was the `reducer` parameter itself. - */ - ( - actionCreator: UnknownActionCreatorWithPayload, - reducer: Reducer - ): RegisteredReducer; - - /** - * Define a reducer for an action without payload - * - * @see combineReducers - * @param actionCreator The action creator to assign this reducer to and - * extract payload type from - * @param reducer The reducer function - * @template `State` - The state the reducer reduces to - * @returns A registered reducer that can be passed into `combineReducers`, or - * called directly as if it was the `reducer` parameter itself. - */ - ( - actionCreator: UnknownActionCreator | UnknownActionCreator[], - reducer: Reducer - ): RegisteredReducer; -}; - /** - * streamReducer * A stream reducer is a stream operator which updates the state of a given stream with the last - * emitted state of another stream, it basically reduces the state of a given stream over another + * 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: StreamReducerCreator = ( +export const streamReducer = ( source: ObservableInput, reducerFn: Reducer -) => { +): RegisteredReducer => { const wrappedStreamReducer = ( state: State, emittedState: EmittedState, @@ -198,49 +105,57 @@ export const streamReducer: StreamReducerCreator = ( /** * actionReducer - * A action reducer is a stream operator which allows to update the state of a given stream based - * on a given action with the payload of that action. + * A action reducer is a stream operator which triggers its reducer when a + * relevant action is dispatched to the action$ * - * @param actionCreator One or multiple actions that should run the reducer + * @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: ActionReducerCreator = ( - actionCreator: UnknownActionCreator | UnknownActionCreator[], - reducerFn: Reducer -) => { +export const actionReducer = ( + actionCreator: ActionCreator, + reducerFn: Reducer +): RegisteredReducer => { const wrappedActionReducer = ( state: State, - payload: ActionPayload, + payload: Payload, namespace?: string ) => reducerFn(state, payload, namespace); wrappedActionReducer.trigger = { - actions: wrapInArray(actionCreator), + actions: [actionCreator], }; return wrappedActionReducer; }; /** - * @deprecated since version 2.4.0 - * Use actionReducer or streamReducer instead + * @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: StreamReducerCreator & ActionReducerCreator = < - State, - Payload = any ->( +export const reducer = ( trigger: - | UnknownActionCreator - | UnknownActionCreator[] + | ActionCreator + | ActionCreator[] | ObservableInput, reducerFn: Reducer -) => { +): RegisteredReducer => { if (!Array.isArray(trigger) && isObservableInput(trigger)) { return streamReducer(trigger, reducerFn); - } else { - return actionReducer(trigger, reducerFn); } + + const wrappedActionReducer = ( + state: State, + payload: Payload, + namespace?: string + ) => reducerFn(state, payload, namespace); + + wrappedActionReducer.trigger = { + actions: wrapInArray(trigger), + }; + + return wrappedActionReducer; }; const ACTION_ORIGIN = Symbol('Action origin'); From 30932066a720a3eacd842fda9bc097ca6a942ebb Mon Sep 17 00:00:00 2001 From: Sindre Seppola Date: Thu, 18 May 2023 13:45:37 +0200 Subject: [PATCH 4/4] chore: cleanup --- src/internal/testing/mock.ts | 20 +++-- src/reducer.ts | 155 ++++++++++++++++++++++++++++++----- 2 files changed, 148 insertions(+), 27 deletions(-) diff --git a/src/internal/testing/mock.ts b/src/internal/testing/mock.ts index 50321ad4..59f65627 100644 --- a/src/internal/testing/mock.ts +++ b/src/internal/testing/mock.ts @@ -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'); @@ -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( incrementMany, (accumulator: number, increment) => accumulator + increment ); -const handleDecrementWithError = reducer(decrement, throwErrorFn); + +const handleDecrementWithError = actionReducer(decrement, throwErrorFn); export const incrementMocks = { error: ERROR, @@ -32,7 +37,12 @@ export const incrementMocks = { handlers: { incrementOne: incrementOneHandler, }, - reducers: { handleOne, handleMany, handleDecrementWithError }, + reducers: { + handleOne, + handleMany, + handleManyExplicitTypes, + handleDecrementWithError, + }, marbles: { errors: { e: ERROR, diff --git a/src/reducer.ts b/src/reducer.ts index 786e7dfa..aa41a781 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -1,4 +1,5 @@ import { + InteropObservable, Observable, ObservableInput, OperatorFunction, @@ -7,7 +8,12 @@ import { pipe, } from 'rxjs'; import { filter, map, mergeWith, scan } from 'rxjs/operators'; -import type { UnknownAction, VoidPayload } from './internal/types'; +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'; @@ -104,10 +110,15 @@ export const streamReducer = ( }; /** - * actionReducer * 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. @@ -129,33 +140,133 @@ export const actionReducer = ( return wrappedActionReducer; }; +type DeprecatedReducerCreator = { + /** + * Define a reducer for a stream + * + * @see combineReducers + * @param source$ The stream which will trigger this reducer + * @param reducer The reducer function + * @template `State` - The state the reducer reduces to + * @template `Payload` - The type of values `source$` emits + * @returns A registered reducer that can be passed into `combineReducers`, or + * called directly as if it was the `reducer` parameter itself. + */ + ( + source$: Observable | InteropObservable, + reducer: Reducer + ): RegisteredReducer; + + /** + * Define a reducer for multiple actions with overlapping payload + * + * @see combineReducers + * @param actionCreator The action creator to assign this reducer to and + * extract payload type from + * @param reducer The reducer function + * @template `State` - The state the reducer reduces to + * @template `Payload` - The payload of the action, fed to the reducer together + * with the state. Should be automatically extracted from + * the `actionCreator` parameter + * @returns A registered reducer that can be passed into `combineReducers`, or + * called directly as if it was the `reducer` parameter itself. + */ + + ( + actionCreator: UnknownActionCreatorWithPayload[], + reducer: Reducer + ): RegisteredReducer; + + /** + * Define a reducer for multiple actions without overlapping payload + * + * @see combineReducers + * @param actionCreator The action creator to assign this reducer to and + * extract payload type from + * @param reducer The reducer function + * @template `State` - The state the reducer reduces to + * @returns A registered reducer that can be passed into `combineReducers`, or + * called directly as if it was the `reducer` parameter itself. + */ + ( + actionCreator: UnknownActionCreatorWithPayload[], + reducer: Reducer + ): RegisteredReducer; + + /** + * Define a reducer for multiple actions without payloads + * + * @see combineReducers + * @param actionCreator The action creator to assign this reducer to and + * extract payload type from + * @param reducer The reducer function + * @template `State` - The state the reducer reduces to + * @returns A registered reducer that can be passed into `combineReducers`, or + * called directly as if it was the `reducer` parameter itself. + */ + ( + actionCreator: UnknownActionCreator[], + reducer: Reducer + ): RegisteredReducer; + + /** + * Define a reducer for an action with payload + * + * @see combineReducers + * @param actionCreator The action creator to assign this reducer to and + * extract payload type from + * @param reducer The reducer function + * @template `State` - The state the reducer reduces to + * @template `Payload` - The payload of the action, fed to the reducer together + * with the state. Should be automatically extracted from + * the `actionCreator` parameter + * @returns A registered reducer that can be passed into `combineReducers`, or + * called directly as if it was the `reducer` parameter itself. + */ + ( + actionCreator: UnknownActionCreatorWithPayload, + reducer: Reducer + ): RegisteredReducer; + + /** + * Define a reducer for an action without payload + * + * @see combineReducers + * @param actionCreator The action creator to assign this reducer to and + * extract payload type from + * @param reducer The reducer function + * @template `State` - The state the reducer reduces to + * @returns A registered reducer that can be passed into `combineReducers`, or + * called directly as if it was the `reducer` parameter itself. + */ + ( + actionCreator: UnknownActionCreator, + reducer: Reducer + ): RegisteredReducer; +}; + /** * @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 = ( - trigger: - | ActionCreator - | ActionCreator[] - | ObservableInput, - reducerFn: Reducer -): RegisteredReducer => { +export const reducer: DeprecatedReducerCreator = ( + trigger: UnknownActionCreator | UnknownActionCreator[] | ObservableInput, + reducerFn: Reducer +) => { + const wrapper = (state: State, payload: any, namespace?: string) => + reducerFn(state, payload, namespace); + if (!Array.isArray(trigger) && isObservableInput(trigger)) { - return streamReducer(trigger, reducerFn); + wrapper.trigger = { + source$: from>(trigger), + }; + } else { + wrapper.trigger = { + actions: wrapInArray(trigger), + }; } - - const wrappedActionReducer = ( - state: State, - payload: Payload, - namespace?: string - ) => reducerFn(state, payload, namespace); - - wrappedActionReducer.trigger = { - actions: wrapInArray(trigger), - }; - - return wrappedActionReducer; + return wrapper; }; const ACTION_ORIGIN = Symbol('Action origin');