From b6f9cfdf712a983f251e5d662a7cf3281773d7f7 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 4 May 2016 16:46:19 -0700 Subject: [PATCH] Move to redux-epic --- client/index.js | 3 +- common/app/App.jsx | 2 +- common/app/create-app.jsx | 2 +- common/app/routes/Hikes/components/Hikes.jsx | 2 +- common/app/routes/Jobs/components/Jobs.jsx | 2 +- common/app/routes/Jobs/components/Show.jsx | 2 +- common/app/routes/map/components/Show.jsx | 2 +- common/app/utils/professor-x.js | 117 ----------- common/app/utils/redux-epic.js | 57 ------ common/app/utils/redux-epic.test.js | 203 ------------------- common/app/utils/render-to-string.js | 24 --- common/app/utils/render.js | 18 -- package.json | 4 +- server/boot/a-react.js | 2 +- 14 files changed, 10 insertions(+), 430 deletions(-) delete mode 100644 common/app/utils/professor-x.js delete mode 100644 common/app/utils/redux-epic.js delete mode 100644 common/app/utils/redux-epic.test.js delete mode 100644 common/app/utils/render-to-string.js delete mode 100644 common/app/utils/render.js diff --git a/client/index.js b/client/index.js index f373787262..ef6d1a5cb6 100644 --- a/client/index.js +++ b/client/index.js @@ -4,6 +4,7 @@ import React from 'react'; import debug from 'debug'; import { Router } from 'react-router'; import { routeReducer as routing, syncHistory } from 'react-router-redux'; +import { render } from 'redux-epic'; import { createHistory } from 'history'; import createApp from '../common/app'; @@ -12,8 +13,6 @@ import provideStore from '../common/app/provide-store'; // client specific sagas import sagas from './sagas'; -// render to observable -import render from '../common/app/utils/render'; import { isColdStored, getColdStorage, diff --git a/common/app/App.jsx b/common/app/App.jsx index 130ac7f50f..d2984be6b2 100644 --- a/common/app/App.jsx +++ b/common/app/App.jsx @@ -3,6 +3,7 @@ import { Row } from 'react-bootstrap'; import { ToastMessage, ToastContainer } from 'react-toastr'; import { compose } from 'redux'; import { connect } from 'react-redux'; +import { contain } from 'redux-epic'; import { createSelector } from 'reselect'; import { @@ -10,7 +11,6 @@ import { updateWindowHeight, updateNavHeight } from './redux/actions'; -import contain from './utils/professor-x'; import getWindowHeight from './utils/get-window-height'; import Nav from './components/Nav'; diff --git a/common/app/create-app.jsx b/common/app/create-app.jsx index 27f8c30547..5fd9cfd4d2 100644 --- a/common/app/create-app.jsx +++ b/common/app/create-app.jsx @@ -8,7 +8,7 @@ import App from './App.jsx'; import childRoutes from './routes'; // redux -import createEpic from './utils/redux-epic'; +import { createEpic } from 'redux-epic'; import createReducer from './create-reducer'; import middlewares from './middlewares'; import sagas from './sagas'; diff --git a/common/app/routes/Hikes/components/Hikes.jsx b/common/app/routes/Hikes/components/Hikes.jsx index 05f11669d3..ccc7a68df0 100644 --- a/common/app/routes/Hikes/components/Hikes.jsx +++ b/common/app/routes/Hikes/components/Hikes.jsx @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react'; import { compose } from 'redux'; +import { contain } from 'redux-epic'; import { connect } from 'react-redux'; import PureComponent from 'react-pure-render/component'; import { createSelector } from 'reselect'; @@ -8,7 +9,6 @@ import { createSelector } from 'reselect'; import HikesMap from './Map.jsx'; import { fetchHikes } from '../redux/actions'; -import contain from '../../../utils/professor-x'; // const log = debug('fcc:hikes'); diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index 3b65c5905b..d0a7102fe8 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -1,5 +1,6 @@ import React, { cloneElement, PropTypes } from 'react'; import { compose } from 'redux'; +import { contain } from 'redux-epic'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { LinkContainer } from 'react-router-bootstrap'; @@ -7,7 +8,6 @@ import { LinkContainer } from 'react-router-bootstrap'; import PureComponent from 'react-pure-render/component'; import { Button, Row, Col } from 'react-bootstrap'; -import contain from '../../../utils/professor-x'; import ListJobs from './List.jsx'; import { diff --git a/common/app/routes/Jobs/components/Show.jsx b/common/app/routes/Jobs/components/Show.jsx index 429e4765b1..5a1f9fa3cf 100644 --- a/common/app/routes/Jobs/components/Show.jsx +++ b/common/app/routes/Jobs/components/Show.jsx @@ -1,11 +1,11 @@ import React, { PropTypes } from 'react'; import { compose } from 'redux'; +import { contain } from 'redux-epic'; import { connect } from 'react-redux'; import { push } from 'react-router-redux'; import PureComponent from 'react-pure-render/component'; import { createSelector } from 'reselect'; -import contain from '../../../utils/professor-x'; import { fetchJobs } from '../redux/actions'; import ShowJob from './ShowJob.jsx'; diff --git a/common/app/routes/map/components/Show.jsx b/common/app/routes/map/components/Show.jsx index 181b94ca66..05ba101889 100644 --- a/common/app/routes/map/components/Show.jsx +++ b/common/app/routes/map/components/Show.jsx @@ -1,11 +1,11 @@ import React, { PropTypes } from 'react'; import { compose } from 'redux'; +import { contain } from 'redux-epic'; import { connect } from 'react-redux'; import PureComponent from 'react-pure-render/component'; import { createSelector } from 'reselect'; import Map from './Map.jsx'; -import contain from '../../../utils/professor-x'; import { clearFilter, fetchChallenges, diff --git a/common/app/utils/professor-x.js b/common/app/utils/professor-x.js deleted file mode 100644 index 890ee8532d..0000000000 --- a/common/app/utils/professor-x.js +++ /dev/null @@ -1,117 +0,0 @@ -import { helpers } from 'rx'; -import { createElement } from 'react'; -import PureComponent from 'react-pure-render/component'; -import debug from 'debug'; - -// interface contain { -// (options?: Object, Component: ReactComponent) => ReactComponent -// (options?: Object) => (Component: ReactComponent) => ReactComponent -// } -// -// Action: { type: String, payload: Any, ...meta } -// -// ActionCreator(...args) => Observable -// -// interface options { -// fetchAction?: ActionCreator, -// getActionArgs?(props: Object, context: Object) => [], -// isPrimed?(props: Object, context: Object) => Boolean, -// handleError?(err) => Void -// shouldRefetch?( -// props: Object, -// nextProps: Object, -// context: Object, -// nextContext: Object -// ) => Boolean, -// } - - -const log = debug('fcc:professerx'); - -const __DEV__ = process.env.NODE_ENV !== 'production'; -const { isFunction } = helpers; - -export default function contain(options = {}, Component) { - /* istanbul ignore else */ - if (!Component) { - return contain.bind(null, options); - } - - let action; - let isActionable = false; - let hasRefetcher = isFunction(options.shouldRefetch); - const getActionArgs = isFunction(options.getActionArgs) ? - options.getActionArgs : - (() => []); - - const isPrimed = isFunction(options.isPrimed) ? - options.isPrimed : - (() => false); - - const name = Component.displayName || 'Anon Component'; - - function runAction(props, context, action) { - const actionArgs = getActionArgs(props, context); - if (__DEV__ && !Array.isArray(actionArgs)) { - throw new TypeError( - `${name} getActionArgs should return an array but got ${actionArgs}` - ); - } - return action.apply(null, actionArgs); - } - - - return class Container extends PureComponent { - static displayName = `Container(${name})`; - - componentWillMount() { - const { props, context } = this; - if (!options.fetchAction) { - log(`${name} has no fetch action defined`); - return; - } - if (isPrimed(this.props, this.context)) { - log(`${name} container is primed`); - return; - } - - action = props[options.fetchAction]; - isActionable = typeof action === 'function'; - - if (__DEV__ && !isActionable) { - throw new Error( - `${options.fetchAction} should return a function but got ${action}. - Check the fetch options for ${name}.` - ); - } - - runAction( - props, - context, - action - ); - } - - componentWillReceiveProps(nextProps, nextContext) { - if ( - !isActionable || - !hasRefetcher || - !options.shouldRefetch(this.props, nextProps, this.context, nextContext) - ) { - return; - } - - runAction( - nextProps, - nextContext, - action - ); - } - render() { - return createElement( - Component, - this.props - ); - } - }; -} diff --git a/common/app/utils/redux-epic.js b/common/app/utils/redux-epic.js deleted file mode 100644 index 0f8fbc4018..0000000000 --- a/common/app/utils/redux-epic.js +++ /dev/null @@ -1,57 +0,0 @@ -import { CompositeDisposable, Observable, Subject } from 'rx'; - -export default (dependencies, ...sagas) => { - if (typeof dependencies === 'function') { - sagas.push(dependencies); - dependencies = {}; - } - let action$; - let lifecycle; - let compositeDisposable; - let start; - function sagaMiddleware({ dispatch, getState }) { - start = () => { - compositeDisposable = new CompositeDisposable(); - action$ = new Subject(); - lifecycle = new Subject(); - const sagaSubscription = Observable - .from(sagas) - .map(saga => saga(action$, getState, dependencies)) - .doOnNext(result$ => { - if (!Observable.isObservable(result$)) { - throw new Error('saga should returned an observable'); - } - if (result$ === action$) { - throw new Error('Saga returned original action stream!'); - } - }) - .mergeAll() - .filter(action => action && typeof action.type === 'string') - .subscribe( - action => dispatch(action), - err => { throw err; }, - () => lifecycle.onCompleted() - ); - compositeDisposable.add(sagaSubscription); - }; - start(); - return next => action => { - const result = next(action); - action$.onNext(action); - return result; - }; - } - - sagaMiddleware.subscribe = - (...args) => lifecycle.subscribe.apply(lifecycle, args); - sagaMiddleware.subscribeOnCompleted = - (...args) => lifecycle.subscribeOnCompleted.apply(lifecycle, args); - sagaMiddleware.end = () => action$.onCompleted(); - sagaMiddleware.dispose = () => compositeDisposable.dispose(); - sagaMiddleware.restart = () => { - sagaMiddleware.dispose(); - action$.dispose(); - start(); - }; - return sagaMiddleware; -}; diff --git a/common/app/utils/redux-epic.test.js b/common/app/utils/redux-epic.test.js deleted file mode 100644 index 603a039b6d..0000000000 --- a/common/app/utils/redux-epic.test.js +++ /dev/null @@ -1,203 +0,0 @@ -import { Observable, Subject } from 'rx'; -import test from 'tape'; -import { spy } from 'sinon'; -import { applyMiddleware, createStore } from 'redux'; -import createSaga from './redux-epic'; - -const setup = (saga, spy) => { - const reducer = (state = 0) => state; - const sagaMiddleware = createSaga( - action$ => action$ - .filter(({ type }) => type === 'foo') - .map(() => ({ type: 'bar' })), - action$ => action$ - .filter(({ type }) => type === 'bar') - .map(({ type: 'baz' })), - saga ? saga : () => Observable.empty() - ); - const store = applyMiddleware(sagaMiddleware)(createStore)(spy || reducer); - return { - reducer, - sagaMiddleware, - store - }; -}; - -test('createSaga', t => { - const sagaMiddleware = createSaga( - action$ => action$.map({ type: 'foo' }) - ); - t.equal( - typeof sagaMiddleware, - 'function', - 'sagaMiddleware is not a function' - ); - t.equal( - typeof sagaMiddleware.subscribe, - 'function', - 'sagaMiddleware does not have a subscription method' - ); - t.equal( - typeof sagaMiddleware.subscribeOnCompleted, - 'function', - 'sagaMiddleware does not have a subscribeOnCompleted method' - ); - t.equal( - typeof sagaMiddleware.end, - 'function', - 'sagaMiddleware does not have an end method' - ); - t.equal( - typeof sagaMiddleware.restart, - 'function', - 'sagaMiddleware does not have an restart method' - ); - t.equal( - typeof sagaMiddleware.dispose, - 'function', - 'sagaMiddleware does not have a dispose method' - ); - t.end(); -}); - -test('dispatching actions', t => { - const reducer = spy((state = 0) => state); - const { store } = setup(null, reducer); - store.dispatch({ type: 'foo' }); - t.equal(reducer.callCount, 4, 'reducer is called four times'); - t.assert( - reducer.getCall(1).calledWith(0, { type: 'foo' }), - 'reducer called with initial action' - ); - t.assert( - reducer.getCall(2).calledWith(0, { type: 'bar' }), - 'reducer was not called with saga action' - ); - t.assert( - reducer.getCall(3).calledWith(0, { type: 'baz' }), - 'second saga responded to action from first saga' - ); - t.end(); -}); - -test('lifecycle', t => { - t.test('subscribe', t => { - const { sagaMiddleware } = setup(); - const subscription = sagaMiddleware.subscribeOnCompleted(() => {}); - t.assert( - subscription, - 'subscribe did not return a disposable' - ); - t.isEqual( - typeof subscription.dispose, - 'function', - 'disposable does not have a dispose method' - ); - t.doesNotThrow( - () => subscription.dispose(), - 'disposable is not disposable' - ); - t.end(); - }); - - t.test('end', t => { - const result$ = new Subject(); - const { sagaMiddleware } = setup(() => result$); - sagaMiddleware.subscribeOnCompleted(() => { - t.pass('all sagas completed'); - t.end(); - }); - sagaMiddleware.end(); - t.pass('saga still active'); - result$.onCompleted(); - }); - - t.test('disposable', t => { - const result$ = new Subject(); - const { sagaMiddleware } = setup(() => result$); - t.plan(2); - sagaMiddleware.subscribeOnCompleted(() => { - t.fail('all sagas completed'); - }); - t.assert( - result$.hasObservers(), - 'saga is observed by sagaMiddleware' - ); - sagaMiddleware.dispose(); - t.false( - result$.hasObservers(), - 'watcher has no observers after sagaMiddleware is disposed' - ); - }); -}); - -test('restart', t => { - const reducer = spy((state = 0) => state); - const { sagaMiddleware, store } = setup(null, reducer); - store.dispatch({ type: 'foo' }); - t.assert( - reducer.getCall(1).calledWith(0, { type: 'foo' }), - 'reducer called with initial dispatch' - ); - t.assert( - reducer.getCall(2).calledWith(0, { type: 'bar' }), - 'reducer called with saga action' - ); - t.assert( - reducer.getCall(3).calledWith(0, { type: 'baz' }), - 'second saga responded to action from first saga' - ); - sagaMiddleware.end(); - t.equal(reducer.callCount, 4, 'saga produced correct amount of actions'); - sagaMiddleware.restart(); - store.dispatch({ type: 'foo' }); - t.equal( - reducer.callCount, - 7, - 'saga restart and produced correct amount of actions' - ); - t.assert( - reducer.getCall(4).calledWith(0, { type: 'foo' }), - 'reducer called with second dispatch' - ); - t.assert( - reducer.getCall(5).calledWith(0, { type: 'bar' }), - 'reducer called with saga reaction' - ); - t.assert( - reducer.getCall(6).calledWith(0, { type: 'baz' }), - 'second saga responded to action from first saga' - ); - t.end(); -}); - -test('long lived saga', t => { - let count = 0; - const tickSaga = action$ => action$ - .filter(({ type }) => type === 'start-tick') - .flatMap(() => Observable.interval(500)) - // make sure long lived saga's do not persist after - // action$ has completed - .takeUntil(action$.last()) - .map(({ type: 'tick' })); - - const reducerSpy = spy((state = 0) => state); - const { store, sagaMiddleware } = setup(tickSaga, reducerSpy); - const unlisten = store.subscribe(() => { - count += 1; - if (count >= 5) { - sagaMiddleware.end(); - } - }); - sagaMiddleware.subscribeOnCompleted(() => { - t.equal( - count, - 5, - 'saga dispatched correct amount of ticks' - ); - unlisten(); - t.pass('long lived saga completed'); - t.end(); - }); - store.dispatch({ type: 'start-tick' }); -}); diff --git a/common/app/utils/render-to-string.js b/common/app/utils/render-to-string.js deleted file mode 100644 index a7fc41f64e..0000000000 --- a/common/app/utils/render-to-string.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Observable } from 'rx'; -import ReactDOM from 'react-dom/server'; -import debug from 'debug'; - -const log = debug('fcc:professor'); - -export default function renderToString(Component, sagaMiddleware) { - try { - log('initial render'); - ReactDOM.renderToStaticMarkup(Component); - log('initial render completed'); - } catch (e) { - return Observable.throw(e); - } - sagaMiddleware.end(); - return Observable.merge(sagaMiddleware) - .last({ defaultValue: null }) - .delay(0) - .map(() => { - sagaMiddleware.restart(); - const markup = ReactDOM.renderToString(Component); - return { markup }; - }); -} diff --git a/common/app/utils/render.js b/common/app/utils/render.js deleted file mode 100644 index 3bfda77633..0000000000 --- a/common/app/utils/render.js +++ /dev/null @@ -1,18 +0,0 @@ -import ReactDOM from 'react-dom'; -import { Disposable, Observable } from 'rx'; - -export default function render(Component, DOMContainer) { - return Observable.create(observer => { - try { - ReactDOM.render(Component, DOMContainer, function() { - observer.onNext(this); - }); - } catch (e) { - return observer.onError(e); - } - - return Disposable.create(() => { - return ReactDOM.unmountComponentAtNode(DOMContainer); - }); - }); -} diff --git a/package.json b/package.json index 44f4d41e38..71b2cb10ca 100644 --- a/package.json +++ b/package.json @@ -81,9 +81,8 @@ "pmx": "~0.6.2", "react": "^15.0.2", "react-bootstrap": "~0.29.4", + "react-css-transition-replace": "^1.2.0-beta", "react-dom": "^15.0.2", - "react-addons-css-transition-group": "^0.14.7", - "react-css-transition-replace": "^1.1.0", "react-fontawesome": "^0.3.3", "react-motion": "~0.4.2", "react-no-ssr": "^1.0.1", @@ -96,6 +95,7 @@ "react-youtube": "^6.1.0", "redux": "^3.0.5", "redux-actions": "^0.9.1", + "redux-epic": "^0.1.1", "redux-form": "^5.2.3", "request": "^2.65.0", "reselect": "^2.0.2", diff --git a/server/boot/a-react.js b/server/boot/a-react.js index 0e1259a933..8a33522d0e 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -2,7 +2,7 @@ import React from 'react'; import { RouterContext } from 'react-router'; import debug from 'debug'; -import renderToString from '../../common/app/utils/render-to-string'; +import { renderToString } from 'redux-epic'; import provideStore from '../../common/app/provide-store'; import createApp from '../../common/app';