rechannel
Version:
Opinionated glue for building web apps with `React` and `Redux`.
151 lines (118 loc) • 4.5 kB
JSX
import React from 'react';
import {render} from 'react-dom';
import {trigger} from 'redial';
import {Provider} from 'react-redux';
import {createStore, combineReducers, compose, applyMiddleware} from 'redux';
import {match, Router, useRouterHistory, browserHistory} from 'react-router';
import {syncHistoryWithStore, routerReducer, routerMiddleware} from 'react-router-redux';
import cookie from 'component-cookie';
import qs from 'query-string';
const isDevMode = process.env.NODE_ENV !== 'production';
const defaultOptions = {
reducer: {},
middleware: [],
enhancer: [],
$init: () => Promise.resolve(),
$load: () => Promise.resolve()
};
/**
* Render an app on the client
* @param {object} options
* @param {Element} options.routes Your react-router routes
* @param {object} options.reducer Your redux reducer
* @param {Array<function>} [options.middleware] Your redux middleware(s)
* @param {Array<function>} [options.enhancer] Your Redux enhancer(s)
* @param {History} [options.history] Your react-router history instance
* @param {HTMLElement} [options.element] The HTMLElement which react will render into
* @returns {function}
*/
export default function(options) {
let {
routes, reducer, middleware, enhancer, history,
$init, $load,
element
} = {...defaultOptions, ...options};
//get the app element to render into
element = element || document.querySelector('#app');
//validate options
if (isDevMode) {
if (typeof reducer !== 'object') {
throw new Error('Your `reducer` must be an object passable to `combineReducers`.');
}
}
//use the browser history if the user hasn't specified one
if (!history) {
history = browserHistory;
}
//add middleware to freeze the redux state
const allTheMiddleware = [...middleware, routerMiddleware(history)];
if (isDevMode) {
allTheMiddleware.unshift(require('redux-immutable-state-invariant')())
}
//create the store
const store = createStore(
combineReducers({
...reducer,
routing: routerReducer
}),
window.__INITIAL_STATE__,
compose(
applyMiddleware(...allTheMiddleware),
...enhancer,
typeof window === 'object' && typeof window.devToolsExtension !== 'undefined'
? window.devToolsExtension()
: f => f
)
);
const context = {
headers: {},
cookies: cookie() || {},
query: qs.parse(window.location.search) || {}
};
Promise.resolve($init({getState: store.getState, dispatch: store.dispatch, ...context}))
.then(() => {
//create the routes if we've been given a factory function
if (typeof routes === 'function') {
routes = routes({getState: store.getState, dispatch: store.dispatch, ...context});
}
//create the enhanced history
const enhancedHistory = syncHistoryWithStore(history, store);
//when the URL changes
enhancedHistory.listen(location => {
//route the URL to a component
match({routes, history: enhancedHistory}, (routeError, redirectLocation, renderProps) => {
if (window.__INITIAL_STATE__) { //the current page was rendered by the server, we don't need to fetch
delete window.__INITIAL_STATE__;
} else { //the current page was navigated to on the client, we need to fetch
if (renderProps) {
const locals = {
dispatch: store.dispatch,
getState: store.getState,
location: renderProps.location,
params: renderProps.params,
...context
};
//fetch data required by the component
Promise.resolve()
.then(() => trigger('fetch', renderProps.components, locals))
.then(() => $load(locals))
;
}
}
});
});
//route the URL to a component
//this is required for https://github.com/ReactTraining/react-router/blob/master/docs/guides/ServerRendering.md#async-routes
match({routes, history: enhancedHistory}, (routeError, redirectLocation, renderProps) => {
//render the app
render(
<Provider store={store}>
<Router {...renderProps}/>
</Provider>,
element
);
});
})
.catch(err => console.error(err))
;
}