Skip to content

Commit 018d4dd

Browse files
committed
website build
1 parent ebccd45 commit 018d4dd

File tree

1 file changed

+64
-16
lines changed

1 file changed

+64
-16
lines changed

_posts/2015-12-12-isomorphic-react-in-real-life.md

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ published: true
88
categories: react, redux, isomorphic
99
---
1010

11-
This post is not a tutorial. There are enough of them on the Internet. It is always more interesting to look at a real production app. A lot of tutorials and boilerplates show how to write isomorphic ReactJS applications, but they do not cover a lot of real-life problems we've faced in production (styling, data-fetching, vendor-prefixes, configutation etc). So we decided to share our experience at github and develop the app in a [public repo](https://github.com/webbylab/itsquiz-wall). The post describes all the issues and howto to deal with them.
11+
This post is not a tutorial. There are enough of them on the Internet. It is always more interesting to look at a real production app. A lot of tutorials and boilerplates show how to write isomorphic ReactJS applications, but they do not cover a lot of real-life problems we've faced in production (styling, data-fetching, vendor-prefixes, configutation etc). So we decided to share our experience at github and develop the app in a [public repo](https://github.com/webbylab/itsquiz-wall). The post describes all the issues and howto to deal with them.
1212

1313
<!--more-->
1414

@@ -231,6 +231,27 @@ You can find whole [production config on github](https://github.com/WebbyLab/its
231231

232232
Alternative solution is to create a plugin for babel that will just return "{}" on the server. I am not sure that it possible to do. If you can create babel-stub-plugin - it will be awesome.
233233

234+
**UPDATE: We've switched to the alternative solution after migrating to Babel 6**
235+
236+
We use [babel-plugin-transform-require-ignore](https://www.npmjs.com/package/babel-plugin-transform-require-ignore) plugin for Babel 6. Special thanks to @morlay (Morlay Null) for the plugin.
237+
238+
All you need is to configure file extentions that should be ignored by babel in [.babelrc](https://github.com/WebbyLab/itsquiz-wall/blob/master/.babelrc)
239+
240+
241+
```javascript
242+
"env": {
243+
"node": {
244+
"plugins": [
245+
[
246+
"babel-plugin-transform-require-ignore", { "extensions": [".less", ".css"] }
247+
]
248+
]
249+
}
250+
}
251+
```
252+
253+
and set environment variable BABEL_ENV='node' before starting your app. So, you can start your app like that [`cross-env BABEL_ENV='node' nodemon server/runner.js`](https://github.com/WebbyLab/itsquiz-wall/blob/7fb861ca09a4759b1b3622ab69d0cd610303da8e/package.json#L14).
254+
234255
#### Pain #3: Material UI uses vendor prefixing based on browser DOM
235256

236257
Ok. Let's go further. We managed our styles. And it seems that the problems with styles are solved. We use [Material UI](http://material-ui.com/) components library for our UI and we like it. But the problem with it is that it uses the same approach to vendor autoprefixing as Radium.
@@ -414,30 +435,54 @@ In browser the wrapper component will call action creators in **componentDidMoun
414435

415436
*IMPORTANT: componentWillMount is not suitable for this because it will be invoked on the client and server. componentDidMount will be invoked only on the client.*
416437

417-
On the server we do this in a different way. We have a function ["fetchComponentsData"](https://github.com/WebbyLab/itsquiz-wall/blob/master/server/utils.js#L4) which takes an array of components you are going to render and calls static method "fetchData" on each. One important thing is a usage of promises. We use promises to postpone rendering until the required data is fetched and saved to the redux store.
438+
On the server we do this in a different way. We have a function ["fetchComponentsData"](https://github.com/WebbyLab/itsquiz-wall/blob/master/server/utils.js#L12) which takes an array of components you are going to render and calls static method "fetchData" on each. One important thing is a usage of promises. We use promises to postpone rendering until the required data is fetched and saved to the redux store.
418439

419440
"connectDataFetchers" is extremely simple:
420441

421442
```javascript
422-
import React from 'react';
423-
import Promise from 'bluebird';
443+
444+
import React from 'react';
445+
446+
let IS_FIRST_MOUNT_AFTER_LOAD = true;
424447

425448
export default function connectDataFetchers(Component, actionCreators) {
426449
return class DataFetchersWrapper extends React.Component {
427-
static fetchData(dispatch, params = {}, query = {}) {
450+
static fetchData({ dispatch, params = {}, query = {} }) {
428451
return Promise.all(
429-
actionCreators.map( actionCreator =>
430-
dispatch( actionCreator(params, query) )
431-
)
452+
actionCreators.map(actionCreator => dispatch(actionCreator({ params, query })))
432453
);
433454
}
434455

435456
componentDidMount() {
436-
DataFetchersWrapper.fetchData(
437-
this.props.dispatch,
438-
this.props.params,
439-
this.props.location.query
440-
);
457+
// If the component is mounted first time
458+
// do no fetch data as it is already fetched on the server.
459+
if (!IS_FIRST_MOUNT_AFTER_LOAD) {
460+
this._fetchDataOnClient();
461+
}
462+
463+
IS_FIRST_MOUNT_AFTER_LOAD = false;
464+
}
465+
466+
componentDidUpdate(prevProps) {
467+
// Refetch data if url was changed but the component is the same
468+
469+
const { location } = this.props;
470+
const { location: prevLocation } = prevProps;
471+
472+
const isUrlChanged = (location.pathname !== prevLocation.pathname)
473+
|| (location.search !== prevLocation.search);
474+
475+
if (isUrlChanged) {
476+
this._fetchDataOnClient();
477+
}
478+
}
479+
480+
_fetchDataOnClient() {
481+
DataFetchersWrapper.fetchData({
482+
dispatch : this.props.dispatch,
483+
params : this.props.params,
484+
query : this.props.location.query
485+
});
441486
}
442487

443488
render() {
@@ -450,6 +495,8 @@ export default function connectDataFetchers(Component, actionCreators) {
450495

451496
```
452497

498+
[Production version](https://github.com/WebbyLab/itsquiz-wall/blob/master/shared/lib/connectDataFetchers.jsx) a little bit longer. It passes locale information and has propTypes described.
499+
453500
So, on server our code looks like:
454501

455502
```javascript
@@ -494,13 +541,15 @@ We load out config on the server and return it in index.html
494541
<div id="react-view">${componentHTML}</div>
495542

496543
<script type="application/javascript">
497-
window.__CONFIG__ = ${JSON.stringify(config)};
498-
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
544+
window.__CONFIG__ = ${serializeJs(config, { isJSON: true })};
545+
window.__INITIAL_STATE__ = ${serializeJs(initialState, { isJSON: true })};
499546
</script>
500547

501548
<script type="application/javascript" src="${config.staticUrl}/static/build/main.js"></script>
502549
```
503550

551+
*IMPORTANT: Serialization of initialState with JSON.stringify will make your application vulnerable to XSS attacks!!! You should use [serialize-javascript](https://github.com/yahoo/serialize-javascript) instead!*
552+
504553

505554
But depending on a global variable in your code is not a good idea. So, we create "config.js" module that just exports global variable. And our code depends on "config.js" module. Our "config.js" should be isomorphic, so on the server we just require json file.
506555

@@ -538,4 +587,3 @@ Very few tutorials explain how to deal with localization in a regular SPA. No tu
538587
* Server specific code - 139 SLOC (5.4%)
539588

540589
While all codebase growths, isomorphic part of the code growths the most. So, code reuse rate will become higher with time.
541-

0 commit comments

Comments
 (0)