Skip to content

Commit

Permalink
Merge branch 'master' into forms
Browse files Browse the repository at this point in the history
* master: (21 commits)
  improved syntax for including styles
  fixed #77
  added apology for #60 in the README
  linted
  url-loader fix for images
  switched back to file-loader to avoid error from requireServerImage() fixes #39
  Works on mac now
  Added better-npm-run to allow setting env-vars on all platforms
  fixed cat pic on server render
  minor grammar and js style tweak to README
  added url-loader to production webpack, too
  merged PR #67. lint-corrected merge. added obligatory cat pic. changed to use url-loader to encode small images.
  made pass lint
  incorporated PR #67 and added some comments to promise chaining in createTransitionHook()
  util import should be in single line.
  correct the , to , sorry for my keyboard.
  remove a space :P
  update README.
  correct the util error :P
  add the image server render example to the logo.png.
  ...
  • Loading branch information
Erik Rasmussen committed Jul 31, 2015
2 parents 43125f0 + 44f0d5d commit d78fec3
Show file tree
Hide file tree
Showing 18 changed files with 190 additions and 86 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
webpack/*
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules/
dist/
*.iml
webpack-stats.json
npm-debug.log
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ npm install
npm run dev
```

Unfortunately, you'll have to run this twice, because the build depends on a file, `webpack-stats.json` generated by the build itself. [If you can solve this, please do.](https://github.com/erikras/react-redux-universal-hot-example/issues/60)

## Building and Running Production Server

```
Expand Down Expand Up @@ -81,21 +83,30 @@ This is where the meat of your server-side application goes. It doesn't have to

To understand how the data and action bindings get into the components – there's only one, `InfoBar`, in this example – I'm going to refer to you to the [Redux](https://github.com/gaearon/redux) library. The only innovation I've made is to package the component and its wrapper in the same js file. This is to encapsulate the fact that the component is bound to the `redux` actions and state. The component using `InfoBar` needn't know or care if `InfoBar` uses the `redux` data or not.

#### Images

Now it's possible to render the image both on client and server. Please refer to issue [#39](https://github.com/erikras/react-redux-universal-hot-example/issues/39) for more detail discussion, the usage would be like below (super easy):

```javascript
let logoImage = '';
if(__CLIENT__) {
logoImage = require('./logo.png');
} else {
logoImage = requireServerImage('./logo.png');
}
```

#### Styles

This project uses [local styles](https://medium.com/seek-ui-engineering/the-end-of-global-css-90d2a4a06284) using [css-loader](https://github.com/webpack/css-loader). The way it works is that you import your stylesheet at the top of the class with your React Component, and then you use the classnames returned from that import. Like so:

```javascript
const styles = (function getStyle() {
const stats = require('../../webpack-stats.json');
if (__CLIENT__) {
return require('./App.scss');
}
return stats.css.modules[path.join(__dirname, './App.scss')];
})();
const styles = __CLIENT__ ?
require('./App.scss') :
requireServerCss(require.resolve('./App.scss'));
```

That's a little ugly, I know, but what it allows is very powerful.
Then you set the `className` of your element ot match one of the CSS classes in your SCSS file, and you're good to go!

```jsx
<div className={styles.mySection}> ... </div>
Expand Down
31 changes: 28 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,37 @@
],
"main": "babel.server.js",
"scripts": {
"start": "NODE_PATH=\"./src\" NODE_ENV=\"production\" PORT=\"8080\" node ./babel.server",
"start": "node ./node_modules/better-npm-run start",
"build": "node ./node_modules/webpack/bin/webpack.js --verbose --colors --display-error-details --config webpack/prod.config.js",
"lint": "node ./node_modules/eslint/bin/eslint.js -c .eslintrc src",
"start-dev": "NODE_PATH=\"./src\" NODE_ENV=\"development\" node ./babel.server",
"watch-client": "UV_THREADPOOL_SIZE=100 NODE_PATH=\"./src\" node webpack/webpack-dev-server.js",
"start-dev": "node ./node_modules/better-npm-run start-dev",
"watch-client": "node ./node_modules/better-npm-run watch-client",
"dev": "node ./node_modules/concurrently/src/main.js --kill-others \"npm run watch-client\" \"npm run start-dev\""
},
"betterScripts": {
"start": {
"command": "node ./babel.server.js",
"env": {
"NODE_PATH": "./src",
"NODE_ENV": "production",
"PORT": 8080
}
},
"start-dev": {
"command": "node ./babel.server.js",
"env": {
"NODE_PATH": "./src",
"NODE_ENV": "development"
}
},
"watch-client": {
"command": "node webpack/webpack-dev-server.js",
"env": {
"UV_THREADPOOL_SIZE": 100,
"NODE_PATH": "./src"
}
}
},
"dependencies": {
"babel": "5.4.7",
"babel-plugin-typecheck": "0.0.3",
Expand Down Expand Up @@ -61,6 +85,7 @@
"babel-eslint": "^3.1.18",
"babel-loader": "5.1.3",
"babel-runtime": "5.4.7",
"better-npm-run": "0.0.1",
"clean-webpack-plugin": "^0.1.3",
"concurrently": "0.1.1",
"css-loader": "^0.15.1",
Expand Down
11 changes: 2 additions & 9 deletions src/components/InfoBar.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import path from 'path';
import React, {Component, PropTypes} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as infoActions from '../actions/infoActions';
import {relativeToSrc} from '../util';
import {requireServerCss} from '../util';

const styles = (function getStyle() {
if (__CLIENT__) {
return require('./InfoBar.scss');
}
const stats = require('../../webpack-stats.json');
return stats.css.modules[relativeToSrc(path.join(__dirname, './InfoBar.scss'))];
})();
const styles = __CLIENT__ ? require('./InfoBar.scss') : requireServerCss(require.resolve('./InfoBar.scss'));

class InfoBar extends Component {
static propTypes = {
Expand Down
13 changes: 6 additions & 7 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,12 @@ app.use((req, res) => {
} else {
universalRouter(location, undefined, store)
.then(({component, transition, isRedirect}) => {

if (isRedirect) {
res.redirect(transition.redirectInfo.pathname);
return;
}
res.send('<!doctype html>\n' +
React.renderToString(<Html webpackStats={webpackStats} component={component} store={store}/>));
if (isRedirect) {
res.redirect(transition.redirectInfo.pathname);
return;
}
res.send('<!doctype html>\n' +
React.renderToString(<Html webpackStats={webpackStats} component={component} store={store}/>));
})
.catch((error) => {
console.error('ROUTER ERROR:', pretty.render(error));
Expand Down
13 changes: 7 additions & 6 deletions src/universalRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import routes from './views/routes';
import { Provider } from 'react-redux';

const getFetchData = (component={}) => {
return component.DecoratedComponent ?
return component.DecoratedComponent ?
getFetchData(component.DecoratedComponent) :
component.fetchData;
};

export function createTransitionHook(store) {
return (nextState, transition, callback) => {
Promise.all(nextState.branch
.map(route => route.component)
.filter(getFetchData(component))
.map(getFetchData)
.map(fetchData => fetchData(store, nextState.params))
const promises = nextState.branch
.map(route => route.component) // pull out individual route components
.filter((component) => getFetchData(component)) // only look at ones with a static fetchData()
.map(getFetchData) // pull out fetch data methods
.map(fetchData => fetchData(store, nextState.params)); // call fetch data methods and save promises
Promise.all(promises)
.then(() => {
callback(); // can't just pass callback to then() because callback assumes first param is error
}, (error) => {
Expand Down
40 changes: 36 additions & 4 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,39 @@
import path from 'path';
const readStats = () => {
// don't cache the `webpack-stats.json` on dev so we read the file on each request.
// on production, use simple `require` to cache the file
const stats = require('../webpack-stats.json');
if (__DEVELOPMENT__) {
delete require.cache[require.resolve('../webpack-stats.json')];
}
return stats;
}

export function requireServerCss(cssPath) {
if (__CLIENT__) {
throw new Error('image-resolver called on browser');
}
return readStats().css.modules[cssPath.slice(__dirname.length)];
}

export function requireServerImage(imagePath) {
if (!imagePath) {
return '';
}
if (__CLIENT__) {
throw new Error('server-side only resolver called on client');
}
const images = readStats().images;
if (!images) {
return '';
}

// Find the correct image
const regex = new RegExp(`${imagePath}$`);
const image = images.find(img => regex.test(img.original));

const pathToSrc = path.resolve(__dirname, '.');
// Serve image.
if (image) return image.compiled;

export function relativeToSrc(value) {
return value.slice(pathToSrc.length);
// Serve a not-found asset maybe?
return '';
}
27 changes: 26 additions & 1 deletion src/views/About.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import React, {Component} from 'react';
import MiniInfoBar from '../components/MiniInfoBar';
import {requireServerImage} from '../util';

const kitten = __CLIENT__ ? require('./kitten.jpg') : requireServerImage('./kitten.jpg');

export default class About extends Component {
state = {
showKitten: false
}

handleToggleKitten() {
this.setState({showKitten: !this.state.showKitten});
}

render() {
const {showKitten} = this.state;
return (
<div>
<div className="container">
Expand All @@ -18,9 +30,22 @@ export default class About extends Component {
<h3>Mini Bar <span style={{color: '#aaa'}}>(not that kind)</span></h3>

<p>Hey! You found the mini info bar! The following component is display-only. Note that it shows the same
time as the info bar.</p>
time as the info bar.</p>

<MiniInfoBar/>

<h3>Images</h3>

<p>
Psst! Would you like to see a kitten?

<button className={'btn btn-' + (showKitten ? 'danger' : 'success')}
style={{marginLeft: 50}}
onClick={::this.handleToggleKitten}>
{showKitten ? 'No! Take it away!' : 'Yes! Please!'}</button>
</p>

{showKitten && <div><img src={kitten}/></div>}
</div>
</div>
);
Expand Down
11 changes: 3 additions & 8 deletions src/views/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,9 @@ import * as authActions from '../actions/authActions';
import {load as loadAuth} from '../actions/authActions';
import InfoBar from '../components/InfoBar';
import {createTransitionHook} from '../universalRouter';
import {relativeToSrc} from '../util';
import {requireServerCss} from '../util';

const styles = (function getStyle() {
if (__CLIENT__) {
return require('./App.scss');
}
const stats = require('../../webpack-stats.json');
return stats.css.modules[relativeToSrc(path.resolve(__dirname, './App.scss'))];
})();
const styles = __CLIENT__ ? require('./App.scss') : requireServerCss(require.resolve('./App.scss'));

class App extends Component {
static propTypes = {
Expand All @@ -43,6 +37,7 @@ class App extends Component {

render() {
const {user} = this.props;
debugger;
return (
<div className={styles.app}>
<nav className="navbar navbar-default navbar-fixed-top">
Expand Down
28 changes: 18 additions & 10 deletions src/views/Home.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import React, {Component} from 'react';
import path from 'path';
import CounterButton from '../components/CounterButton';
import GithubButton from '../components/GithubButton';
import {relativeToSrc} from '../util';
import {requireServerCss, requireServerImage} from '../util';

const styles = (function getStyle() {
if (__CLIENT__) {
return require('./Home.scss');
}
const stats = require('../../webpack-stats.json');
return stats.css.modules[relativeToSrc(path.resolve(__dirname, './Home.scss'))];
})();
const styles = __CLIENT__ ? require('./Home.scss') : requireServerCss(require.resolve('./Home.scss'));

// require the logo image both from client and server
let logoImage = '';
if (__CLIENT__) {
logoImage = require('./logo.png');
} else {
logoImage = requireServerImage('./logo.png');
}

export default class Home extends Component {
render() {
return (
<div>
<div className={styles.masthead}>
<div className="container">
<div className={styles.logo}/>
<div className={styles.logo}>
<p>
<img src={logoImage}/>
</p>
</div>
<h1>React Redux Example</h1>

<h2>All the modern best practices in one example.</h2>
Expand Down Expand Up @@ -84,14 +89,17 @@ export default class Home extends Component {
</ul>

<h3>From the author</h3>

<p>
I cobbled this together from a wide variety of similar "starter" repositories. As I post this in June 2015,
all of these libraries are right at the bleeding edge of web development. They may fall out of fashion as
quickly as they have come into it, but I personally believe that this stack is the future of web development
and will survive for several years. I'm building my new projects like this, and I recommend that you do,
too.
</p>

<p>Thanks for taking the time to check this out.</p>

<p>– Erik Rasmussen</p>
</div>
</div>
Expand Down
11 changes: 9 additions & 2 deletions src/views/Home.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ $humility: #777;
.logo {
$size: 200px;
margin: auto;
background: url('./logo.png') no-repeat center center;
background-size: 75%;
height: $size;
width: $size;
border-radius: $size / 2;
border: 1px solid $cyan;
box-shadow: inset 0 0 10px $cyan;
vertical-align: middle;
p {
line-height: $size;
margin: 0px;
}
img {
width: 75%;
margin: auto;
}
}
h1 {
color: $cyan;
Expand Down
11 changes: 2 additions & 9 deletions src/views/Login.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import path from 'path';
import React, {Component, PropTypes} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {isLoaded as isAuthLoaded} from '../reducers/auth';
import * as authActions from '../actions/authActions';
import {load as loadAuth} from '../actions/authActions';
import {relativeToSrc} from '../util';
import {requireServerCss} from '../util';

const styles = (function getStyle() {
if (__CLIENT__) {
return require('./Login.scss');
}
const stats = require('../../webpack-stats.json');
return stats.css.modules[relativeToSrc(path.join(__dirname, './Login.scss'))];
})();
const styles = __CLIENT__ ? require('./Login.scss') : requireServerCss(require.resolve('./Login.scss'));

class Login extends Component {
static propTypes = {
Expand Down
Loading

0 comments on commit d78fec3

Please sign in to comment.