20 | )
21 | }
22 |
23 | export default reactiveComponent(TimelineUnit, 'draggingState$');
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 soopjs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/actionStreams.js:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs/Rx';
2 | import { drag, stopDrag, startDrag } from './actions.js';
3 |
4 | const click$ = Observable.fromEvent(document, 'click');
5 | const sliderClick$ = click$.filter((e) => e.target.id === 'sliderBar');
6 | const nonsliderClick$ = click$.filter((e) => (e.target.id !== 'sliderBar'));
7 | const mouseMove$ = Observable.fromEvent(document, 'mousemove');
8 |
9 | const currentlyDragging = (omnistream) => omnistream.filter(x => x.type === 'MOUSE_LEAVE')
10 | .map(x => 'stop')
11 | .merge(sliderClick$.map(x => 'slider'))
12 | .merge(nonsliderClick$.map(x => 'nonslider'))
13 | .scan((dragging, val) => {
14 | if (val === 'slider') return !dragging;
15 | if (val === 'nonslider' || val === 'stop') return false;
16 | }, false)
17 | .map(dragging => dragging ? startDrag() : stopDrag())
18 |
19 | const dragMovement = (omnistream) => {
20 | return omnistream.filterForActionTypes('STOP_DRAG', 'START_DRAG')
21 | .switchMap((dragging) => {
22 | return dragging.type === 'STOP_DRAG' ? Observable.never() : mouseMove$
23 | })
24 | .map(e => drag(e.pageX))
25 | };
26 |
27 | export { dragMovement, currentlyDragging };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "omnistream",
3 | "version": "0.1.4",
4 | "description": "Stream based state management for React built on RxJs",
5 | "main": "./dist/",
6 | "scripts": {
7 | "test": "ava",
8 | "build": "babel src --out-dir dist",
9 | "build:pack": "babel src --out-dir dist && npm pack"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/soup-js/omnistream.git"
14 | },
15 | "author": "",
16 | "license": "MIT",
17 | "bugs": {
18 | "url": "https://github.com/soup-js/omnistream/issues"
19 | },
20 | "homepage": "https://github.com/soup-js/omnistream#readme",
21 | "dependencies": {
22 | "react": "^15.4.1",
23 | "react-dom": "^15.4.1",
24 | "rxjs": "^5.0.0-rc.4"
25 | },
26 | "devDependencies": {
27 | "ava": "^0.17.0",
28 | "babel-core": "^6.18.2",
29 | "babel-loader": "^6.2.8",
30 | "babel-preset-es2015": "^6.18.0",
31 | "babel-preset-react": "^6.16.0",
32 | "babel-tape-runner": "^2.0.1",
33 | "mocha": "^3.2.0",
34 | "tap-spec": "^4.1.1",
35 | "tape": "^4.6.3",
36 | "webpack": "^1.13.3",
37 | "webpack-dev-server": "^1.16.2"
38 | },
39 | "ava": {
40 | "babel": {
41 | "presets": [
42 | "es2015"
43 | ]
44 | },
45 | "require": ["babel-register"]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/reactiveComponent.js:
--------------------------------------------------------------------------------
1 | const Rx = require('rxjs/Rx');
2 | import React, { PureComponent } from 'react';
3 |
4 | // mapStreamsToProps creates an observable stream for each action type given in "streamNames".
5 | // This stream emits objects that will be passed down as props to the reactive components.
6 | const combineStreamsToState = (stateStreams) => {
7 | return Rx.Observable.combineLatest(...stateStreams, (...stateData) => {
8 | return stateData.reduce((accum, curr) => {
9 | return Object.assign(accum, curr)
10 | }, {})
11 | }).distinctUntilChanged(null, state => JSON.stringify(state));
12 | }
13 |
14 | //ReactiveComponent subscribes to a stream and re-renders when it receives new data.
15 | function makeReactive(componentDefinition, renderFn, ...stateStreamNames) {
16 | class ReactiveComponent extends PureComponent {
17 | constructor(props, context) {
18 | super(props, context);
19 | this.state = { childProps: {} }
20 | this.omnistream = this.context.omnistream;
21 |
22 | // Make the dispatch function accessible to be passed as a prop to child components.
23 | this.dispatch = this.omnistream.dispatch.bind(context.omnistream);
24 | this.dispatchObservableFn = this.omnistream.dispatchObservableFn.bind(context.omnistream);
25 | }
26 |
27 | componentDidMount() {
28 | // Creates a new substream for each action type based on the provided "streamNames"
29 | const stateStreams = stateStreamNames.map(name => this.omnistream.store[name]);
30 | const state$ = combineStreamsToState(stateStreams);
31 | // Subscribes to the props stream. This will trigger a re-render whenever a new action has been dispatched to
32 | // any filtered stream passed down as props to a component.
33 | this.subscription = state$.subscribe((props) => {
34 | this.setState({ childProps: Object.assign({}, this.props, props) });
35 | });
36 | }
37 |
38 | componentWillUnmount() {
39 | this.subscription.unsubscribe();
40 | }
41 |
42 | render() {
43 | return renderFn.call(this, componentDefinition);
44 | }
45 | }
46 | ReactiveComponent.contextTypes = { omnistream: React.PropTypes.object.isRequired }
47 | return ReactiveComponent;
48 | }
49 |
50 | function renderTimeline(componentDefinition) {
51 | return React.createElement(componentDefinition,
52 | Object.assign({}, this.state.childProps, {
53 | dispatch: this.dispatch,
54 | dispatchObservableFn: this.dispatchObservableFn,
55 | omnistream: this.omnistream
56 | }, this.props), null)
57 | }
58 |
59 | function renderStandard(componentDefinition) {
60 | return React.createElement(componentDefinition,
61 | Object.assign({}, this.state.childProps, {
62 | dispatch: this.dispatch,
63 | dispatchObservableFn: this.dispatchObservableFn,
64 | }, this.props), null)
65 | }
66 |
67 | export const reactiveComponent = (componentDefinition, ...stateStreamNames) =>
68 | makeReactive(componentDefinition, renderStandard, ...stateStreamNames)
69 |
70 | export const reactiveTimeline = (componentDefinition, ...stateStreamNames) =>
71 | makeReactive(componentDefinition, renderTimeline, ...stateStreamNames)
--------------------------------------------------------------------------------
/src/Timeline.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Rx from 'rxjs/Rx';
3 | import Slider from './Slider';
4 | import TimelineUnit from './TimelineUnit';
5 | import createOmnistream from './omnistream.js';
6 | import ActionViewer from './ActionViewer.js';
7 | import { reactiveTimeline } from './reactiveComponent.js';
8 | import { dragMovement, currentlyDragging } from './actionStreams.js';
9 | import { dragReducer, barPositionReducer } from './reducer.js';
10 | import { stopDrag, mouseLeave } from './actions';
11 |
12 | const STYLES = {
13 | position: 'fixed',
14 | backgroundColor: '#f4f4f4',
15 | overflowX: 'scroll',
16 | overflowY: 'hidden',
17 | whiteSpace: 'nowrap',
18 | width: '100%',
19 | height: '70px',
20 | bottom: '0px',
21 | borderTop: '1px solid #b0b0b0'
22 | }
23 |
24 | const MAIN_CONTAINER_STYLES = {
25 | fontFamily: 'monospace',
26 | position: 'fixed',
27 | display: 'flex',
28 | justifyContent: 'center',
29 | width: '100%',
30 | bottom: '0',
31 | }
32 |
33 | const UNIT_STYLES = {
34 | display: 'inline-block',
35 | zIndex: 0,
36 | height: '70px',
37 | marginTop: '-30px',
38 | borderLeft: '1px solid #909090',
39 | width: '24px',
40 | textAlign: 'center',
41 | lineHeight: '70px',
42 | marginLeft: '5px'
43 | }
44 |
45 | const CONTAINER_STYLE = {
46 | fontWeight: '200',
47 | fontSize: '.75em',
48 | position: 'relative',
49 | bottom: '40px',
50 | }
51 |
52 |
53 | const draggingStateFn = (omnistream) => {
54 | return omnistream.filterForActionTypes(['START_DRAG', 'STOP_DRAG', 'SELECT_ACTION'])
55 | }
56 |
57 | // setup OMNISTREAMS
58 | const addTimelinestore = (omnistream) => {
59 | const sliderState$ = omnistream._createTimelineStatestream(barPositionReducer, dragMovement);
60 | const draggingState$ = omnistream._createTimelineStatestream(dragReducer, draggingStateFn);
61 | omnistream.addToStore({ sliderState$, draggingState$ });
62 | }
63 |
64 |
65 | class Timeline extends Component {
66 | constructor(props) {
67 | super(props);
68 | this.omnistream = this.props.omnistream;
69 | addTimelinestore(this.omnistream);
70 | this.state = { history: [] };
71 | this.history$ = this.omnistream.history$;
72 | this.timeTravelToPointN = this.omnistream.timeTravelToPointN.bind(this.omnistream);
73 | }
74 |
75 | componentDidMount() {
76 | this.history$.subscribe((historyArray) => {
77 | this.setState({ history: historyArray });
78 | })
79 | this.props.dispatchObservableFn(currentlyDragging);
80 | this.listener = document.getElementById('timeline').addEventListener('mouseleave', (x) => {
81 | this.props.dispatch(mouseLeave());
82 | });
83 | }
84 |
85 | compomentWillUnmount() {
86 | document.getElementById('timeline').removeEventListener(this.listener);
87 | }
88 |
89 | render() {
90 | const units = this.state.history.map((node, index) => {
91 | return
92 | })
93 | return (
94 |
95 |
96 |
97 |
98 |
99 | {units}
100 |
101 |
102 |
103 | )
104 | }
105 | }
106 |
107 | export default reactiveTimeline(Timeline);
--------------------------------------------------------------------------------
/src/omnistream.js:
--------------------------------------------------------------------------------
1 | const Rx = require('rxjs/Rx');
2 |
3 | class Omnistream {
4 | // Instatiates a new stream to manage state for the application
5 | constructor() {
6 | this.stream = new Rx.BehaviorSubject();
7 | // Creates an array to hold all actions dispatched within an application.
8 | // This feature allows for time travel debugging in O(n) space.
9 | this.history = [];
10 | this.timeTravelEnabled = false;
11 | this.history$ = this.getHistory();
12 | this.store = { 'omniHistory$': this.history$ };
13 | }
14 |
15 | // make it so actions are not dispatched if currently dragging timeline UNLESS they have an ignore property
16 |
17 |
18 | // Creates a state-stream with provided reducer and action stream
19 | createStatestream(reducer, actionStream = this.stream) {
20 | return actionStream(this)
21 | .merge(this.stream.filter(value => value ? value._clearState : false))
22 | .startWith(reducer(undefined, { type: null }))
23 | .scan((acc, curr) => (
24 | curr._clearState ? reducer(undefined, { type: null }) : reducer(acc, curr)
25 | ))
26 | }
27 |
28 | _createTimelineStatestream(reducer, actionStream) {
29 | return actionStream(this)
30 | .merge(this.stream.filter(value => value ? value._clearState : false))
31 | .startWith(reducer(undefined, { type: null }))
32 | .scan(reducer)
33 | }
34 |
35 | // Creates a collection of all state-streams
36 | createStore(streamCollection) {
37 | this.store = streamCollection;
38 | }
39 |
40 | addToStore(streamCollection) {
41 | this.store = Object.assign({}, this.store, streamCollection);
42 | }
43 |
44 | // Check whether each action dispatched has data and type properties.
45 | // If so, pass the action to the omnistream.
46 | dispatch(action) {
47 | if (!(action.hasOwnProperty('type') && !(action._clearState))) {
48 | throw new Error('Actions dispatched to omnistream must be objects with type properties')
49 | }
50 | if (this.timeTravelEnabled && action._ignore) this.stream.next(action);
51 | else if (!this.timeTravelEnabled) this.stream.next(action);
52 | }
53 | // Dispatch an observable to the omnistream
54 | dispatchObservableFn(streamFunction) {
55 | const sideEffectStream = streamFunction(this.stream.filter(action => action).skip(1));
56 | sideEffectStream.subscribe((action) => {
57 | this.dispatch(action);
58 | })
59 | }
60 |
61 | // Create an observable of data for a specific action type.
62 | filterForActionTypes(...actionTypes) {
63 | const actions = Array.isArray(actionTypes[0]) ? actionTypes[0] : actionTypes;
64 | const hash = actions.reduce((acc, curr) => Object.assign(acc, { [curr]: true }), {});
65 | return this.stream.filter(action => {
66 | return action ? (hash[action.type]) : false
67 | })
68 | }
69 |
70 | // Create an observable that updates history when a new action is received.
71 | getHistory() {
72 | const history$ = this.stream.filter(action => action && !action._ignore)
73 | .scan((acc, cur) => {
74 | acc.push(cur);
75 | return acc;
76 | }, [])
77 | .publish()
78 | history$.connect();
79 | history$.subscribe(el => this.history = el)
80 |
81 | // ignore side effects during time travel
82 | const enableTimeTravel$ = this.stream
83 | .filter(action => action ? (action.type === 'START_DRAG' || action.type === 'STOP_DRAG') : false)
84 | .map(action => action.type)
85 | enableTimeTravel$.subscribe(val => {
86 | this.timeTravelEnabled = (val === 'START_DRAG') ? true : false
87 | })
88 | return history$
89 | }
90 |
91 | // Revert the app back to its original state
92 | clearState() {
93 | this.stream.next({ _clearState: true, _ignore: true });
94 | }
95 |
96 | timeTravelToPointN(n) {
97 | this.clearState();
98 | for (let i = 0; i <= n; i++) {
99 | if (!(this.history[i].sideEffect)) {
100 | this.dispatch(Object.assign({ _ignore: true }, this.history[i]));
101 | }
102 | }
103 | }
104 | }
105 |
106 | export default function createOmnistream() {
107 | return new Omnistream();
108 | }
109 |
--------------------------------------------------------------------------------
/test/omnistream.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import createOmnistream from '../src/omnistream.js';
3 | import Rx from 'rxjs/rx';
4 |
5 | const omnistream = createOmnistream();
6 | test('omnistream has a behavior subject stream', (t) => {
7 | t.plan(1);
8 | t.true(omnistream.stream instanceof Rx.BehaviorSubject);
9 | });
10 |
11 | test('omnistream has history$ observable', (t) => {
12 | t.plan(2);
13 | t.true(omnistream.hasOwnProperty('history$'));
14 | t.true(omnistream.history$ instanceof Rx.Observable);
15 | });
16 |
17 | test('omnistream has store object with omniHistory$ observable', (t) => {
18 | t.plan(3);
19 | t.true(omnistream.hasOwnProperty('store'));
20 | t.true(typeof omnistream.store === 'object');
21 | t.true(omnistream.store.omniHistory$ instanceof Rx.Observable);
22 | });
23 |
24 | test('clearState emits action with _clearState and _ignore properties', (t) => {
25 | t.plan(2);
26 | const omnistream = createOmnistream();
27 | omnistream.clearState();
28 | return omnistream.stream.take(1).map((action) => {
29 | t.true(action._clearState);
30 | t.true(action._ignore);
31 | })
32 | })
33 |
34 | test('dispatch emits actions to omnistream', (t) => {
35 | t.plan(1);
36 | const omnistream = createOmnistream();
37 | omnistream.dispatch({ type: 'test' });
38 | return omnistream.stream.take(1).map((action) => {
39 | t.deepEqual({ type: 'test' }, action);
40 | })
41 | })
42 |
43 | test('dispatch throws error if no type provided', (t) => {
44 | t.plan(1);
45 | const omnistream = createOmnistream();
46 | t.throws(() => omnistream.dispatch({ notType: 'test' }),
47 | 'Actions dispatched to superstream must be objects with type properties')
48 | })
49 |
50 | test('createStore creates a store property', (t) => {
51 | t.plan(1);
52 | omnistream.createStore({ testStore$: Rx.Observable.never() });
53 | t.true(omnistream.hasOwnProperty('store'));
54 | })
55 |
56 | test('get history returns a subject', (t) => {
57 | t.plan(1);
58 | const omnistream = createOmnistream();
59 | const history$ = omnistream.getHistory();
60 | t.true(history$ instanceof Rx.Subject);
61 | })
62 |
63 | test('subscription to history$ returns y', (t) => {
64 | t.plan(2);
65 | const omnistream = createOmnistream();
66 | const history$ = omnistream.getHistory();
67 | omnistream.dispatch({ type: 'A' })
68 | t.deepEqual(omnistream.history, [{type: 'A'}])
69 | omnistream.dispatch({ type: 'B' })
70 | t.deepEqual(omnistream.history, [{type: 'A'}, {type: 'B'}])
71 | })
72 |
73 | test('history$ does not record actions with _ignore property', (t) => {
74 | t.plan(1);
75 | const omnistream = createOmnistream();
76 | const history$ = omnistream.getHistory();
77 | omnistream.dispatch({ type: 'A' })
78 | omnistream.dispatch({ type: 'B', _ignore: true })
79 | t.deepEqual(omnistream.history, [{type: 'A'}])
80 | })
81 |
82 | test('filterForActionTypes only outputs actions according to the parameters passed in', (t) => {
83 | t.plan(1);
84 | const omnistream = createOmnistream();
85 | const filteredStream$ = omnistream.filterForActionTypes('FIRST_ACTION');
86 | filteredStream$.subscribe((el) =>{
87 | t.deepEqual({ type: 'FIRST_ACTION' }, el)
88 | })
89 | omnistream.dispatch({ type: 'SECOND_ACTION' } )
90 | omnistream.dispatch({ type: 'FIRST_ACTION' })
91 | })
92 |
93 | test('filterForActionTypes takes multiple inputs', (t) => {
94 | t.plan(1);
95 | const omnistream = createOmnistream();
96 | const filteredStream$ = omnistream.filterForActionTypes('FIRST_ACTION', 'SECOND_ACTION');
97 | const actions = [];
98 | filteredStream$.subscribe(el => actions.push(el));
99 | omnistream.dispatch({ type: 'SECOND_ACTION' } )
100 | omnistream.dispatch({ type: 'FIRST_ACTION' })
101 | omnistream.dispatch({ type: 'THIRD_ACTION' })
102 | t.deepEqual(actions, [{ type: 'SECOND_ACTION' },{ type: 'FIRST_ACTION' }])
103 | })
104 |
105 | test('filterForActionTypes takes multiple inputs as an array', (t) => {
106 | t.plan(1);
107 | const omnistream = createOmnistream();
108 | const filteredStream$ = omnistream.filterForActionTypes(['FIRST_ACTION', 'SECOND_ACTION']);
109 | const actions = [];
110 | filteredStream$.subscribe(el => actions.push(el));
111 | omnistream.dispatch({ type: 'SECOND_ACTION' } )
112 | omnistream.dispatch({ type: 'FIRST_ACTION' })
113 | omnistream.dispatch({ type: 'THIRD_ACTION' })
114 | t.deepEqual(actions, [{ type: 'SECOND_ACTION' },{ type: 'FIRST_ACTION' }])
115 | })
116 |
117 | test('dispatchObservableFn dispatches values from returned observable', (t) => {
118 | t.plan(1);
119 | const testObservable = () => Rx.Observable.of({type: 'ACTION'});
120 | const omnistream = createOmnistream();
121 | omnistream.stream.subscribe(element => {
122 | if (element) t.deepEqual(element, {type: 'ACTION'})
123 | });
124 | omnistream.dispatchObservableFn(testObservable);
125 | })
126 |
127 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Omnistream
2 | ### Omnistream is a stream-based state management library for React built on RxJs observables.
3 |
4 | Omnistream has a simple API that allows React components to selectively subscribe to portions of a central store. This avoids unnecessary re-renders without the need of `shouldComponentUpdate` or other workarounds. Upon connecting, your components will always stay up to date with the store and re-render as needed. With this model, it's possible to work exclusively with stateless functional components, creating a more reactive application structure. Omnistream also features a built-in time-travelling debugger that operates without keeping any copies of the application state (since in Omnistream, the store is a stream of actions).
5 |
6 | In the spirit of [redux-observable](https://github.com/redux-observable/redux-observable), Omnistream is built around the idea of dispatching observables to your store. This allows you to compose some complicated async logic fairly easily.
7 |
8 | ## Disclaimer
9 |
10 | Omnistream is in early stages of development and all features are currently experimental. We would appreciate hearing about any issues you encounter or feature requests. Feel free to open issues or submit pull requests.
11 |
12 | ## Getting Started
13 | ----
14 |
15 | `npm install --save omnistream`
16 |
17 | ### Create a central store
18 |
19 | The central store is called the omnistream. After creating it and adding state streams, wrap your components in the provided StreamProvider component to give them access to it.
20 |
21 | ``` javascript
22 |
23 | import React from 'react'
24 | import ReactDOM from 'react-dom'
25 | import { createOmnistream, StreamProvider } from 'Omnistream'
26 | import { loginAction } from './actionStreams'; // import action stream creator
27 | import { loginReducer } from './reducers';
28 |
29 | const omnistream = createOmnistream();
30 | const loginState$ = omnistream.createStatestream(loginReducer, loginAction); // create a state stream
31 | omnistream.createStore({ loginState$ });
32 |
33 | ReactDOM.render(
34 | // wrap components in StreamProvider to give them access to the omnistream
35 | // pass the omnistream instance to the StreamProvider
36 |
37 | ,
38 | document.getElementById('root')
39 | );
40 | ```
41 |
42 | ### Creating a collection of state streams
43 |
44 | Omnistream stores state in a collection of "state streams." This is a simple object with keys representing different streams. Each of these streams emit objects which hold the current state for its subscriptions. With each new relevant action, a reducer will reduce a new copy of state, and this new copy will be pushed to subscribers.
45 |
46 | To set up state streams, there are three steps:
47 |
48 | 1. Write reducers
49 |
50 | 2. Call `omnistream.createStatestream` with a reducer and optional function to create a custom action stream
51 |
52 | 3. Combine state streams into the omnistream store
53 |
54 | If the second argument to `createStatestream` is not provided, the resulting state stream will send all dispatched actions to its reducer. We recommend filtering the omnistream to only the relevant actions required by the reducer. This can be done with the `omnistream.filterForActionTypes` method, which takes an array of action types (or multiple parameters specifying the action types), and returns a filtered action stream according to those actions. To use the second argument to `createStatestream`, provide a function that takes the omnistream as an argument and applies the `filterForActionTypes` method.
55 |
56 | Once state streams have been created, they can be added to the omnistream's store with the `omnistream.createStore` method. Note that this method overwrites the current store if it exists. To instead add a state stream to the store after it has been created, use `omnistream.addToStore`.
57 |
58 |
59 | Creating an action stream and state stream:
60 |
61 | ```javascript
62 | const omnistream = omnistream.createOmnistream();
63 | const loginAction = (omnisteam) => omnistream.filterForActionTypes('USER_LOGIN'); // creates login action stream
64 | const loginState$ = omnistream.createStatestream(loginReducer, loginAction); // creates login state stream
65 | omnistream.createStore({ loginState$, ...otherStates$ });
66 | ```
67 |
68 | ### Connecting a component
69 |
70 | To connect a component to the omnistream, wrap your component in a call to the provided `reactiveComponent` method, along with strings specifying the specific streams you'd like to subscribe to.
71 |
72 | ```javascript
73 | function User(props) {
74 | // destructure the props received from stream subscriptions
75 | const {username, url} = props;
76 | return (
77 |
78 |
{username}
79 |
80 |
81 | )
82 | }
83 | export default reactiveComponent(User, 'loginState$');
84 | ```
85 |
86 | In the above example, the component will be subscribed to `loginState$`, and all new copies of that state will be pushed to the component in its props.
87 |
88 | ### Dispatching actions
89 |
90 | Omnistream provides a `dispatch` method to all Reactive Components as part of their props. To dispatch an action, simply call dispatch with an object containing a `type` property.
91 |
92 | ```javascript
93 | function User(props) {
94 | const {username, url, dispatch} = props;
95 | return (
96 |
101 | )
102 | }
103 |
104 | export default reactiveComponent(User, 'loginState$');
105 | ```
106 |
107 | Here, an action of the form `{data: e.target.value, type: 'USER_NAME'}` is dispatched. When this action is dispatched, it is merged into the omnistream. Any updates from the `loginState$` will be pushed to this component in the form of props.
108 |
109 | ### Dispatching observables
110 |
111 | Omnistream also provides `omnistream.dispatchObservableFn` as a method to dispatch observables instead of simple actions. The observables can then emit their own streams of actions, which will be folded into the omnistream in the correct order. This allows one to design complex asynchronous action sequences. Furthermore, every dispatched observable will have access to `omnistream`, so you can create observables that interact with actions dispatched from separate parts of the app.
112 |
113 | `dispatchObservableFn` takes one argument, which should be a function that returns an observable. The observable function you provide will be passed the omnistream's action stream as its first parameter.
114 |
115 | ```javascript
116 | const timeUntilLogin = (omnistream) => (
117 | const login = omnistream.filter(action => action.type === 'USER_LOGIN');
118 | return Rx.Observable.interval(100)
119 | .scan((acc, curr) => acc + 100, 0)
120 | .map(ms => ({type: 'TIME_TO_LOGIN', ms}))
121 | .takeUntil(login);
122 | )
123 |
124 | class User extends Component() {
125 | componentDidMount() {
126 | this.props.dispatchObservableFn(timeUntilLogin);
127 | }
128 |
129 | render() {
130 | return (
131 |
136 | )
137 | }
138 | }
139 |
140 | export default reactiveComponent(User, 'loginState$');
141 | ```
142 |
143 | In this example, an observable is dispatched which will record the time up until a login action is sent. This is possible because `omnistream` is passed in as the first argument to the `timeUntilLogin` function. When the component mounts, it dispatches this function to the omnistream which evaluates it and folds in all resulting actions.
144 |
145 |
146 | ### Adding the timeline
147 |
148 | Adding the timeline debugger is simply a matter of including the provided `Timeline` component in your app, along with the omnistream as a prop.
149 |
150 | ```javascript
151 | import React from 'react'
152 | import ReactDOM from 'react-dom'
153 | import { createOmnistream, StreamProvider } from 'Omnistream'
154 | import {loginAction} from './actionStreams';
155 |
156 | const omnistream = createOmnistream();
157 | const loginState$ = omnistream.createStatestream(barPositionReducer, loginAction);
158 | omnistream.createStore({ loginState$ }); // create the store
159 |
160 | ReactDOM.render(
161 |
162 |
163 |
164 |
165 | // add the timeline component with omnistream as a prop
166 |
167 |
,
168 | document.getElementById('root')
169 | );
170 | ```
171 |
172 | When your app is rendered, it will now include a timeline with a visualization of every action in your app. Clicking on the slider will enable time travel, and dragging it to different actions will revert the app to that particular point. Side effects are ignored during time travel, so you don't need to worry about `componentDidMount` AJAX calls or similar events polluting the timeline.
173 |
174 | Double clicking on any action displayed in the timeline will revert the app to it's state upon receiving that action, and hovering over any action will display its actual javascript object representation.
175 |
176 | 
177 |
--------------------------------------------------------------------------------