Skip to content

Commit e08b4cb

Browse files
author
Dmytro Lebedynskyi
committed
readme
1 parent a854f47 commit e08b4cb

File tree

5 files changed

+314
-3
lines changed

5 files changed

+314
-3
lines changed

README.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,219 @@
11
# react-playground
22
React application with React Router v4, async components etc
3+
4+
## What is this for?
5+
Demo app to show react router v4 beta with async component, code split and async reducer registration.
6+
7+
Bases on excellent work of great people
8+
- [Dan Abramov on inject reducers](http://stackoverflow.com/questions/32968016/how-to-dynamically-load-reducers-for-code-splitting-in-a-redux-application/33045558)
9+
- [React async component](https://github.com/ctrlplusb/react-async-component)
10+
- [Create react app scripts](https://github.com/facebookincubator/create-react-app/tree/master/packages/react-scripts)
11+
- [React Universally](https://github.com/ctrlplusb/react-universally)
12+
- [React Router v4](https://github.com/ReactTraining/react-router/tree/v4)
13+
- [webpack 2](https://github.com/webpack/webpack/)
14+
15+
16+
## Client Side setup
17+
18+
1. Open `Root.jsx`. There you can see that we have pretty basic RR4 routing.
19+
```jsx
20+
<ul>
21+
<li> <Link to="/">Home</Link> </li>
22+
<li> <Link to="/about">About</Link> </li>
23+
<li> <Link to="/topics">Topics</Link> </li>
24+
<li> <Link to="/legal">Legal</Link> </li>
25+
</ul>
26+
```
27+
28+
2. Open `client-entry.js`. Here we have Routing set up for React Router and Redux store.
29+
```jsx
30+
// create render function
31+
const render = RootEl => {
32+
const app = (
33+
<Provider store={store}>
34+
<ReactHotLoader>
35+
<Router><RootEl /></Router>
36+
</ReactHotLoader>
37+
</Provider>
38+
);
39+
```
40+
41+
and set up for async components
42+
```jsx
43+
withAsyncComponents(app).then(({appWithAsyncComponents}) => {
44+
ReactDOM.render(appWithAsyncComponents, rootEl);
45+
});
46+
```
47+
48+
3. Open `About/index.jsx`. It has a bit more then you need to code split. And we will get back to it later.
49+
Below is full code you need to code split of components using RR4 and `react-async-component`.
50+
51+
```jsx
52+
import { createAsyncComponent } from 'react-async-component';
53+
54+
const AsyncAbout = createAsyncComponent({
55+
name: 'about',
56+
resolve: () => new Promise(resolve =>
57+
require.ensure([
58+
'./reducers/about'
59+
], require => {
60+
const component = require('./containers/About').default;
61+
resolve({default: component});
62+
}, 'about'))
63+
});
64+
65+
export default AsyncAbout;
66+
```
67+
68+
### Server Side setup
69+
70+
Server side setup is done within `render-app.js`
71+
1. Redux Store and Router
72+
```jsx
73+
import {Provider as Redux} from 'react-redux';
74+
import StaticRouter from 'react-router/StaticRouter';
75+
76+
const App = (store, req, routerContext) => (
77+
<Redux store={store}>
78+
<StaticRouter location={req.url} context={routerContext}>
79+
<Root />
80+
</StaticRouter>
81+
</Redux>
82+
);
83+
84+
```
85+
86+
2. rendering app with Router context and async components
87+
```jsx
88+
// create router context
89+
const routerContext = {};
90+
// construct app component with async loaded chunks
91+
const asyncSplit = await withAsyncComponents(App(store, req, routerContext));
92+
// getting async component after code split loaded
93+
const {appWithAsyncComponents} = asyncSplit;
94+
// actual component to string
95+
const body = renderToString(appWithAsyncComponents);
96+
```
97+
98+
3. Rendering actual page is done in `Html.jsx`. For client to understand what content we rendered and do same we need to pass down async chunk state
99+
```jsx
100+
{asyncComponents && asyncComponents.state ?
101+
<script
102+
dangerouslySetInnerHTML={{ __html: `
103+
window.${asyncComponents.STATE_IDENTIFIER} = ${serialize(asyncComponents.state, {isJSON: true})};
104+
`}} /> :
105+
null}
106+
```
107+
108+
And at this point you have SSR of React app using React router with Async Components.
109+
110+
## Handling 404 and redirects with React Router
111+
112+
1. View `Status.jsx`. All this component is doing really is just setting value on Static Router Context.
113+
114+
```jsx
115+
componentWillMount() {
116+
const { staticContext } = this.context.router;
117+
if (staticContext) {
118+
staticContext.status = this.props.code;
119+
}
120+
}
121+
```
122+
123+
2. then we can handle this value in `render-app.js` for SSR
124+
125+
```
126+
// checking is page is 404
127+
let status = 200;
128+
if (routerContext.status === '404') {
129+
log('sending 404 for ', req.url);
130+
status = 404;
131+
} else {
132+
log('router resolved to actual page');
133+
}
134+
135+
// rendering result page
136+
const page = renderPage(body, head, initialState, config, assets, asyncSplit);
137+
res.status(status).send(page);
138+
```
139+
3. This is basically same exact thing RR 4 is doing for redirect.
140+
141+
```jsx
142+
if (routerContext.url) {
143+
// we got URL - this is a signal that redirect happened
144+
res.status(301).setHeader('Location', routerContext.url);
145+
```
146+
147+
4. If you try to navigate to `/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](https://reacttraining.com/react-router/examples/ambiguous-matches)
148+
149+
## Enabling async reducers
150+
151+
Coming back to About component. Full source
152+
153+
```jsx
154+
import { createAsyncComponent } from 'react-async-component';
155+
import withAsyncReducers from '../store/withAsyncReducers';
156+
157+
const AsyncAbout = createAsyncComponent({
158+
name: 'about',
159+
resolve: () => new Promise(resolve =>
160+
require.ensure([
161+
'./reducers/about'
162+
], require => {
163+
const reducer = require('./reducers/about').default;
164+
const component = require('./containers/About').default;
165+
const withReducer = withAsyncReducers('about', reducer)(component);
166+
resolve({default: withReducer});
167+
}, 'about'))
168+
});
169+
170+
export default AsyncAbout;
171+
```
172+
173+
`withAsyncReducers` is core function that we use here. It finds redux store from context and tries to register **top-level reducer** passed into it.
174+
175+
```jsx
176+
177+
import {injectReducer} from './store';
178+
179+
//...
180+
181+
componentWillMount() {
182+
this.attachReducers();
183+
}
184+
185+
attachReducers() {
186+
if (!reducer || !name) { return; }
187+
injectReducer(this.store, `${name}`, reducer, force);
188+
}
189+
190+
```
191+
192+
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](https://github.com/rt2zz/redux-persist)
193+
194+
`injectReducer` is a function that is responsible for
195+
- checking is async reducer was already injected into async registry
196+
- Creating new redux function and replacing state function with it.
197+
198+
###Caveats using async reducers###
199+
1. **SSR.** we don't won't to loose initialState that was sent from server. Redux currently is checking that once we create store on client and will remove all state that does not have reducers yet. _And we don't have it since we have not loaded our components yet_. To fix that we use `dummyReducer` function that will be later replaced with real one.
200+
201+
```
202+
const initialReducers = createAsyncReducers({}, Object.keys(initialState));
203+
// ... setting dummy
204+
persist.forEach(key => {
205+
if (!{}.hasOwnProperty.call(allReducers, key)) {
206+
allReducers[key] = dummyReducer;
207+
}
208+
});
209+
//... replacing dummy
210+
if (!force && has(store.asyncReducers, name)) {
211+
const r = get(store.asyncReducers, name);
212+
if (r === dummyReducer) { return; }
213+
}
214+
215+
```
216+
2. All shared reducers should be registered outside of code split. See `core` folder and stuff.
217+
218+
219+

build/server.js

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10184,6 +10184,10 @@ var _react = __webpack_require__(10);
1018410184

1018510185
var _react2 = _interopRequireDefault(_react);
1018610186

10187+
var _Redirect = __webpack_require__(341);
10188+
10189+
var _Redirect2 = _interopRequireDefault(_Redirect);
10190+
1018710191
var _Route = __webpack_require__(309);
1018810192

1018910193
var _Route2 = _interopRequireDefault(_Route);
@@ -10273,6 +10277,7 @@ exports.default = () => _react2.default.createElement(
1027310277
_Switch2.default,
1027410278
null,
1027510279
_react2.default.createElement(_Route2.default, { exact: true, path: '/', component: _home2.default }),
10280+
_react2.default.createElement(_Route2.default, { path: '/topics', render: () => _react2.default.createElement(_Redirect2.default, { to: '/' }) }),
1027610281
_react2.default.createElement(_Route2.default, { path: '/about', component: _about2.default }),
1027710282
_react2.default.createElement(_Route2.default, { component: NotFound })
1027810283
)
@@ -10422,7 +10427,10 @@ var _core2 = _interopRequireDefault(_core);
1042210427

1042310428
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
1042410429

10425-
exports.default = { core: _core2.default };
10430+
exports.default = {
10431+
core: _core2.default
10432+
// rest of shared reducers like forms etc
10433+
};
1042610434

1042710435
/***/ }),
1042810436
/* 158 */
@@ -25067,6 +25075,87 @@ module.exports = require("webpack");
2506725075
module.exports = __webpack_require__(149);
2506825076

2506925077

25078+
/***/ }),
25079+
/* 339 */,
25080+
/* 340 */,
25081+
/* 341 */
25082+
/***/ (function(module, exports, __webpack_require__) {
25083+
25084+
"use strict";
25085+
25086+
25087+
exports.__esModule = true;
25088+
25089+
var _react = __webpack_require__(10);
25090+
25091+
var _react2 = _interopRequireDefault(_react);
25092+
25093+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
25094+
25095+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
25096+
25097+
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
25098+
25099+
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
25100+
25101+
/**
25102+
* The public API for updating the location programatically
25103+
* with a component.
25104+
*/
25105+
var Redirect = function (_React$Component) {
25106+
_inherits(Redirect, _React$Component);
25107+
25108+
function Redirect() {
25109+
_classCallCheck(this, Redirect);
25110+
25111+
return _possibleConstructorReturn(this, _React$Component.apply(this, arguments));
25112+
}
25113+
25114+
Redirect.prototype.componentWillMount = function componentWillMount() {
25115+
if (this.context.router.staticContext) this.perform();
25116+
};
25117+
25118+
Redirect.prototype.componentDidMount = function componentDidMount() {
25119+
if (!this.context.router.staticContext) this.perform();
25120+
};
25121+
25122+
Redirect.prototype.perform = function perform() {
25123+
var router = this.context.router;
25124+
var _props = this.props,
25125+
push = _props.push,
25126+
to = _props.to;
25127+
25128+
25129+
if (push) {
25130+
router.push(to);
25131+
} else {
25132+
router.replace(to);
25133+
}
25134+
};
25135+
25136+
Redirect.prototype.render = function render() {
25137+
return null;
25138+
};
25139+
25140+
return Redirect;
25141+
}(_react2.default.Component);
25142+
25143+
Redirect.contextTypes = {
25144+
router: _react.PropTypes.shape({
25145+
push: _react.PropTypes.func.isRequired,
25146+
replace: _react.PropTypes.func.isRequired,
25147+
staticContext: _react.PropTypes.object
25148+
}).isRequired
25149+
};
25150+
Redirect.propTypes = {
25151+
push: _react.PropTypes.bool,
25152+
to: _react.PropTypes.oneOfType([_react.PropTypes.string, _react.PropTypes.object])
25153+
};
25154+
Redirect.defaultProps = {
25155+
push: false
25156+
};
25157+
exports.default = Redirect;
25158+
2507025159
/***/ })
2507125160
/******/ ]);
2507225161
//# sourceMappingURL=server.js.map

build/server.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/Root.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import Redirect from 'react-router/Redirect';
23
import Route from 'react-router-dom/Route';
34
import Link from 'react-router-dom/Link';
45
import Switch from 'react-router-dom/Switch';
@@ -24,6 +25,7 @@ export default() => (
2425
<hr />
2526
<Switch>
2627
<Route exact path="/" component={Home} />
28+
<Route path="/topics" render={() => <Redirect to="/" />} />
2729
<Route path="/about" component={About} />
2830
<Route component={NotFound} />
2931
</Switch>

src/app/core/reducers/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import core from './core';
22

3-
export default {core};
3+
export default {
4+
core
5+
// rest of shared reducers like forms etc
6+
};

0 commit comments

Comments
 (0)