Skip to content

Commit 2c904e0

Browse files
committed
2 parents 24e1755 + b77e7f6 commit 2c904e0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1026
-660
lines changed

.eslintrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"block-scoped-var": 0,
1414
// Temporarily disabled for test/* until babel/babel-eslint#33 is resolved
1515
"padded-blocks": 0,
16+
"comma-dangle": 0, // not sure why airbnb turned this on. gross!
17+
"indent": [2, 2, {"SwitchCase": 1}],
1618
"no-console": 0
1719
},
1820
"plugins": [
@@ -23,6 +25,7 @@
2325
"__CLIENT__": true,
2426
"__SERVER__": true,
2527
"__DISABLE_SSR__": true,
26-
"__DEVTOOLS__": true
28+
"__DEVTOOLS__": true,
29+
"webpackIsomorphicTools": true
2730
}
2831
}

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
[![Demo on Heroku](https://img.shields.io/badge/demo-heroku-lightgrey.png)](https://react-redux.herokuapp.com)
66
[![Dependency Status](https://david-dm.org/erikras/react-redux-universal-hot-example.svg)](https://david-dm.org/erikras/react-redux-universal-hot-example)
77
[![devDependency Status](https://david-dm.org/erikras/react-redux-universal-hot-example/dev-status.svg)](https://david-dm.org/erikras/react-redux-universal-hot-example#info=devDependencies)
8+
[![PayPal donate button](http://img.shields.io/paypal/donate.png?color=yellowgreen)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=E2LK57ZQ9YRMN)
89

910
This is a starter boiler plate app I've put together using the following technologies:
1011

@@ -22,6 +23,7 @@ This is a starter boiler plate app I've put together using the following technol
2223
* [redux-form](https://github.com/erikras/redux-form) to manage form state in Redux
2324
* [lru-memoize](https://github.com/erikras/lru-memoize) to speed up form validation
2425
* [style-loader](https://github.com/webpack/style-loader) and [sass-loader](https://github.com/jtangelder/sass-loader) to allow import of stylesheets
26+
* [react-document-meta](https://github.com/kodyl/react-document-meta) to manage title and meta tag information on both server and client
2527
* [webpack-isomorphic-tools](https://github.com/halt-hammerzeit/webpack-isomorphic-tools) to allow require() work for statics both on client and server
2628
* [mocha](https://mochajs.org/) to allow writing unit tests for the project.
2729

@@ -48,7 +50,7 @@ npm run start
4850

4951
## Demo
5052

51-
A demonstration of this app can be see [running on heroku](https://react-redux.herokuapp.com), which is a deployment of the [heroku branch](https://github.com/erikras/react-redux-universal-hot-example/tree/heroku).
53+
A demonstration of this app can be seen [running on heroku](https://react-redux.herokuapp.com), which is a deployment of the [heroku branch](https://github.com/erikras/react-redux-universal-hot-example/tree/heroku).
5254

5355
## Explanation
5456

@@ -79,6 +81,10 @@ The middleware, [`clientMiddleware.js`](https://github.com/erikras/react-redux-u
7981
1. To allow the action creators access to the client API facade. Remember this is the same on both the client and the server, and cannot simply be `import`ed because it holds the cookie needed to maintain session on server-to-server requests.
8082
2. To allow some actions to pass a "promise generator", a function that takes the API client and returns a promise. Such actions require three action types, the `REQUEST` action that initiates the data loading, and a `SUCCESS` and `FAILURE` action that will be fired depending on the result of the promise. There are other ways to accomplish this, some discussed [here](https://github.com/gaearon/redux/issues/99), which you may prefer, but to the author of this example, the middleware way feels cleanest.
8183

84+
#### What the Duck?
85+
86+
[Ducks](https://github.com/erikras/react-redux-universal-hot-example/blob/master/docs/Ducks.md) are a Redux Style Proposal that I came up with to better isolate concerns within a Redux application. I encourage you to read the [Ducks Docs](https://github.com/erikras/react-redux-universal-hot-example/blob/master/docs/Ducks.md) and provide feedback.
87+
8288
#### API Server
8389

8490
This is where the meat of your server-side application goes. It doesn't have to be implemented in Node or Express at all. This is where you connect to your database and provide authentication and session management. In this example, it's just spitting out some json with the current time stamp.

bin/server.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22
require('../compiler'); // enables ES6 support
3-
3+
var path = require('path');
4+
var rootDir = path.resolve(__dirname, '..');
45
/**
56
* Define isomorphic constants.
67
*/
@@ -18,12 +19,11 @@ if (__DEVELOPMENT__) {
1819
}
1920
}
2021

21-
// alternatively, if you you can skip using this and instead use this:
22-
// (and webpack DefinePlugin for setting _client_ environment variable)
23-
// const picture = _client_ ? require('./image.png') : webpackIsomorphicTools.require('./image.png')
24-
var webpackConfiguration = require('../webpack/prod.config.js');
22+
// https://github.com/halt-hammerzeit/webpack-isomorphic-tools
2523
var WebpackIsomorphicTools = require('webpack-isomorphic-tools');
26-
global.webpackIsomorphicTools = new WebpackIsomorphicTools(webpackConfiguration, require('../webpack/webpack-isomorphic-tools'));
27-
global.webpackIsomorphicTools.register();
28-
29-
require('../src/server');
24+
global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools'))
25+
.development(__DEVELOPMENT__)
26+
.server(rootDir, function()
27+
{
28+
require('../src/server');
29+
});

docs/Ducks.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Ducks: Redux Reducer Bundles
2+
3+
<img src="duck.jpg" align="right"/>
4+
5+
I find as I am building my redux app, one piece of functionality at a time, I keep needing to add `{actionTypes, actions, reducer}` tuples for each use case. I have been keeping these in separate files and even separate folders, however 95% of the time, it's only one reducer/actions pair that ever needs their associated actions.
6+
7+
To me, it makes more sense for these components to be bundled together in an isolated module that is self contained, and can even be packaged easily into a library.
8+
9+
## The Proposal
10+
11+
### Example
12+
13+
```javascript
14+
// widgets.js
15+
16+
const LOAD = 'my-app/widgets/LOAD';
17+
const CREATE = 'my-app/widgets/CREATE';
18+
const UPDATE = 'my-app/widgets/UPDATE';
19+
const REMOVE = 'my-app/widgets/REMOVE';
20+
21+
export default function reducer(state = {}, action = {}) {
22+
switch (action.type) {
23+
// do reducer stuff
24+
default: return state;
25+
}
26+
}
27+
28+
export function loadWidgets() = {
29+
return { type: LOAD };
30+
}
31+
32+
export function createWidget(widget) = {
33+
return { type: CREATE, widget };
34+
}
35+
36+
export function updateWidget(widget) = {
37+
return { type: UPDATE, widget };
38+
}
39+
40+
export function removeWidget(widget) = {
41+
return { type: REMOVE, widget };
42+
}
43+
```
44+
### Rules
45+
46+
A module...
47+
48+
1. MUST `export default` a function called `reducer()`
49+
2. MUST `export` its action creators as functions
50+
3. MUST have action types in the form `npm-module-or-app/reducer/ACTION_TYPE`
51+
3. MAY export its action types as `UPPER_SNAKE_CASE`, if an external reducer needs to listen for them, or if it is a published reusable library
52+
53+
These same guidelines are recommended for `{actionType, action, reducer}` bundles that are shared as reusable Redux libraries.
54+
55+
### Name
56+
57+
Java has jars and beans. Ruby has gems. I suggest we call these reducer bundles "ducks", as in the last syllable of "redux".
58+
59+
### Usage
60+
61+
You can still do:
62+
63+
```javascript
64+
import { combineReducers } from 'redux';
65+
import * as reducers from './ducks/index';
66+
67+
const rootReducer = combineReducers(reducers);
68+
export default rootReducer;
69+
```
70+
71+
You can still do:
72+
73+
```javascript
74+
import * as widgetActions from './ducks/widgets';
75+
```
76+
...and it will only import the action creators, ready to be passed to `bindActionCreators()`.
77+
78+
There will be some times when you want to `export` something other than an action creator. That's okay, too. The rules don't say that you can *only* `export` action creators. When that happens, you'll just have to enumerate the action creators that you want. Not a big deal.
79+
80+
```javascript
81+
import {create, update, remove, increment} as widgetActions from './ducks/widgets';
82+
// ...
83+
bindActionCreators({create, update, remove, increment}, dispatch);
84+
```
85+
86+
### Implementation
87+
88+
The migration to this code structure was [painless](https://github.com/erikras/react-redux-universal-hot-example/commit/3fdf194683abb7c40f3cb7969fd1f8aa6a4f9c57), and I foresee it reducing much future development misery.
89+
90+
Please submit any feedback via an issue or a tweet to [@erikras](https://twitter.com/erikras). It will be much appreciated.
91+
92+
Happy coding!
93+
94+
-- Erik Rasmussen

docs/duck.jpg

7.14 KB
Loading

package.json

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "Example of an isomorphic (universal) webapp using react redux and hot reloading",
44
"author": "Erik Rasmussen <[email protected]> (http://github.com/erikras)",
55
"license": "MIT",
6-
"version": "0.0.3",
6+
"version": "0.0.4",
77
"repository": {
88
"type": "git",
99
"url": "https://github.com/erikras/react-redux-universal-hot-example"
@@ -57,64 +57,67 @@
5757
}
5858
},
5959
"dependencies": {
60-
"babel": "5.4.7",
61-
"babel-plugin-typecheck": "0.0.3",
60+
"babel": "5.8.21",
61+
"babel-plugin-typecheck": "1.2.0",
6262
"body-parser": "^1.13.2",
6363
"compression": "^1.5.0",
6464
"express": "^4.13.0",
6565
"express-session": "^1.11.3",
6666
"file-loader": "^0.8.4",
6767
"http-proxy": "^1.11.1",
68-
"lru-memoize": "0.0.1",
69-
"piping": "0.1.8",
68+
"lru-memoize": "0.0.2",
69+
"map-props": "^0.1.1",
70+
"piping": "0.2.0",
7071
"pretty-error": "^1.1.2",
72+
"query-string": "^2.4.0",
7173
"react": "0.13.3",
72-
"react-inline-css": "1.1.1",
73-
"react-redux": "0.8.0",
74-
"react-router": "v1.0.0-beta2",
75-
"redux": "1.0.0-rc",
76-
"redux-form": "^0.1.2",
74+
"react-document-meta": "^0.1.4",
75+
"react-inline-css": "1.2.1",
76+
"react-redux": "0.9.0",
77+
"react-router": "v1.0.0-beta3",
78+
"redux": "^1.0.1",
79+
"redux-form": "^0.5.0",
7780
"serialize-javascript": "^1.0.0",
7881
"serve-favicon": "^2.3.0",
7982
"serve-static": "^1.10.0",
8083
"superagent": "^1.2.0",
8184
"url-loader": "^0.5.6",
82-
"webpack-isomorphic-tools": "^0.3.3"
85+
"webpack-isomorphic-tools": "^0.8.1"
8386
},
8487
"devDependencies": {
85-
"redux-devtools": "1.0.2",
8688
"autoprefixer-loader": "^2.0.0",
87-
"babel-core": "5.4.7",
88-
"babel-eslint": "^3.1.18",
89-
"babel-loader": "5.1.3",
90-
"babel-runtime": "5.4.7",
89+
"babel-core": "^5.8.22",
90+
"babel-eslint": "^4.0.10",
91+
"babel-loader": "5.3.2",
92+
"babel-runtime": "5.8.20",
9193
"better-npm-run": "0.0.1",
9294
"chai": "^3.2.0",
9395
"clean-webpack-plugin": "^0.1.3",
9496
"concurrently": "0.1.1",
95-
"css-loader": "^0.15.1",
96-
"eslint": "^0.23.0",
97-
"eslint-config-airbnb": "0.0.6",
98-
"eslint-plugin-react": "^2.5.2",
97+
"css-loader": "^0.16.0",
98+
"eslint": "^1.2.0",
99+
"eslint-config-airbnb": "0.0.7",
100+
"eslint-plugin-react": "^3.2.3",
99101
"extract-text-webpack-plugin": "^0.8.1",
100102
"json-loader": "0.5.2",
101103
"karma": "^0.13.3",
102104
"karma-chrome-launcher": "^0.2.0",
103-
"karma-cli": "0.0.4",
105+
"karma-cli": "0.1.0",
104106
"karma-firefox-launcher": "^0.1.4",
105-
"karma-mocha": "^0.1.10",
107+
"karma-mocha": "^0.2.0",
106108
"karma-mocha-reporter": "^1.1.0",
107109
"karma-sourcemap-loader": "^0.3.5",
108110
"karma-webpack": "^1.7.0",
109111
"mocha": "^2.2.5",
110112
"node-sass": "^3.2.0",
111-
"react-a11y": "0.1.1",
112-
"react-hot-loader": "1.2.7",
113+
"react-a11y": "0.2.6",
114+
"react-hot-loader": "1.2.8",
115+
"redux-devtools": "1.0.2",
113116
"sass-loader": "^2.0.0",
114117
"strip-loader": "^0.1.0",
115118
"style-loader": "^0.12.3",
116119
"webpack": "^1.9.11",
117-
"webpack-dev-server": "1.9.0"
120+
"webpack-dev-server": "1.10.1"
118121
},
119122
"engines": {
120123
"node": ">=0.10.32"

src/ApiClient.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class ApiClient_ {
1313
forEach((method) => {
1414
this[method] = (path, options) => {
1515
return new Promise((resolve, reject) => {
16-
let request = superagent[method](this.formatUrl(path));
16+
const request = superagent[method](this.formatUrl(path));
1717
if (options && options.params) {
1818
request.query(options.params);
1919
}
@@ -39,7 +39,7 @@ class ApiClient_ {
3939

4040
/* This was originally a standalone function outside of this class, but babel kept breaking, and this fixes it */
4141
formatUrl(path) {
42-
let adjustedPath = path[0] !== '/' ? '/' + path : path;
42+
const adjustedPath = path[0] !== '/' ? '/' + path : path;
4343
if (__SERVER__) {
4444
// Prepend host and port of the API server to the path.
4545
return 'http://localhost:' + config.apiPort + adjustedPath;

src/Html.js

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, {Component, PropTypes} from 'react';
22
import serialize from 'serialize-javascript';
3+
import DocumentMeta from 'react-document-meta';
34
const cdn = '//cdnjs.cloudflare.com/ajax/libs/';
45

56
/**
@@ -20,27 +21,11 @@ export default class Html extends Component {
2021

2122
render() {
2223
const {assets, component, store} = this.props;
23-
const title = 'React Redux Example';
24-
const description = 'All the modern best practices in one example.';
25-
const image = 'https://react-redux.herokuapp.com/logo.jpg';
2624
return (
2725
<html lang="en-us">
2826
<head>
2927
<meta charSet="utf-8"/>
30-
<title>{title}</title>
31-
<meta property="og:site_name" content={title}/>
32-
<meta property="og:image" content={image}/>
33-
<meta property="og:locale" content="en_US"/>
34-
<meta property="og:title" content={title}/>
35-
<meta property="og:description" content={description}/>
36-
<meta name="twitter:card" content="summary"/>
37-
<meta property="twitter:site" content="@erikras"/>
38-
<meta property="twitter:creator" content="@erikras"/>
39-
<meta property="twitter:image" content={image}/>
40-
<meta property="twitter:image:width" content="200"/>
41-
<meta property="twitter:image:height" content="200"/>
42-
<meta property="twitter:title" content={title}/>
43-
<meta property="twitter:description" content={description}/>
28+
{DocumentMeta.rewind({asReact: true})}
4429

4530
<link rel="shortcut icon" href="/favicon.ico" />
4631
<link href={cdn + 'twitter-bootstrap/3.3.5/css/bootstrap.css'}

src/actions/actionTypes.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/actions/authActions.js

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/actions/counterActions.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)