React application with React Router v4, async components etc
MIT License
React application with React Router v4, async components etc
Demo app to show react router v4 beta with async component, code split and async reducer registration.
Bases on excellent work of great people
Root.jsx
. There you can see that we have pretty basic RR4 routing.<ul>
<li> <Link to="/">Home</Link> </li>
<li> <Link to="/about">About</Link> </li>
<li> <Link to="/topics">Topics</Link> </li>
<li> <Link to="/legal">Legal</Link> </li>
</ul>
client-entry.js
. Here we have Routing set up for React Router and Redux store.// create render function
const render = RootEl => {
const app = (
<Provider store={store}>
<ReactHotLoader>
<Router><RootEl /></Router>
</ReactHotLoader>
</Provider>
);
and set up for async components
withAsyncComponents(app).then(({appWithAsyncComponents}) => {
ReactDOM.render(appWithAsyncComponents, rootEl);
});
About/index.jsx
. It has a bit more then you need to code split. And we will get back to it later.react-async-component
.import { createAsyncComponent } from 'react-async-component';
const AsyncAbout = createAsyncComponent({
name: 'about',
resolve: () => new Promise(resolve =>
require.ensure([
'./reducers/about'
], require => {
const component = require('./containers/About').default;
resolve({default: component});
}, 'about'))
});
export default AsyncAbout;
Server side setup is done within render-app.js
import {Provider as Redux} from 'react-redux';
import StaticRouter from 'react-router/StaticRouter';
const App = (store, req, routerContext) => (
<Redux store={store}>
<StaticRouter location={req.url} context={routerContext}>
<Root />
</StaticRouter>
</Redux>
);
// create router context
const routerContext = {};
// construct app component with async loaded chunks
const asyncSplit = await withAsyncComponents(App(store, req, routerContext));
// getting async component after code split loaded
const {appWithAsyncComponents} = asyncSplit;
// actual component to string
const body = renderToString(appWithAsyncComponents);
Html.jsx
. For client to understand what content we rendered and do same we need to pass down async chunk state{asyncComponents && asyncComponents.state ?
<script
dangerouslySetInnerHTML={{ __html: `
window.${asyncComponents.STATE_IDENTIFIER} = ${serialize(asyncComponents.state, {isJSON: true})};
`}} /> :
null}
And at this point you have SSR of React app using React router with Async Components.
Status.jsx
. All this component is doing really is just setting value on Static Router Context.componentWillMount() {
const { staticContext } = this.context.router;
if (staticContext) {
staticContext.status = this.props.code;
}
}
render-app.js
for SSR// checking is page is 404
let status = 200;
if (routerContext.status === '404') {
log('sending 404 for ', req.url);
status = 404;
} else {
log('router resolved to actual page');
}
// rendering result page
const page = renderPage(body, head, initialState, config, assets, asyncSplit);
res.status(status).send(page);
if (routerContext.url) {
// we got URL - this is a signal that redirect happened
res.status(301).setHeader('Location', routerContext.url);
/legal
you will see that Not Found is returned and server is giving us 404 as expected. /topic
will do 301 redirect. More details on how to use Switch
Coming back to About component. Full source
import { createAsyncComponent } from 'react-async-component';
import withAsyncReducers from '../store/withAsyncReducers';
const AsyncAbout = createAsyncComponent({
name: 'about',
resolve: () => new Promise(resolve =>
require.ensure([
'./reducers/about'
], require => {
const reducer = require('./reducers/about').default;
const component = require('./containers/About').default;
const withReducer = withAsyncReducers('about', reducer)(component);
resolve({default: withReducer});
}, 'about'))
});
export default AsyncAbout;
withAsyncReducers
is core function that we use here. It finds redux store from context and tries to register top-level reducer passed into it.
import {injectReducer} from './store';
//...
componentWillMount() {
this.attachReducers();
}
attachReducers() {
if (!reducer || !name) { return; }
injectReducer(this.store, `${name}`, reducer, force);
}
This may not be ideal for some scenarios and should be used with caution. Main risk is that some actions that happen before reducer is loaded and registered would be tracked. In case you need track of those you might look into more complex and robust solutions like redux-persist
injectReducer
is a function that is responsible for
dummyReducer
function that will be later replaced with real one.const initialReducers = createAsyncReducers({}, Object.keys(initialState));
// ... setting dummy
persist.forEach(key => {
if (!{}.hasOwnProperty.call(allReducers, key)) {
allReducers[key] = dummyReducer;
}
});
//... replacing dummy
if (!force && has(store.asyncReducers, name)) {
const r = get(store.asyncReducers, name);
if (r === dummyReducer) { return; }
}
core
folder and stuff.