();
12 | */
13 | export declare class Action extends Subject
{
14 | }
15 | /**
16 | * Creates an Observable of state which will emit everytime the state is mutated.
17 | * @param stateMutators
18 | * @param initialState
19 | */
20 | export declare function createState(stateMutators: Observable>, initialState: S): Observable;
21 | export declare function createStateMutators(...observables: Observable>[]): Observable>;
22 | export declare type Reducer = (state: S, actionPayload: P) => S;
23 | export declare type GenericReducer = (payload: P) => StateMutation;
24 | export declare class Store {
25 | state: Observable;
26 | stateMutators: Subject>;
27 | constructor(initialState: S);
28 | addReducer(action: Observable
, reducer: Reducer): Subscription;
29 | select(fn: (state: S) => T): Observable;
30 | }
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UPDATE: I have created a standalone and more advanced project on GitHub with ready-to-use code on GitHub: . All further development happens there. Please use this repo in the future. This existing repo is kept for educational purposes in the meantime.
2 |
3 | Redux pattern implemented in RxJS
4 | =====
5 |
6 | This is a sample project / tutorial that demonstrates how to implement the Redux pattern (i.e. for usage with React) in TypeScript using only [RxJS v5](https://github.com/ReactiveX/rxjs) inspired by the blog post by [Michael Zalecki](http://michalzalecki.com/use-rxjs-with-react/). The code is very simple and can be used in production (you only need to include the `rxjs-redux.ts` file which has less than 20 lines of code).
7 |
8 | Advantages
9 | -----
10 |
11 | * __Typed actions by default__: No need for [FSA](https://github.com/acdlite/flux-standard-action) or magic string constants; all actions are implemented using the `.next()` and `.error()` methods on typed `Rx.Subject`s.
12 | * No need for endless `switch` statements
13 | * Almost __[no boilerplate code](https://github.com/Dynalon/redux-pattern-with-rx/blob/master/src/rxjs-redux.ts)__, no dependencies other than RxJS using only the `map`, `merge`, `scan` and `publishReplay` operators.
14 |
15 | How to build
16 | ----
17 |
18 | 1. Clone this repo
19 | 1. `npm install`
20 | 1. `npm run build`
21 | 1. `npm run serve` to start a http-server in `dist/` for testing
22 |
23 | Production use
24 | ----
25 |
26 | You can use this code in production, all you need is to copy the file [rxjs-redux.ts](https://github.com/Dynalon/redux-pattern-with-rx/blob/master/src/rxjs-redux.ts) into your project (or use the compiled .js alternative if you use plain JavaScript).
27 |
28 | Notes
29 | ----
30 |
31 | * Webpack is used as a module bundler, but the pattern will also work using other module bundlers or typescript module outputs.
32 |
--------------------------------------------------------------------------------
/dist/main.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
--------------------------------------------------------------------------------
/src/rxjs-redux.ts:
--------------------------------------------------------------------------------
1 | import { Observable, Subject, Subscription } from "rxjs/Rx";
2 | import "rxjs/add/observable/merge";
3 | import "rxjs/add/operator/scan";
4 | import "rxjs/add/operator/publishReplay";
5 |
6 | // THIS FILE CONTAINS THE BOILERPLATE CODE YOU NEED TO COPY/REFERENCE
7 |
8 | /// STATIC INTERFACES - WIRE UP ACTIONS/STATEMUTATORS (~REDUCERS) STATICALLY (=REDUX STYLE)
9 | export type StateMutation = (state: S) => S;
10 |
11 | /**
12 | * Helper type so that you can create more expressive action types (which are just subjects)
13 | * @example
14 | * var incrementAction = new Action();
15 | * var addAction = new Action();
16 | * var genericAction = new Action();
17 | */
18 | export class Action extends Subject
{ };
19 |
20 |
21 | /**
22 | * Creates an Observable of state which will emit everytime the state is mutated.
23 | * @param stateMutators
24 | * @param initialState
25 | */
26 | export function createState(stateMutators: Observable>, initialState: S): Observable {
27 | return stateMutators
28 | .scan((state: S, reducer: StateMutation) => reducer(state), initialState)
29 |
30 | // these two lines make our observable hot and have it emit the last state
31 | // upon subscription
32 | .publishReplay(1)
33 | .refCount();
34 | }
35 |
36 | export function createStateMutators(...observables: Observable>[]): Observable> {
37 | return Observable.merge.apply(Observable, observables);
38 | }
39 |
40 | /// DYNAMIC INTERFACE - ABLE TO WIRE UP ACTIONS/REDUCERS AT DYNAMICALLY AT RUNTIME
41 |
42 | export type Reducer = (state: S, actionPayload: P) => S;
43 | export type GenericReducer = (payload: P) => StateMutation;
44 |
45 | export class Store {
46 | state: Observable;
47 | stateMutators = new Subject>();
48 |
49 | constructor(initialState: S) {
50 | this.state = createState(this.stateMutators, initialState);
51 | }
52 |
53 | addReducer(action: Observable
, reducer: Reducer): Subscription {
54 | const combinedReducer: GenericReducer = (payload) => (state) => reducer(state, payload);
55 | return action.map(combinedReducer).subscribe(cr => this.stateMutators.next(cr));
56 | }
57 |
58 | select(fn: (state: S) => T) {
59 | return this.state.map(fn);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/dist/rxjs-redux.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __extends = (this && this.__extends) || (function () {
3 | var extendStatics = Object.setPrototypeOf ||
4 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
5 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
6 | return function (d, b) {
7 | extendStatics(d, b);
8 | function __() { this.constructor = d; }
9 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
10 | };
11 | })();
12 | Object.defineProperty(exports, "__esModule", { value: true });
13 | var Rx_1 = require("rxjs/Rx");
14 | require("rxjs/add/observable/merge");
15 | require("rxjs/add/operator/scan");
16 | require("rxjs/add/operator/publishReplay");
17 | /**
18 | * Helper type so that you can create more expressive action types (which are just subjects)
19 | * @example
20 | * var incrementAction = new Action();
21 | * var addAction = new Action();
22 | * var genericAction = new Action();
23 | */
24 | var Action = (function (_super) {
25 | __extends(Action, _super);
26 | function Action() {
27 | return _super !== null && _super.apply(this, arguments) || this;
28 | }
29 | return Action;
30 | }(Rx_1.Subject));
31 | exports.Action = Action;
32 | ;
33 | /**
34 | * Creates an Observable of state which will emit everytime the state is mutated.
35 | * @param stateMutators
36 | * @param initialState
37 | */
38 | function createState(stateMutators, initialState) {
39 | return stateMutators
40 | .scan(function (state, reducer) { return reducer(state); }, initialState)
41 | .publishReplay(1)
42 | .refCount();
43 | }
44 | exports.createState = createState;
45 | function createStateMutators() {
46 | var observables = [];
47 | for (var _i = 0; _i < arguments.length; _i++) {
48 | observables[_i] = arguments[_i];
49 | }
50 | return Rx_1.Observable.merge.apply(Rx_1.Observable, observables);
51 | }
52 | exports.createStateMutators = createStateMutators;
53 | var Store = (function () {
54 | function Store(initialState) {
55 | this.stateMutators = new Rx_1.Subject();
56 | this.state = createState(this.stateMutators, initialState);
57 | }
58 | Store.prototype.addReducer = function (action, reducer) {
59 | var _this = this;
60 | var combinedReducer = function (payload) { return function (state) { return reducer(state, payload); }; };
61 | return action.map(combinedReducer).subscribe(function (cr) { return _this.stateMutators.next(cr); });
62 | };
63 | Store.prototype.select = function (fn) {
64 | return this.state.map(fn);
65 | };
66 | return Store;
67 | }());
68 | exports.Store = Store;
69 | //# sourceMappingURL=rxjs-redux.js.map
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from "rxjs/Observable";
2 |
3 | // imports for static example (Redux like)
4 | import { createState, createStateMutators, StateMutation } from "./rxjs-redux";
5 | import { counterActions, counterMutator, CounterState } from "./counter";
6 | import { nameActions, nameMutator, NameState } from "./appName";
7 |
8 | // imports for dynamic example
9 | import { Action, Reducer, Store } from "./rxjs-redux";
10 |
11 | // our application state as a strongly typed interface
12 | interface IAppState extends CounterState, NameState /* otherState1, otherState2,...*/ { }
13 |
14 | /// --- STATIC EXAMPLE
15 |
16 | // best practice is to use a plain object as State instance to allow serialization etc.
17 | const initialState: IAppState = {
18 | counter: 0,
19 | appName: "Initial Name"
20 | };
21 |
22 | // put together all reducers just as with createReducer in Redux
23 | const stateMutators = createStateMutators(
24 | counterMutator,
25 | nameMutator
26 | /* ...
27 | myOtherReducer1,
28 | myOtherReducer2
29 | */
30 | );
31 |
32 | // the state observable replaces the store known from Redux
33 | const state: Observable = createState(stateMutators, initialState);
34 |
35 | // helper function to print state changes
36 | function printStateChange(newState) {
37 | const stateJson = document.createTextNode(JSON.stringify(newState));
38 | document.querySelector("body").appendChild(stateJson);
39 | // add a line break after each state update
40 | const breakLine = document.createElement("br");
41 | document.querySelector("body").appendChild(breakLine);
42 | }
43 | // output every state change
44 | state.subscribe(printStateChange);
45 |
46 | // dispatch some actions
47 | counterActions.increment.next();
48 | counterActions.increment.next();
49 | nameActions.appName.next("Foo");
50 | counterActions.decrement.next(5);
51 | counterActions.increment.next(8);
52 | nameActions.appName.next("Bar");
53 |
54 |
55 | /// --- DYNAMIC EXAMPLE USING AN EXPLICIT STORE
56 | const store = new Store(initialState);
57 |
58 | const incrementAction = new Action();
59 | const decrementAction = new Action();
60 |
61 | const incrementReducer: Reducer = (state, n = 1) => ({ ...state, counter: state.counter + n });
62 | const decrementReducer = (state: IAppState, n: number = 1) => ({ ...state, counter: state.counter - n });
63 |
64 | store.select(s => s.counter).subscribe(printStateChange);
65 |
66 | const incSubscription = store.addReducer(incrementAction, incrementReducer);
67 |
68 | incrementAction.next(1);
69 | incrementAction.next(1);
70 |
71 | // add a new reducer during runtime
72 | const decSubscription = store.addReducer(decrementAction, decrementReducer);
73 | decrementAction.next(1);
74 | decrementAction.next(1);
75 |
76 | // remove reducer during runtime to deactivate it
77 | decSubscription.unsubscribe();
78 |
79 | // these have no effect now
80 | decrementAction.next(1);
81 | decrementAction.next(1);
82 |
--------------------------------------------------------------------------------
/dist/main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __assign = (this && this.__assign) || Object.assign || function(t) {
3 | for (var s, i = 1, n = arguments.length; i < n; i++) {
4 | s = arguments[i];
5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6 | t[p] = s[p];
7 | }
8 | return t;
9 | };
10 | Object.defineProperty(exports, "__esModule", { value: true });
11 | // imports for static example (Redux like)
12 | var rxjs_redux_1 = require("./rxjs-redux");
13 | var counter_1 = require("./counter");
14 | var appName_1 = require("./appName");
15 | // imports for dynamic example
16 | var rxjs_redux_2 = require("./rxjs-redux");
17 | /// --- STATIC EXAMPLE
18 | // best practice is to use a plain object as State instance to allow serialization etc.
19 | var initialState = {
20 | counter: 0,
21 | appName: "Initial Name"
22 | };
23 | // put together all reducers just as with createReducer in Redux
24 | var stateMutators = rxjs_redux_1.createStateMutators(counter_1.counterMutator, appName_1.nameMutator
25 | /* ...
26 | myOtherReducer1,
27 | myOtherReducer2
28 | */
29 | );
30 | // the state observable replaces the store known from Redux
31 | var state = rxjs_redux_1.createState(stateMutators, initialState);
32 | // helper function to print state changes
33 | function printStateChange(newState) {
34 | var stateJson = document.createTextNode(JSON.stringify(newState));
35 | document.querySelector("body").appendChild(stateJson);
36 | // add a line break after each state update
37 | var breakLine = document.createElement("br");
38 | document.querySelector("body").appendChild(breakLine);
39 | }
40 | // output every state change
41 | state.subscribe(printStateChange);
42 | // dispatch some actions
43 | counter_1.counterActions.increment.next();
44 | counter_1.counterActions.increment.next();
45 | appName_1.nameActions.appName.next("Foo");
46 | counter_1.counterActions.decrement.next(5);
47 | counter_1.counterActions.increment.next(8);
48 | appName_1.nameActions.appName.next("Bar");
49 | /// --- DYNAMIC EXAMPLE USING AN EXPLICIT STORE
50 | var store = new rxjs_redux_2.Store(initialState);
51 | var incrementAction = new rxjs_redux_2.Action();
52 | var decrementAction = new rxjs_redux_2.Action();
53 | var incrementReducer = function (state, n) {
54 | if (n === void 0) { n = 1; }
55 | return (__assign({}, state, { counter: state.counter + n }));
56 | };
57 | var decrementReducer = function (state, n) {
58 | if (n === void 0) { n = 1; }
59 | return (__assign({}, state, { counter: state.counter - n }));
60 | };
61 | store.select(function (s) { return s.counter; }).subscribe(printStateChange);
62 | var incSubscription = store.addReducer(incrementAction, incrementReducer);
63 | incrementAction.next(1);
64 | incrementAction.next(1);
65 | // add a new reducer during runtime
66 | var decSubscription = store.addReducer(decrementAction, decrementReducer);
67 | decrementAction.next(1);
68 | decrementAction.next(1);
69 | // remove reducer during runtime to deactivate it
70 | decSubscription.unsubscribe();
71 | // these have no effect now
72 | decrementAction.next(1);
73 | decrementAction.next(1);
74 | //# sourceMappingURL=main.js.map
--------------------------------------------------------------------------------