Use react-router 4

This commit is contained in:
Pierre Burgy 2017-08-21 15:12:53 +02:00
parent 385a486716
commit fe8fa6a54b
24 changed files with 453 additions and 226 deletions

View File

@ -10,58 +10,32 @@ import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { applyRouterMiddleware, Router, browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import useScroll from 'react-router-scroll';
import { ConnectedRouter } from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
import _ from 'lodash';
import 'sanitize.css/sanitize.css';
import LanguageProvider from 'containers/LanguageProvider';
import NotificationProvider from 'containers/NotificationProvider';
import { selectLocationState } from 'containers/App/selectors';
import App from 'containers/App';
import { showNotification } from 'containers/NotificationProvider/actions';
import { pluginLoaded, updatePlugin } from 'containers/App/actions';
import createRoutes from './routes';
import configureStore from './store';
import { translationMessages, languages } from './i18n';
// Create redux store with history
// this uses the singleton browserHistory provided by react-router
// Optionally, this could be changed to leverage a created history
// e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();`
const initialState = {};
const store = configureStore(initialState, browserHistory);
// Sync history and store, as the react-router-redux reducer
// is under the non-default key ("routing"), selectLocationState
// must be provided for resolving how to retrieve the "route" in the state
const history = syncHistoryWithStore(browserHistory, store, {
selectLocationState: selectLocationState(),
});
// Set up the router, wrapping all Routes in the App component
const rootRoute = {
component: App,
childRoutes: createRoutes(store),
};
const history = createHistory();
const store = configureStore(initialState, history);
const render = (translatedMessages) => {
ReactDOM.render(
<Provider store={store}>
<LanguageProvider messages={translatedMessages}>
<NotificationProvider>
<Router
history={history}
routes={rootRoute}
render={
// Scroll to top when going to a new page, imitating default browser
// behaviour
applyRouterMiddleware(useScroll())
}
/>
</NotificationProvider>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</LanguageProvider>
</Provider>,
document.getElementById('app')
@ -160,7 +134,7 @@ window.Strapi = {
store.dispatch(updatePlugin(pluginId, 'leftMenuSections', leftMenuSectionsUpdated));
},
}),
router: browserHistory,
// router: browserHistory,
languages,
};

View File

@ -5,7 +5,7 @@
*/
import React from 'react';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import styles from './styles.scss';
@ -13,7 +13,7 @@ class LeftMenuHeader extends React.Component { // eslint-disable-line react/pref
render() {
return (
<div className={styles.leftMenuHeader}>
<Link to="/" className={styles.leftMenuHeaderLink}>
<Link to="/admin" className={styles.leftMenuHeaderLink}>
<span className={styles.projectName}></span>
</Link>
</div>

View File

@ -7,7 +7,7 @@
import _ from 'lodash';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import styles from './styles.scss';

View File

@ -68,17 +68,17 @@ class LeftMenuLinkContainer extends React.Component { // eslint-disable-line rea
<LeftMenuLink
icon="cubes"
label={messages.listPlugins.id}
destination="/list-plugins"
destination="/admin/list-plugins"
/>
<LeftMenuLink
icon="download"
label={messages.installNewPlugin.id}
destination="/install-plugin"
destination="/admin/install-plugin"
/>
<LeftMenuLink
icon="gear"
label={messages.configuration.id}
destination="/configuration"
destination="/admin/configuration"
/>
</ul>
</div>

View File

@ -0,0 +1,73 @@
/*
* AdminPage
*
* This is the first thing users see of our AdminPage, at the '/' route
*
* NOTE: while this component should technically be a stateless functional
* component (SFC), hot reloading does not currently support SFCs. If hot
* reloading is not a neccessity for you then you can refactor it and remove
* the linting exception.
*/
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { Switch, Route } from 'react-router-dom';
import HomePage from 'containers/HomePage';
import PluginPage from 'containers/PluginPage';
import ComingSoonPage from 'containers/ComingSoonPage';
import LeftMenu from 'containers/LeftMenu';
import Content from 'containers/Content';
import NotFoundPage from 'containers/NotFoundPage';
import { selectPlugins } from 'containers/App/selectors';
import { hideNotification } from 'containers/NotificationProvider/actions';
import Header from 'components/Header/index';
import styles from './syles.scss';
export class AdminPage extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<div className={styles.adminPage}>
<LeftMenu plugins={this.props.plugins} />
<div className={styles.adminPageRightWrapper}>
<Header />
<Content {...this.props}>
<Switch>
<Route path="/admin" component={HomePage} exact />
<Route path="/admin/plugins" component={PluginPage} />
<Route path="/admin/list-plugins" component={ComingSoonPage} exact />
<Route path="/admin/install-plugin" component={ComingSoonPage} exact />
<Route path="/admin/configuration" component={ComingSoonPage} exact />
<Route path="" component={NotFoundPage} />
</Switch>
</Content>
</div>
</div>
);
}
}
AdminPage.contextTypes = {
router: React.PropTypes.object.isRequired,
};
AdminPage.propTypes = {
plugins: React.PropTypes.object.isRequired,
};
const mapStateToProps = createStructuredSelector({
plugins: selectPlugins(),
});
function mapDispatchToProps(dispatch) {
return {
onHideNotification: (id) => { dispatch(hideNotification(id)); },
dispatch,
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AdminPage);

View File

@ -1,10 +1,10 @@
/* Import */
@import '../../styles/variables/variables';
.homePage { /* stylelint-disable */
.adminPage { /* stylelint-disable */
display: flex;
}
.homePageRightWrapper {
.adminPageRightWrapper {
width: calc(100% - #{$left-menu-width});
}
}

View File

@ -1,6 +1,6 @@
/**
*
* App.react.js
* App.js
*
* This component is the skeleton around the actual pages, and should only
* contain code that should be seen on all pages. (e.g. navigation bar)
@ -12,14 +12,13 @@
*/
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { Switch, Route } from 'react-router-dom';
import { hideNotification } from 'containers/NotificationProvider/actions';
import { selectNotifications } from 'containers/NotificationProvider/selectors';
import NotificationsContainer from 'components/NotificationsContainer';
import AdminPage from 'containers/AdminPage';
import NotFoundPage from 'containers/NotFoundPage';
import NotificationProvider from 'containers/NotificationProvider';
import { selectPlugins } from './selectors';
import '../../styles/main.scss';
import styles from './styles.scss';
@ -27,9 +26,12 @@ export class App extends React.Component { // eslint-disable-line react/prefer-s
render() {
return (
<div>
<NotificationsContainer onHideNotification={this.props.onHideNotification} notifications={this.props.notifications}></NotificationsContainer>
<NotificationProvider />
<div className={styles.container}>
{React.Children.toArray(this.props.children)}
<Switch>
<Route path="/admin" component={AdminPage} />
<Route path="" component={NotFoundPage} />
</Switch>
</div>
</div>
);
@ -40,22 +42,6 @@ App.contextTypes = {
router: React.PropTypes.object.isRequired,
};
App.propTypes = {
children: React.PropTypes.node.isRequired,
notifications: React.PropTypes.object.isRequired,
onHideNotification: React.PropTypes.func.isRequired,
};
App.propTypes = {};
const mapStateToProps = createStructuredSelector({
plugins: selectPlugins(),
notifications: selectNotifications(),
});
function mapDispatchToProps(dispatch) {
return {
onHideNotification: (id) => { dispatch(hideNotification(id)); },
dispatch,
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
export default App;

View File

@ -1,22 +1,5 @@
import { createSelector } from 'reselect';
// selectLocationState expects a plain JS object for the routing state
const selectLocationState = () => {
let prevRoutingState;
let prevRoutingStateJS;
return (state) => {
const routingState = state.get('route'); // or state.route
if (!routingState.equals(prevRoutingState)) {
prevRoutingState = routingState;
prevRoutingStateJS = routingState.toJS();
}
return prevRoutingStateJS;
};
};
/**
* Direct selector to the languageToggle state domain
*/
@ -28,12 +11,10 @@ const selectApp = () => (state) => state.get('app');
const selectPlugins = () => createSelector(
selectApp(),
(languageState) => languageState.get('plugins')
(appState) => appState.get('plugins')
);
export {
selectApp,
selectPlugins,
selectLocationState,
};

View File

@ -1,46 +1,29 @@
/*
*
* HomePage
*
* This is the first thing users see of our App, at the '/' route
*
* NOTE: while this component should technically be a stateless functional
* component (SFC), hot reloading does not currently support SFCs. If hot
* reloading is not a neccessity for you then you can refactor it and remove
* the linting exception.
*/
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { selectPlugins } from 'containers/App/selectors';
import LeftMenu from 'containers/LeftMenu';
import Header from 'components/Header/index';
import Content from 'containers/Content';
import styles from './syles.scss';
import Helmet from 'react-helmet';
import { FormattedMessage } from 'react-intl';
import messages from './messages.json';
import styles from './styles.scss';
export class HomePage extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<div className={styles.homePage}>
<LeftMenu plugins={this.props.plugins}></LeftMenu>
<div className={styles.homePageRightWrapper}>
<Header></Header>
<Content {...this.props}>
</Content>
</div>
<div className={styles.wrapper}>
<Helmet
title="Home Page"
/>
<p><FormattedMessage {...messages.welcome} />.</p>
</div>
);
}
}
HomePage.propTypes = {
plugins: React.PropTypes.object.isRequired,
};
const mapStateToProps = createStructuredSelector({
plugins: selectPlugins(),
});
function mapDispatchToProps(dispatch) {
return {
@ -48,4 +31,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
export default connect(mapDispatchToProps)(HomePage);

View File

@ -0,0 +1,6 @@
{
"welcome": {
"id": "app.components.HomePage.welcome",
"defaultMessage": "Welcome to the home page"
}
}

View File

@ -0,0 +1,3 @@
.wrapper {
padding: 2.3rem;
}

View File

@ -11,7 +11,7 @@
import React from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Link } from 'react-router-dom';
import styles from './styles.scss';
import messages from './messages.json';
@ -28,7 +28,7 @@ export default class NotFound extends React.Component { // eslint-disable-line r
<h2 className={styles.notFoundDescription}>
<FormattedMessage {...messages.description} />
</h2>
<Link to={'/'}>Back to home page.</Link>
<Link to="/admin">Back to home page.</Link>
</div>
);
}

View File

@ -6,27 +6,38 @@
import React from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import NotificationsContainer from 'components/NotificationsContainer';
import { selectNotifications } from './selectors';
import { hideNotification } from './actions';
import selectNotificationProvider from './selectors';
export class NotificationProvider extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<div>
{React.Children.only(this.props.children)}
</div>
<NotificationsContainer
onHideNotification={this.props.onHideNotification}
notifications={this.props.notifications}
/>
);
}
}
NotificationProvider.propTypes = {
children: React.PropTypes.object.isRequired,
notifications: React.PropTypes.object.isRequired,
onHideNotification: React.PropTypes.func.isRequired,
};
const mapStateToProps = selectNotificationProvider();
const mapStateToProps = createStructuredSelector({
notifications: selectNotifications(),
});
function mapDispatchToProps(dispatch) {
return {
onHideNotification: (id) => {
dispatch(hideNotification(id));
},
dispatch,
};
}

View File

@ -1,12 +1,12 @@
/**
* Combine all reducers in this file and export the combined reducers.
* If we were to do this in store.js, reducers wouldn't be hot reloadable.
*/
import { combineReducers } from 'redux-immutable';
import { fromJS } from 'immutable';
import { combineReducers } from 'redux-immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
import appReducer from 'containers/App/reducer';
import globalReducer from 'containers/App/reducer';
import languageProviderReducer from 'containers/LanguageProvider/reducer';
import notificationProviderReducer from 'containers/NotificationProvider/reducer';
@ -14,13 +14,13 @@ import notificationProviderReducer from 'containers/NotificationProvider/reducer
* routeReducer
*
* The reducer merges route location changes into our immutable state.
* The change is necessitated by moving to react-router-redux@4
* The change is necessitated by moving to react-router-redux@5
*
*/
// Initial routing state
const routeInitialState = fromJS({
locationBeforeTransitions: null,
location: null,
});
/**
@ -31,7 +31,7 @@ function routeReducer(state = routeInitialState, action) {
/* istanbul ignore next */
case LOCATION_CHANGE:
return state.merge({
locationBeforeTransitions: action.payload,
location: action.payload,
});
default:
return state;
@ -39,14 +39,14 @@ function routeReducer(state = routeInitialState, action) {
}
/**
* Creates the main reducer with the asynchronously loaded ones
* Creates the main reducer with the dynamically injected ones
*/
export default function createReducer(asyncReducers) {
export default function createReducer(injectedReducers) {
return combineReducers({
route: routeReducer,
app: globalReducer,
language: languageProviderReducer,
notification: notificationProviderReducer,
app: appReducer,
...asyncReducers,
...injectedReducers,
});
}

View File

@ -1,5 +1,5 @@
/**
* Create the store with asynchronously loaded reducers
* Create the store with dynamic reducers
*/
import { createStore, applyMiddleware, compose } from 'redux';
@ -9,7 +9,6 @@ import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';
const sagaMiddleware = createSagaMiddleware();
const devtools = window.devToolsExtension || (() => noop => noop);
export default function configureStore(initialState = {}, history) {
// Create the store with two middlewares
@ -22,30 +21,40 @@ export default function configureStore(initialState = {}, history) {
const enhancers = [
applyMiddleware(...middlewares),
devtools(),
];
// If Redux DevTools Extension is installed use it, otherwise use Redux compose
/* eslint-disable no-underscore-dangle */
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading
// Prevent recomputing reducers for `replaceReducer`
shouldHotReload: false,
})
: compose;
/* eslint-enable */
const store = createStore(
createReducer(),
fromJS(initialState),
compose(...enhancers)
composeEnhancers(...enhancers)
);
// Create hook for async sagas
// Extensions
store.runSaga = sagaMiddleware.run;
store.injectedReducers = {}; // Reducer registry
store.injectedSagas = {}; // Saga registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
System.import('./reducers').then((reducerModule) => {
const createReducers = reducerModule.default;
const nextReducers = createReducers(store.asyncReducers);
store.replaceReducer(nextReducers);
module.hot.accept('./reducers', () => {
store.replaceReducer(createReducer(store.injectedReducers));
});
}
// Initialize it with no other reducers
store.asyncReducers = {};
return store;
}

View File

@ -1,72 +0,0 @@
import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash';
import invariant from 'invariant';
import warning from 'warning';
import createReducer from 'reducers';
/**
* Validate the shape of redux store
*/
export function checkStore(store) {
const shape = {
dispatch: isFunction,
subscribe: isFunction,
getState: isFunction,
replaceReducer: isFunction,
runSaga: isFunction,
asyncReducers: isObject,
};
invariant(
conformsTo(store, shape),
'(app/utils...) asyncInjectors: Expected a valid redux store'
);
}
/**
* Inject an asynchronously loaded reducer
*/
export function injectAsyncReducer(store, isValid) {
return function injectReducer(name, asyncReducer) {
if (!isValid) checkStore(store);
invariant(
isString(name) && !isEmpty(name) && isFunction(asyncReducer),
'(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function'
);
store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.asyncReducers));
};
}
/**
* Inject an asynchronously loaded saga
*/
export function injectAsyncSagas(store, isValid) {
return function injectSagas(sagas) {
if (!isValid) checkStore(store);
invariant(
Array.isArray(sagas),
'(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions'
);
warning(
!isEmpty(sagas),
'(app/utils...) injectAsyncSagas: Received an empty `sagas` array'
);
sagas.map(store.runSaga);
};
}
/**
* Helper for creating injectors
*/
export function getAsyncInjectors(store) {
checkStore(store);
return {
injectReducer: injectAsyncReducer(store, true),
injectSagas: injectAsyncSagas(store, true),
};
}

View File

@ -0,0 +1,25 @@
import conformsTo from 'lodash/conformsTo';
import isFunction from 'lodash/isFunction';
import isObject from 'lodash/isObject';
import invariant from 'invariant';
/**
* Validate the shape of redux store
*/
export default function checkStore(store) {
console.log(store);
const shape = {
dispatch: isFunction,
subscribe: isFunction,
getState: isFunction,
replaceReducer: isFunction,
runSaga: isFunction,
injectedReducers: isObject,
injectedSagas: isObject,
};
invariant(
conformsTo(store, shape),
'(app/utils...) injectors: Expected a valid redux store'
);
}

View File

@ -0,0 +1,3 @@
export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount';
export const DAEMON = '@@saga-injector/daemon';
export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount';

View File

@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import hoistNonReactStatics from 'hoist-non-react-statics';
import getInjectors from './reducerInjectors';
/**
* Dynamically injects a reducer
*
* @param {string} key A key of the reducer
* @param {function} reducer A reducer that will be injected
*
*/
export default ({ key, reducer }) => (WrappedComponent) => {
class ReducerInjector extends React.Component {
static WrappedComponent = WrappedComponent;
static contextTypes = {
store: PropTypes.object.isRequired,
};
static displayName = `withReducer(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`;
componentWillMount() {
const { injectReducer } = this.injectors;
injectReducer(key, reducer);
}
injectors = getInjectors(this.context.store);
render() {
return <WrappedComponent {...this.props} />;
}
}
return hoistNonReactStatics(ReducerInjector, WrappedComponent);
};

View File

@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import hoistNonReactStatics from 'hoist-non-react-statics';
import getInjectors from './sagaInjectors';
/**
* Dynamically injects a saga, passes component's props as saga arguments
*
* @param {string} key A key of the saga
* @param {function} saga A root saga that will be injected
* @param {string} [mode] By default (constants.RESTART_ON_REMOUNT) the saga will be started on component mount and
* cancelled with `task.cancel()` on component un-mount for improved performance. Another two options:
* - constants.DAEMONstarts the saga on component mount and never cancels it or starts again,
* - constants.ONCE_TILL_UNMOUNTbehaves like 'RESTART_ON_REMOUNT' but never runs it again.
*
*/
export default ({ key, saga, mode }) => (WrappedComponent) => {
class InjectSaga extends React.Component {
static WrappedComponent = WrappedComponent;
static contextTypes = {
store: PropTypes.object.isRequired,
};
static displayName = `withSaga(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`;
componentWillMount() {
const { injectSaga } = this.injectors;
injectSaga(key, { saga, mode }, this.props);
}
componentWillUnmount() {
const { ejectSaga } = this.injectors;
ejectSaga(key);
}
injectors = getInjectors(this.context.store);
render() {
return <WrappedComponent {...this.props} />;
}
}
return hoistNonReactStatics(InjectSaga, WrappedComponent);
};

View File

@ -0,0 +1,32 @@
import invariant from 'invariant';
import isEmpty from 'lodash/isEmpty';
import isFunction from 'lodash/isFunction';
import isString from 'lodash/isString';
import createReducer from '../reducers';
import checkStore from './checkStore';
export function injectReducerFactory(store, isValid) {
return function injectReducer(key, reducer) {
if (!isValid) checkStore(store);
invariant(
isString(key) && !isEmpty(key) && isFunction(reducer),
'(app/utils...) injectReducer: Expected `reducer` to be a reducer function'
);
// Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
if (Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer) return;
store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.injectedReducers));
};
}
export default function getInjectors(store) {
checkStore(store);
return {
injectReducer: injectReducerFactory(store, true),
};
}

View File

@ -0,0 +1,46 @@
import 'whatwg-fetch';
/**
* Parses the JSON returned by a network request
*
* @param {object} response A response from a network request
*
* @return {object} The parsed JSON from the request
*/
function parseJSON(response) {
if (response.status === 204 || response.status === 205) {
return null;
}
return response.json();
}
/**
* Checks if a network request came back fine, and throws an error if not
*
* @param {object} response A response from a network request
*
* @return {object|undefined} Returns either the response, or throws an error
*/
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
/**
* Requests a URL, returning a promise
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
*
* @return {object} The response data
*/
export default function request(url, options) {
return fetch(url, options)
.then(checkStatus)
.then(parseJSON);
}

View File

@ -0,0 +1,86 @@
import isEmpty from 'lodash/isEmpty';
import isFunction from 'lodash/isFunction';
import isString from 'lodash/isString';
import invariant from 'invariant';
import conformsTo from 'lodash/conformsTo';
import checkStore from './checkStore';
import {
DAEMON,
ONCE_TILL_UNMOUNT,
RESTART_ON_REMOUNT,
} from './constants';
const allowedModes = [RESTART_ON_REMOUNT, DAEMON, ONCE_TILL_UNMOUNT];
const checkKey = (key) => invariant(
isString(key) && !isEmpty(key),
'(app/utils...) injectSaga: Expected `key` to be a non empty string'
);
const checkDescriptor = (descriptor) => {
const shape = {
saga: isFunction,
mode: (mode) => isString(mode) && allowedModes.includes(mode),
};
invariant(
conformsTo(descriptor, shape),
'(app/utils...) injectSaga: Expected a valid saga descriptor'
);
};
export function injectSagaFactory(store, isValid) {
return function injectSaga(key, descriptor = {}, args) {
if (!isValid) checkStore(store);
const newDescriptor = { ...descriptor, mode: descriptor.mode || RESTART_ON_REMOUNT };
const { saga, mode } = newDescriptor;
checkKey(key);
checkDescriptor(newDescriptor);
let hasSaga = Reflect.has(store.injectedSagas, key);
if (process.env.NODE_ENV !== 'production') {
const oldDescriptor = store.injectedSagas[key];
// enable hot reloading of daemon and once-till-unmount sagas
if (hasSaga && oldDescriptor.saga !== saga) {
oldDescriptor.task.cancel();
hasSaga = false;
}
}
if (!hasSaga || (hasSaga && mode !== DAEMON && mode !== ONCE_TILL_UNMOUNT)) {
store.injectedSagas[key] = { ...newDescriptor, task: store.runSaga(saga, args) }; // eslint-disable-line no-param-reassign
}
};
}
export function ejectSagaFactory(store, isValid) {
return function ejectSaga(key) {
if (!isValid) checkStore(store);
checkKey(key);
if (Reflect.has(store.injectedSagas, key)) {
const descriptor = store.injectedSagas[key];
if (descriptor.mode !== DAEMON) {
descriptor.task.cancel();
// Clean up in production; in development we need `descriptor.saga` for hot reloading
if (process.env.NODE_ENV === 'production') {
// Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga`
store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign
}
}
}
};
}
export default function getInjectors(store) {
checkStore(store);
return {
injectSaga: injectSagaFactory(store, true),
ejectSaga: ejectSagaFactory(store, true),
};
}

View File

@ -66,7 +66,7 @@
"express": "^4.15.4",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^0.11.2",
"history": "3.0.0",
"history": "^4.6.3",
"html-loader": "^0.5.1",
"html-webpack-plugin": "^2.30.1",
"image-webpack-loader": "^3.3.1",
@ -92,9 +92,8 @@
"react-helmet": "^5.1.3",
"react-intl": "^2.3.0",
"react-redux": "^5.0.6",
"react-router": "2.6.1",
"react-router-redux": "4.0.5",
"react-router-scroll": "0.2.1",
"react-router-dom": "^4.1.2",
"react-router-redux": "^5.0.0-alpha.6",
"redux": "^3.7.2",
"redux-immutable": "^4.0.0",
"redux-saga": "^0.15.6",
@ -112,4 +111,4 @@
"devDependencies": {
"uglifyjs-webpack-plugin": "^1.0.0-beta.2"
}
}
}