23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/redux/Logic.js:
--------------------------------------------------------------------------------
1 | import {push} from 'react-router-redux';
2 |
3 | export default class Logic {
4 | constructor({store}={}) {
5 | this._store = store;
6 | }
7 |
8 | routerPush(path) {
9 | this.dispatch(push(path));
10 | }
11 |
12 | get location() {
13 | return this.state.routing.locationBeforeTransitions;
14 | }
15 |
16 | get path() {
17 | return this.location.pathname;
18 | }
19 |
20 | get loggedInUser() {
21 | return this.state.auth.user;
22 | }
23 |
24 | get isLoggedIn() {
25 | return !!this.state.auth.user;
26 | }
27 |
28 | get state() {
29 | return this.store.getState();
30 | }
31 |
32 | get store() {
33 | return this._store;
34 | }
35 |
36 | dispatch(obj) {
37 | this.store.dispatch(obj);
38 | }
39 | }
--------------------------------------------------------------------------------
/src/redux/middleware/clientMiddleware.js:
--------------------------------------------------------------------------------
1 | export default function clientMiddleware(client) {
2 | return ({dispatch, getState}) => {
3 | return next => action => {
4 | if (typeof action === 'function') {
5 | return action(dispatch, getState);
6 | }
7 |
8 | const { promise, types, ...rest } = action; // eslint-disable-line no-redeclare
9 | if (!promise) {
10 | return next(action);
11 | }
12 |
13 | const [REQUEST, SUCCESS, FAILURE] = types;
14 | next({...rest, type: REQUEST});
15 |
16 | const actionPromise = promise(client);
17 | actionPromise.then(
18 | (result) => next({...rest, result, type: SUCCESS}),
19 | (error) => next({...rest, error, type: FAILURE})
20 | ).catch((error)=> {
21 | console.error('MIDDLEWARE ERROR:', error);
22 | next({...rest, error, type: FAILURE});
23 | });
24 |
25 | return actionPromise;
26 | };
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/bin/server.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | require('../server.babel'); // babel registration (runtime transpilation for node)
3 | var path = require('path');
4 | var rootDir = path.resolve(__dirname, '..');
5 | /**
6 | * Define isomorphic constants.
7 | */
8 | global.__CLIENT__ = false;
9 | global.__SERVER__ = true;
10 | global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING
11 | global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production';
12 |
13 | if (__DEVELOPMENT__) {
14 | if (!require('piping')({
15 | hook: true,
16 | ignore: /(\/\.|~$|\.json|\.scss$)/i
17 | })) {
18 | return;
19 | }
20 | }
21 |
22 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools
23 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools');
24 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools'))
25 | .development(__DEVELOPMENT__)
26 | .server(rootDir, function () {
27 | require('../src/server');
28 | });
29 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | { "extends": "eslint-config-airbnb",
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "mocha": true
6 | },
7 | "rules": {
8 | "react/no-multi-comp": 0,
9 | "import/default": 0,
10 | "import/no-duplicates": 0,
11 | "import/named": 0,
12 | "import/namespace": 0,
13 | "import/no-unresolved": 0,
14 | "import/no-named-as-default": 2,
15 | "comma-dangle": 0, // not sure why airbnb turned this on. gross!
16 | "indent": [2, 2, {"SwitchCase": 1}],
17 | "no-console": 0,
18 | "no-alert": 0
19 | },
20 | "plugins": [
21 | "react", "import"
22 | ],
23 | "settings": {
24 | "import/parser": "babel-eslint",
25 | "import/resolve": {
26 | "moduleDirectory": ["node_modules", "src"]
27 | }
28 | },
29 | "globals": {
30 | "__DEVELOPMENT__": true,
31 | "__CLIENT__": true,
32 | "__SERVER__": true,
33 | "__DISABLE_SSR__": true,
34 | "__DEVTOOLS__": true,
35 | "socket": true,
36 | "webpackIsomorphicTools": true
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/webpack/webpack-dev-server.js:
--------------------------------------------------------------------------------
1 | var Express = require('express');
2 | var webpack = require('webpack');
3 |
4 | var config = require('../src/config');
5 | var webpackConfig = require('./dev.config');
6 | var compiler = webpack(webpackConfig);
7 |
8 | var host = config.host || 'localhost';
9 | var port = (Number(config.port) + 1) || 3001;
10 | var serverOptions = {
11 | contentBase: 'http://' + host + ':' + port,
12 | quiet: true,
13 | noInfo: true,
14 | hot: true,
15 | inline: true,
16 | lazy: false,
17 | publicPath: webpackConfig.output.publicPath,
18 | headers: {'Access-Control-Allow-Origin': '*'},
19 | stats: {colors: true}
20 | };
21 |
22 | var app = new Express();
23 |
24 | app.use(require('webpack-dev-middleware')(compiler, serverOptions));
25 | app.use(require('webpack-hot-middleware')(compiler));
26 |
27 | app.listen(port, function onAppListening(err) {
28 | if (err) {
29 | console.error(err);
30 | } else {
31 | console.info('==> 🚧 Webpack development server listening on port %s', port);
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/src/containers/Home/Home.scss:
--------------------------------------------------------------------------------
1 | @import "../../theme/variables.scss";
2 |
3 | .home {
4 | dd {
5 | margin-bottom: 15px;
6 | }
7 | }
8 | .masthead {
9 | background: #2d2d2d;
10 | padding: 40px 20px;
11 | color: white;
12 | text-align: center;
13 | .logo {
14 | $size: 200px;
15 | margin: auto;
16 | height: $size;
17 | width: $size;
18 | border-radius: $size / 2;
19 | border: 1px solid $cyan;
20 | box-shadow: inset 0 0 10px $cyan;
21 | vertical-align: middle;
22 | p {
23 | line-height: $size;
24 | margin: 0px;
25 | }
26 | img {
27 | width: 75%;
28 | margin: auto;
29 | }
30 | }
31 | h1 {
32 | color: $cyan;
33 | font-size: 4em;
34 | }
35 | h2 {
36 | color: #ddd;
37 | font-size: 2em;
38 | margin: 20px;
39 | }
40 | a {
41 | color: #ddd;
42 | }
43 | p {
44 | margin: 10px;
45 | }
46 | .humility {
47 | color: $humility;
48 | a {
49 | color: $humility;
50 | }
51 | }
52 | .github {
53 | font-size: 1.5em;
54 | }
55 | }
56 |
57 | .counterContainer {
58 | text-align: center;
59 | margin: 20px;
60 | }
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Erik Rasmussen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/redux/create.js:
--------------------------------------------------------------------------------
1 | import {createStore as _createStore, applyMiddleware, compose} from 'redux';
2 | import createMiddleware from './middleware/clientMiddleware';
3 | import {routerMiddleware} from 'react-router-redux';
4 |
5 | export default function createStore(history, client, data) {
6 | // Sync dispatched route actions to the history
7 | const reduxRouterMiddleware = routerMiddleware(history);
8 |
9 | const middleware = [createMiddleware(client), reduxRouterMiddleware];
10 |
11 | let finalCreateStore;
12 | if (__DEVELOPMENT__ && __CLIENT__ && __DEVTOOLS__) {
13 | const {persistState} = require('redux-devtools');
14 | const DevTools = require('../containers/DevTools/DevTools');
15 | finalCreateStore = compose(
16 | applyMiddleware(...middleware),
17 | window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument(),
18 | persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
19 | )(_createStore);
20 | } else {
21 | finalCreateStore = applyMiddleware(...middleware)(_createStore);
22 | }
23 |
24 | const reducer = require('./modules/reducer');
25 | const store = finalCreateStore(reducer, data);
26 |
27 |
28 | if (__DEVELOPMENT__ && module.hot) {
29 | module.hot.accept('./modules/reducer', () => {
30 | store.replaceReducer(require('./modules/reducer'));
31 | });
32 | }
33 |
34 | return store;
35 | }
36 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | require('babel-polyfill');
2 |
3 | const environment = {
4 | development: {
5 | isProduction: false
6 | },
7 | production: {
8 | isProduction: true
9 | }
10 | }[process.env.NODE_ENV || 'development'];
11 |
12 | module.exports = Object.assign({
13 | host: process.env.HOST || 'localhost',
14 | port: process.env.PORT,
15 | apiHost: process.env.APIHOST || 'localhost',
16 | apiPort: process.env.APIPORT,
17 | app: {
18 | title: 'React Redux Example',
19 | description: 'All the modern best practices in one example.',
20 | head: {
21 | titleTemplate: 'React Redux Example: %s',
22 | meta: [
23 | {name: 'description', content: 'All the modern best practices in one example.'},
24 | {charset: 'utf-8'},
25 | {property: 'og:site_name', content: 'React Redux Example'},
26 | {property: 'og:image', content: 'https://react-redux.herokuapp.com/logo.jpg'},
27 | {property: 'og:locale', content: 'en_US'},
28 | {property: 'og:title', content: 'React Redux Example'},
29 | {property: 'og:description', content: 'All the modern best practices in one example.'},
30 | {property: 'og:card', content: 'summary'},
31 | {property: 'og:site', content: '@erikras'},
32 | {property: 'og:creator', content: '@erikras'},
33 | {property: 'og:image:width', content: '200'},
34 | {property: 'og:image:height', content: '200'}
35 | ]
36 | }
37 | },
38 |
39 | }, environment);
40 |
--------------------------------------------------------------------------------
/src/helpers/ApiClient.js:
--------------------------------------------------------------------------------
1 | import superagent from 'superagent';
2 | import config from '../config';
3 |
4 | const methods = ['get', 'post', 'put', 'patch', 'del'];
5 |
6 | function formatUrl(path) {
7 | const adjustedPath = path[0] !== '/' ? '/' + path : path;
8 | if (__SERVER__) {
9 | // Prepend host and port of the API server to the path.
10 | return 'http://' + config.apiHost + ':' + config.apiPort + adjustedPath;
11 | }
12 | // Prepend `/api` to relative URL, to proxy to API server.
13 | return '/api' + adjustedPath;
14 | }
15 |
16 | export default class ApiClient {
17 | constructor(req) {
18 | methods.forEach((method) =>
19 | this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => {
20 | const request = superagent[method](formatUrl(path));
21 |
22 | if (params) {
23 | request.query(params);
24 | }
25 |
26 | if (__SERVER__ && req.get('cookie')) {
27 | request.set('cookie', req.get('cookie'));
28 | }
29 |
30 | if (data) {
31 | request.send(data);
32 | }
33 |
34 | request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body));
35 | }));
36 | }
37 | /*
38 | * There's a V8 bug where, when using Babel, exporting classes with only
39 | * constructors sometimes fails. Until it's patched, this is a solution to
40 | * "ApiClient is not defined" from issue #14.
41 | * https://github.com/erikras/react-redux-universal-hot-example/issues/14
42 | *
43 | * Relevant Babel bug (but they claim it's V8): https://phabricator.babeljs.io/T2455
44 | *
45 | * Remove it at your own risk.
46 | */
47 | empty() {}
48 | }
49 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | module.exports = function (config) {
4 | config.set({
5 |
6 | browsers: ['PhantomJS'],
7 |
8 | singleRun: !!process.env.CI,
9 |
10 | frameworks: [ 'mocha' ],
11 |
12 | files: [
13 | './node_modules/phantomjs-polyfill/bind-polyfill.js',
14 | 'tests.webpack.js'
15 | ],
16 |
17 | preprocessors: {
18 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ]
19 | },
20 |
21 | reporters: [ 'mocha' ],
22 |
23 | plugins: [
24 | require("karma-webpack"),
25 | require("karma-mocha"),
26 | require("karma-mocha-reporter"),
27 | require("karma-phantomjs-launcher"),
28 | require("karma-sourcemap-loader")
29 | ],
30 |
31 | webpack: {
32 | devtool: 'inline-source-map',
33 | module: {
34 | loaders: [
35 | { test: /\.(jpe?g|png|gif|svg)$/, loader: 'url', query: {limit: 10240} },
36 | { test: /\.js$/, exclude: /node_modules/, loaders: ['babel']},
37 | { test: /\.json$/, loader: 'json-loader' },
38 | { test: /\.less$/, loader: 'style!css!less' },
39 | { test: /\.scss$/, loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap' }
40 | ]
41 | },
42 | resolve: {
43 | modulesDirectories: [
44 | 'src',
45 | 'node_modules'
46 | ],
47 | extensions: ['', '.json', '.js']
48 | },
49 | plugins: [
50 | new webpack.IgnorePlugin(/\.json$/),
51 | new webpack.NoErrorsPlugin(),
52 | new webpack.DefinePlugin({
53 | __CLIENT__: true,
54 | __SERVER__: false,
55 | __DEVELOPMENT__: true,
56 | __DEVTOOLS__: false // <-------- DISABLE redux-devtools HERE
57 | })
58 | ]
59 | },
60 |
61 | webpackServer: {
62 | noInfo: true
63 | }
64 |
65 | });
66 | };
67 |
--------------------------------------------------------------------------------
/src/utils/validation.js:
--------------------------------------------------------------------------------
1 | const isEmpty = value => value === undefined || value === null || value === '';
2 | const join = (rules) => (value, data) => rules.map(rule => rule(value, data)).filter(error => !!error)[0 /* first error */ ];
3 |
4 | export function email(value) {
5 | // Let's not start a debate on email regex. This is just for an example app!
6 | if (!isEmpty(value) && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) {
7 | return 'Invalid email address';
8 | }
9 | }
10 |
11 | export function required(value) {
12 | if (isEmpty(value)) {
13 | return 'Required';
14 | }
15 | }
16 |
17 | export function minLength(min) {
18 | return value => {
19 | if (!isEmpty(value) && value.length < min) {
20 | return `Must be at least ${min} characters`;
21 | }
22 | };
23 | }
24 |
25 | export function maxLength(max) {
26 | return value => {
27 | if (!isEmpty(value) && value.length > max) {
28 | return `Must be no more than ${max} characters`;
29 | }
30 | };
31 | }
32 |
33 | export function integer(value) {
34 | if (!Number.isInteger(Number(value))) {
35 | return 'Must be an integer';
36 | }
37 | }
38 |
39 | export function oneOf(enumeration) {
40 | return value => {
41 | if (!~enumeration.indexOf(value)) {
42 | return `Must be one of: ${enumeration.join(', ')}`;
43 | }
44 | };
45 | }
46 |
47 | export function match(field) {
48 | return (value, data) => {
49 | if (data) {
50 | if (value !== data[field]) {
51 | return 'Do not match';
52 | }
53 | }
54 | };
55 | }
56 |
57 | export function createValidator(rules) {
58 | return (data = {}) => {
59 | const errors = {};
60 | Object.keys(rules).forEach((key) => {
61 | const rule = join([].concat(rules[key])); // concat enables both functions and arrays of functions
62 | const error = rule(data[key], data);
63 | if (error) {
64 | errors[key] = error;
65 | }
66 | });
67 | return errors;
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Some basic conventions for contributing to this project.
4 |
5 | ### General
6 |
7 | Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork.
8 |
9 | * Non-trivial changes should be discussed in an issue first
10 | * Develop in a topic branch, not master
11 | * Squash your commits
12 |
13 | ### Linting
14 |
15 | Please check your code using `npm run lint` before submitting your pull requests, as the CI build will fail if `eslint` fails.
16 |
17 | ### Commit Message Format
18 |
19 | Each commit message should include a **type**, a **scope** and a **subject**:
20 |
21 | ```
22 | ():
23 | ```
24 |
25 | Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
26 |
27 | ```
28 | #459 refactor(utils): create url mapper utility function
29 | #463 chore(webpack): update to isomorphic tools v2
30 | #494 fix(babel): correct dependencies and polyfills
31 | #510 feat(app): add react-bootstrap responsive navbar
32 | ```
33 |
34 | #### Type
35 |
36 | Must be one of the following:
37 |
38 | * **feat**: A new feature
39 | * **fix**: A bug fix
40 | * **docs**: Documentation only changes
41 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
42 | semi-colons, etc)
43 | * **refactor**: A code change that neither fixes a bug or adds a feature
44 | * **test**: Adding missing tests
45 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
46 | generation
47 |
48 | #### Scope
49 |
50 | The scope could be anything specifying place of the commit change. For example `webpack`,
51 | `helpers`, `api` etc...
52 |
53 | #### Subject
54 |
55 | The subject contains succinct description of the change:
56 |
57 | * use the imperative, present tense: "change" not "changed" nor "changes"
58 | * don't capitalize first letter
59 | * no dot (.) at the end
60 |
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import createStore from './redux/create';
5 | import ApiClient from './helpers/ApiClient';
6 | import {Provider} from 'react-redux';
7 | import {Router, browserHistory} from 'react-router';
8 | import {syncHistoryWithStore} from 'react-router-redux';
9 | import {ReduxAsyncConnect} from 'redux-connect';
10 | import useScroll from 'scroll-behavior/lib/useStandardScroll';
11 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
12 | import Logic from './redux/Logic';
13 | import injectTapEventPlugin from 'react-tap-event-plugin';
14 |
15 | import getRoutes from './routes';
16 |
17 | const client = new ApiClient();
18 | const _browserHistory = useScroll(() => browserHistory)();
19 | const dest = document.getElementById('content');
20 | const store = createStore(_browserHistory, client, window.__data);
21 | const history = syncHistoryWithStore(_browserHistory, store);
22 |
23 | const logic = new Logic({store});
24 | injectTapEventPlugin();
25 |
26 | const MyProvider = Provider;
27 | MyProvider.prototype.getChildContext = function getChildContext() {
28 | return {store, logic};
29 | };
30 |
31 | Object.assign(MyProvider.childContextTypes, {logic: React.PropTypes.object});
32 |
33 | const component = (
34 |
35 | !item.deferred} />
36 | } history={history}>
37 | {getRoutes(store)}
38 |
39 | );
40 |
41 | ReactDOM.render(
42 |
43 |
44 | {component}
45 |
46 | ,
47 | dest
48 | );
49 |
50 | if (process.env.NODE_ENV !== 'production') {
51 | window.React = React; // enable debugger
52 |
53 | if (!dest || !dest.firstChild || !dest.firstChild.attributes || !dest.firstChild.attributes['data-react-checksum']) {
54 | console.error('Server-side React render was discarded. Make sure that your initial render does not contain any client-side code.');
55 | }
56 | }
57 |
58 | if (__DEVTOOLS__ && !window.devToolsExtension) {
59 | const DevTools = require('./containers/DevTools/DevTools');
60 | ReactDOM.render(
61 |
62 |
63 | {component}
64 |
65 |
66 | ,
67 | dest
68 | );
69 | }
--------------------------------------------------------------------------------
/src/helpers/Html.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import ReactDOM from 'react-dom/server';
3 | import serialize from 'serialize-javascript';
4 | import Helmet from 'react-helmet';
5 |
6 | /**
7 | * Wrapper component containing HTML metadata and boilerplate tags.
8 | * Used in server-side code only to wrap the string output of the
9 | * rendered route component.
10 | *
11 | * The only thing this component doesn't (and can't) include is the
12 | * HTML doctype declaration, which is added to the rendered output
13 | * by the server.js file.
14 | */
15 | export default class Html extends Component {
16 | static propTypes = {
17 | assets: PropTypes.object,
18 | component: PropTypes.node,
19 | store: PropTypes.object
20 | };
21 |
22 | render() {
23 | const {assets, component, store} = this.props;
24 | const content = component ? ReactDOM.renderToString(component) : '';
25 | const head = Helmet.rewind();
26 |
27 | return (
28 |
29 |
30 | {head.base.toComponent()}
31 | {head.title.toComponent()}
32 | {head.meta.toComponent()}
33 | {head.link.toComponent()}
34 | {head.script.toComponent()}
35 |
36 |
37 |
38 | {/* styles (will be present only in production with webpack extract text plugin) */}
39 | {Object.keys(assets.styles).map((style, key) =>
40 |
42 | )}
43 |
44 | {/* (will be present only in development mode) */}
45 | {/* outputs a tag with all bootstrap styles + App.scss + it could be CurrentPage.scss. */}
46 | {/* can smoothen the initial style flash (flicker) on page load in development mode. */}
47 | {/* ideally one could also include here the style for the current page (Home.scss, About.scss, etc) */}
48 | { Object.keys(assets.styles).length === 0 ? : null }
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/redux/modules/auth.js:
--------------------------------------------------------------------------------
1 | const LOAD = 'redux-example/auth/LOAD';
2 | const LOAD_SUCCESS = 'redux-example/auth/LOAD_SUCCESS';
3 | const LOAD_FAIL = 'redux-example/auth/LOAD_FAIL';
4 | const LOGIN = 'redux-example/auth/LOGIN';
5 | const LOGIN_SUCCESS = 'redux-example/auth/LOGIN_SUCCESS';
6 | const LOGIN_FAIL = 'redux-example/auth/LOGIN_FAIL';
7 | const LOGOUT = 'redux-example/auth/LOGOUT';
8 | const LOGOUT_SUCCESS = 'redux-example/auth/LOGOUT_SUCCESS';
9 | const LOGOUT_FAIL = 'redux-example/auth/LOGOUT_FAIL';
10 |
11 | const initialState = {
12 | loaded: false
13 | };
14 |
15 | export default function reducer(state = initialState, action = {}) {
16 | switch (action.type) {
17 | case LOAD:
18 | return {
19 | ...state,
20 | loading: true
21 | };
22 | case LOAD_SUCCESS:
23 | return {
24 | ...state,
25 | loading: false,
26 | loaded: true,
27 | user: action.result
28 | };
29 | case LOAD_FAIL:
30 | return {
31 | ...state,
32 | loading: false,
33 | loaded: false,
34 | error: action.error
35 | };
36 | case LOGIN:
37 | return {
38 | ...state,
39 | loggingIn: true
40 | };
41 | case LOGIN_SUCCESS:
42 | return {
43 | ...state,
44 | loggingIn: false,
45 | user: action.result
46 | };
47 | case LOGIN_FAIL:
48 | return {
49 | ...state,
50 | loggingIn: false,
51 | user: null,
52 | loginError: action.error
53 | };
54 | case LOGOUT:
55 | return {
56 | ...state,
57 | loggingOut: true
58 | };
59 | case LOGOUT_SUCCESS:
60 | return {
61 | ...state,
62 | loggingOut: false,
63 | user: null
64 | };
65 | case LOGOUT_FAIL:
66 | return {
67 | ...state,
68 | loggingOut: false,
69 | logoutError: action.error
70 | };
71 | default:
72 | return state;
73 | }
74 | }
75 |
76 | export function isLoaded(globalState) {
77 | return globalState.auth && globalState.auth.loaded;
78 | }
79 |
80 | export function load() {
81 | return {
82 | types: [LOAD, LOAD_SUCCESS, LOAD_FAIL],
83 | promise: (client) => client.get('/loadAuth')
84 | };
85 | }
86 |
87 | export function login(name) {
88 | return {
89 | types: [LOGIN, LOGIN_SUCCESS, LOGIN_FAIL],
90 | promise: (client) => client.post('/login', {
91 | data: {
92 | name: name
93 | }
94 | })
95 | };
96 | }
97 |
98 | export function logout() {
99 | return {
100 | types: [LOGOUT, LOGOUT_SUCCESS, LOGOUT_FAIL],
101 | promise: (client) => client.get('/logout')
102 | };
103 | }
104 |
--------------------------------------------------------------------------------
/webpack/prod.config.js:
--------------------------------------------------------------------------------
1 | require('babel-polyfill');
2 |
3 | // Webpack config for creating the production bundle.
4 | var path = require('path');
5 | var webpack = require('webpack');
6 | var CleanPlugin = require('clean-webpack-plugin');
7 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
8 | var strip = require('strip-loader');
9 |
10 | var projectRootPath = path.resolve(__dirname, '../');
11 | var assetsPath = path.resolve(projectRootPath, './static/dist');
12 |
13 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools
14 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
15 | var webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools'));
16 |
17 | module.exports = {
18 | devtool: 'source-map',
19 | context: path.resolve(__dirname, '..'),
20 | entry: {
21 | 'main': [
22 | 'bootstrap-sass!./src/theme/bootstrap.config.prod.js',
23 | 'font-awesome-webpack!./src/theme/font-awesome.config.prod.js',
24 | './src/client.js'
25 | ]
26 | },
27 | output: {
28 | path: assetsPath,
29 | filename: '[name]-[chunkhash].js',
30 | chunkFilename: '[name]-[chunkhash].js',
31 | publicPath: '/dist/'
32 | },
33 | module: {
34 | loaders: [
35 | { test: /\.jsx?$/, exclude: /node_modules/, loaders: [strip.loader('debug'), 'babel']},
36 | { test: /\.json$/, loader: 'json-loader' },
37 | { test: /\.less$/, loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=2&sourceMap!autoprefixer?browsers=last 2 version!less?outputStyle=expanded&sourceMap=true&sourceMapContents=true') },
38 | { test: /\.scss$/, loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=2&sourceMap!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap=true&sourceMapContents=true') },
39 | { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff" },
40 | { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff" },
41 | { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream" },
42 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" },
43 | { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml" },
44 | { test: webpackIsomorphicToolsPlugin.regular_expression('images'), loader: 'url-loader?limit=10240' }
45 | ]
46 | },
47 | progress: true,
48 | resolve: {
49 | modulesDirectories: [
50 | 'src',
51 | 'node_modules'
52 | ],
53 | extensions: ['', '.json', '.js', '.jsx']
54 | },
55 | plugins: [
56 | new CleanPlugin([assetsPath], { root: projectRootPath }),
57 |
58 | // css files from the extract-text-plugin loader
59 | new ExtractTextPlugin('[name]-[chunkhash].css', {allChunks: true}),
60 | new webpack.DefinePlugin({
61 | 'process.env': {
62 | NODE_ENV: '"production"'
63 | },
64 |
65 | __CLIENT__: true,
66 | __SERVER__: false,
67 | __DEVELOPMENT__: false,
68 | __DEVTOOLS__: false
69 | }),
70 |
71 | // ignore dev config
72 | new webpack.IgnorePlugin(/\.\/dev/, /\/config$/),
73 |
74 | // optimizations
75 | new webpack.optimize.DedupePlugin(),
76 | new webpack.optimize.OccurenceOrderPlugin(),
77 | new webpack.optimize.UglifyJsPlugin({
78 | compress: {
79 | warnings: false
80 | }
81 | }),
82 |
83 | webpackIsomorphicToolsPlugin
84 | ]
85 | };
86 |
--------------------------------------------------------------------------------
/webpack/webpack-isomorphic-tools.js:
--------------------------------------------------------------------------------
1 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
2 |
3 | // see this link for more info on what all of this means
4 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools
5 | module.exports = {
6 |
7 | // when adding "js" extension to asset types
8 | // and then enabling debug mode, it may cause a weird error:
9 | //
10 | // [0] npm run start-prod exited with code 1
11 | // Sending SIGTERM to other processes..
12 | //
13 | // debug: true,
14 |
15 | assets: {
16 | images: {
17 | extensions: [
18 | 'jpeg',
19 | 'jpg',
20 | 'png',
21 | 'gif'
22 | ],
23 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser
24 | },
25 | fonts: {
26 | extensions: [
27 | 'woff',
28 | 'woff2',
29 | 'ttf',
30 | 'eot'
31 | ],
32 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser
33 | },
34 | svg: {
35 | extension: 'svg',
36 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser
37 | },
38 | // this whole "bootstrap" asset type is only used once in development mode.
39 | // the only place it's used is the Html.js file
40 | // where a tag is created with the contents of the
41 | // './src/theme/bootstrap.config.js' file.
42 | // (the aforementioned tag can reduce the white flash
43 | // when refreshing page in development mode)
44 | //
45 | // hooking into 'js' extension require()s isn't the best solution
46 | // and I'm leaving this comment here in case anyone finds a better idea.
47 | bootstrap: {
48 | extension: 'js',
49 | include: ['./src/theme/bootstrap.config.js'],
50 | filter: function(module, regex, options, log) {
51 | function is_bootstrap_style(name) {
52 | return name.indexOf('./src/theme/bootstrap.config.js') >= 0;
53 | }
54 | if (options.development) {
55 | return is_bootstrap_style(module.name) && WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
56 | }
57 | // no need for it in production mode
58 | },
59 | // in development mode there's webpack "style-loader",
60 | // so the module.name is not equal to module.name
61 | path: WebpackIsomorphicToolsPlugin.style_loader_path_extractor,
62 | parser: WebpackIsomorphicToolsPlugin.css_loader_parser
63 | },
64 | style_modules: {
65 | extensions: ['less','scss'],
66 | filter: function(module, regex, options, log) {
67 | if (options.development) {
68 | // in development mode there's webpack "style-loader",
69 | // so the module.name is not equal to module.name
70 | return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
71 | } else {
72 | // in production mode there's no webpack "style-loader",
73 | // so the module.name will be equal to the asset path
74 | return regex.test(module.name);
75 | }
76 | },
77 | path: function(module, options, log) {
78 | if (options.development) {
79 | // in development mode there's webpack "style-loader",
80 | // so the module.name is not equal to module.name
81 | return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
82 | } else {
83 | // in production mode there's no webpack "style-loader",
84 | // so the module.name will be equal to the asset path
85 | return module.name;
86 | }
87 | },
88 | parser: function(module, options, log) {
89 | if (options.development) {
90 | return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
91 | } else {
92 | // in production mode there's Extract Text Loader which extracts CSS text away
93 | return module.source;
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | import Express from 'express';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom/server';
4 | import config from './config';
5 | import favicon from 'serve-favicon';
6 | import compression from 'compression';
7 | import httpProxy from 'http-proxy';
8 | import path from 'path';
9 | import createStore from './redux/create';
10 | import ApiClient from './helpers/ApiClient';
11 | import Html from './helpers/Html';
12 | import PrettyError from 'pretty-error';
13 | import http from 'http';
14 |
15 |
16 | import {match} from 'react-router';
17 | import {syncHistoryWithStore} from 'react-router-redux';
18 | import {ReduxAsyncConnect, loadOnServer} from 'redux-connect';
19 | import createHistory from 'react-router/lib/createMemoryHistory';
20 | import {Provider} from 'react-redux';
21 | import getRoutes from './routes';
22 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
23 | import Logic from './redux/Logic';
24 |
25 |
26 |
27 | const targetUrl = 'http://' + config.apiHost + ':' + config.apiPort;
28 | const pretty = new PrettyError();
29 | const app = new Express();
30 | const server = new http.Server(app);
31 |
32 | app.use(compression());
33 | app.use(favicon(path.join(__dirname, '..', 'static', 'favicon.ico')));
34 |
35 | app.use(Express.static(path.join(__dirname, '..', 'static')));
36 |
37 | app.use((req, res) => {
38 | if (__DEVELOPMENT__) {
39 | // Do not cache webpack stats: the script file would change since
40 | // hot module replacement is enabled in the development env
41 | webpackIsomorphicTools.refresh();
42 | }
43 | const client = new ApiClient(req);
44 | const memoryHistory = createHistory(req.originalUrl);
45 | const store = createStore(memoryHistory, client);
46 | const history = syncHistoryWithStore(memoryHistory, store);
47 |
48 | const logic = new Logic({store});
49 |
50 | const MyProvider = Provider;
51 | MyProvider.prototype.getChildContext = function getChildContext() {
52 | return {store, logic};
53 | };
54 |
55 | Object.assign(MyProvider.childContextTypes, {logic: React.PropTypes.object});
56 |
57 | function hydrateOnClient() {
58 | res.send('\n' +
59 | ReactDOM.renderToString());
60 | }
61 |
62 | if (__DISABLE_SSR__) {
63 | hydrateOnClient();
64 | return;
65 | }
66 |
67 | match({history, routes: getRoutes(store), location: req.originalUrl}, (error, redirectLocation, renderProps) => {
68 | if (redirectLocation) {
69 | res.redirect(redirectLocation.pathname + redirectLocation.search);
70 | } else if (error) {
71 | console.error('ROUTER ERROR:', pretty.render(error));
72 | res.status(500);
73 | hydrateOnClient();
74 | } else if (renderProps) {
75 | loadOnServer({...renderProps, store, helpers: {client}}).then(() => {
76 | const component = (
77 |
78 |
79 |
80 |
81 |
82 | );
83 |
84 | res.status(200);
85 |
86 | global.navigator = {userAgent: req.headers['user-agent']};
87 |
88 | res.send('\n' +
89 | ReactDOM.renderToString());
91 | });
92 | } else {
93 | res.status(404).send('Not found');
94 | }
95 | });
96 | });
97 |
98 | if (config.port) {
99 | server.listen(config.port, (err) => {
100 | if (err) {
101 | console.error(err);
102 | }
103 | console.info('==> 💻 Open http://%s:%s in a browser to view the app.', config.host, config.port);
104 | });
105 | } else {
106 | console.error('==> ERROR: No PORT environment variable has been specified');
107 | }
108 |
--------------------------------------------------------------------------------
/webpack/dev.config.js:
--------------------------------------------------------------------------------
1 | require('babel-polyfill');
2 |
3 | // Webpack config for development
4 | var fs = require('fs');
5 | var path = require('path');
6 | var webpack = require('webpack');
7 | var assetsPath = path.resolve(__dirname, '../static/dist');
8 | var host = (process.env.HOST || 'localhost');
9 | var port = (+process.env.PORT + 1) || 3001;
10 |
11 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools
12 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
13 | var webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools'));
14 |
15 | var babelrc = fs.readFileSync('./.babelrc');
16 | var babelrcObject = {};
17 |
18 | try {
19 | babelrcObject = JSON.parse(babelrc);
20 | } catch (err) {
21 | console.error('==> ERROR: Error parsing your .babelrc.');
22 | console.error(err);
23 | }
24 |
25 |
26 | var babelrcObjectDevelopment = babelrcObject.env && babelrcObject.env.development || {};
27 |
28 | // merge global and dev-only plugins
29 | var combinedPlugins = babelrcObject.plugins || [];
30 | combinedPlugins = combinedPlugins.concat(babelrcObjectDevelopment.plugins);
31 |
32 | var babelLoaderQuery = Object.assign({}, babelrcObjectDevelopment, babelrcObject, {plugins: combinedPlugins});
33 | delete babelLoaderQuery.env;
34 |
35 | // Since we use .babelrc for client and server, and we don't want HMR enabled on the server, we have to add
36 | // the babel plugin react-transform-hmr manually here.
37 |
38 | // make sure react-transform is enabled
39 | babelLoaderQuery.plugins = babelLoaderQuery.plugins || [];
40 | var reactTransform = null;
41 | for (var i = 0; i < babelLoaderQuery.plugins.length; ++i) {
42 | var plugin = babelLoaderQuery.plugins[i];
43 | if (Array.isArray(plugin) && plugin[0] === 'react-transform') {
44 | reactTransform = plugin;
45 | }
46 | }
47 |
48 | if (!reactTransform) {
49 | reactTransform = ['react-transform', {transforms: []}];
50 | babelLoaderQuery.plugins.push(reactTransform);
51 | }
52 |
53 | if (!reactTransform[1] || !reactTransform[1].transforms) {
54 | reactTransform[1] = Object.assign({}, reactTransform[1], {transforms: []});
55 | }
56 |
57 | // make sure react-transform-hmr is enabled
58 | reactTransform[1].transforms.push({
59 | transform: 'react-transform-hmr',
60 | imports: ['react'],
61 | locals: ['module']
62 | });
63 |
64 | module.exports = {
65 | devtool: 'inline-source-map',
66 | context: path.resolve(__dirname, '..'),
67 | entry: {
68 | 'main': [
69 | 'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr',
70 | './src/client.js'
71 | ]
72 | },
73 | output: {
74 | path: assetsPath,
75 | filename: '[name]-[hash].js',
76 | chunkFilename: '[name]-[chunkhash].js',
77 | publicPath: 'http://' + host + ':' + port + '/dist/'
78 | },
79 | module: {
80 | loaders: [
81 | {test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel?' + JSON.stringify(babelLoaderQuery)]},
82 | {test: /\.json$/, loader: 'json-loader'},
83 | {
84 | test: /\.less$/,
85 | loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!less?outputStyle=expanded&sourceMap'
86 | },
87 | {
88 | test: /\.scss$/,
89 | loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap'
90 | },
91 | {test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"},
92 | {test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"},
93 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream"},
94 | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file"},
95 | {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml"},
96 | {test: webpackIsomorphicToolsPlugin.regular_expression('images'), loader: 'url-loader?limit=10240'}
97 | ]
98 | },
99 | progress: true,
100 | resolve: {
101 | modulesDirectories: [
102 | 'src',
103 | 'node_modules'
104 | ],
105 | extensions: ['', '.json', '.js', '.jsx']
106 | },
107 | plugins: [
108 | // hot reload
109 | new webpack.HotModuleReplacementPlugin(),
110 | new webpack.IgnorePlugin(/webpack-stats\.json$/),
111 | new webpack.DefinePlugin({
112 | __CLIENT__: true,
113 | __SERVER__: false,
114 | __DEVELOPMENT__: true,
115 | __DEVTOOLS__: true // <-------- DISABLE redux-devtools HERE
116 | }),
117 | webpackIsomorphicToolsPlugin.development()
118 | ]
119 | };
120 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-universal-hot-example-bare",
3 | "description": "Example of an isomorphic (universal) webapp using react redux and hot reloading",
4 | "author": "Erik Rasmussen (http://github.com/erikras) customized by Robert Lancer",
5 | "license": "MIT",
6 | "version": "0.9.0",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/rlancer/react-redux-starter-bare"
10 | },
11 | "homepage": "https://github.com/erikras/react-redux-universal-hot-example",
12 | "keywords": [],
13 | "main": "bin/server.js",
14 | "scripts": {
15 | "start": "concurrent --kill-others \"npm run start-prod\"",
16 | "start-prod": "better-npm-run start-prod",
17 | "build": "better-npm-run build",
18 | "postinstall": "npm run build",
19 | "lint": "eslint -c .eslintrc src",
20 | "start-dev": "better-npm-run start-dev",
21 | "watch-client": "better-npm-run watch-client",
22 | "dev": "concurrent --kill-others \"npm run watch-client\" \"npm run start-dev\"",
23 | "test": "karma start",
24 | "test-node": "./node_modules/mocha/bin/mocha $(find -name '*-test.js') --compilers js:babel-core/register",
25 | "test-node-watch": "./node_modules/mocha/bin/mocha $(find -name '*-test.js') --compilers js:babel-core/register --watch"
26 | },
27 | "betterScripts": {
28 | "start-prod": {
29 | "command": "node ./bin/server.js",
30 | "env": {
31 | "NODE_PATH": "./src",
32 | "NODE_ENV": "production",
33 | "PORT": 8080
34 | }
35 | },
36 | "start-dev": {
37 | "command": "node ./bin/server.js",
38 | "env": {
39 | "NODE_PATH": "./src",
40 | "NODE_ENV": "development",
41 | "PORT": 3000
42 | }
43 | },
44 | "watch-client": {
45 | "command": "node webpack/webpack-dev-server.js",
46 | "env": {
47 | "UV_THREADPOOL_SIZE": 100,
48 | "NODE_PATH": "./src",
49 | "PORT": 3000
50 | }
51 | },
52 | "build": {
53 | "command": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js",
54 | "env": {
55 | "NODE_ENV": "production"
56 | }
57 | }
58 | },
59 | "dependencies": {
60 | "babel-core": "^6.5.2",
61 | "babel-loader": "^6.2.1",
62 | "babel-plugin-add-module-exports": "^0.1.2",
63 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
64 | "babel-plugin-transform-react-display-name": "^6.3.13",
65 | "babel-plugin-transform-runtime": "^6.3.13",
66 | "babel-polyfill": "^6.3.14",
67 | "babel-preset-es2015": "^6.3.13",
68 | "babel-preset-react": "^6.3.13",
69 | "babel-preset-stage-0": "^6.3.13",
70 | "babel-register": "^6.3.13",
71 | "babel-runtime": "^6.3.19",
72 | "body-parser": "^1.14.1",
73 | "compression": "^1.6.0",
74 | "express": "^4.13.3",
75 | "express-session": "^1.12.1",
76 | "file-loader": "^0.8.5",
77 | "hoist-non-react-statics": "^1.0.3",
78 | "http-proxy": "^1.12.0",
79 | "invariant": "^2.2.0",
80 | "less": "^2.5.3",
81 | "less-loader": "^2.2.1",
82 | "lru-memoize": "^1.0.0",
83 | "map-props": "^1.0.0",
84 | "material-ui": "^0.15.2",
85 | "multireducer": "^2.0.0",
86 | "piping": "^0.3.0",
87 | "pretty-error": "^1.2.0",
88 | "react": "^15.0.2",
89 | "react-bootstrap": "^0.29.3",
90 | "react-dom": "^15.0.2",
91 | "react-helmet": "^3.1.0",
92 | "react-inline-css": "^2.0.0",
93 | "react-redux": "^4.0.0",
94 | "react-router": "2.4.0",
95 | "react-router-bootstrap": "^0.23.0",
96 | "react-router-redux": "^4.0.0",
97 | "react-tap-event-plugin": "^1.0.0",
98 | "redux": "^3.0.4",
99 | "redux-connect": "^2.1.0",
100 | "redux-form": "^3.0.12",
101 | "scroll-behavior": "^0.3.2",
102 | "serialize-javascript": "^1.1.2",
103 | "serve-favicon": "^2.3.0",
104 | "socket.io": "^1.3.7",
105 | "socket.io-client": "^1.3.7",
106 | "superagent": "^1.4.0",
107 | "url-loader": "^0.5.7",
108 | "warning": "^2.1.0",
109 | "webpack-isomorphic-tools": "^2.2.18"
110 | },
111 | "devDependencies": {
112 | "autoprefixer-loader": "^3.1.0",
113 | "babel-eslint": "^5.0.0-beta6",
114 | "babel-plugin-react-transform": "^2.0.0",
115 | "babel-plugin-typecheck": "^3.6.0",
116 | "better-npm-run": "0.0.8",
117 | "bootstrap-sass": "^3.3.5",
118 | "bootstrap-sass-loader": "^1.0.9",
119 | "chai": "^3.3.0",
120 | "clean-webpack-plugin": "^0.1.6",
121 | "concurrently": "^0.1.1",
122 | "css-loader": "^0.23.1",
123 | "eslint": "1.10.3",
124 | "eslint-config-airbnb": "0.1.0",
125 | "eslint-loader": "^1.0.0",
126 | "eslint-plugin-import": "^0.8.0",
127 | "eslint-plugin-react": "^3.5.0",
128 | "extract-text-webpack-plugin": "^0.9.1",
129 | "font-awesome": "^4.4.0",
130 | "font-awesome-webpack": "0.0.4",
131 | "json-loader": "^0.5.4",
132 | "karma": "^0.13.10",
133 | "karma-cli": "^0.1.1",
134 | "karma-mocha": "^0.2.0",
135 | "karma-mocha-reporter": "^1.1.1",
136 | "karma-phantomjs-launcher": "^0.2.1",
137 | "karma-sourcemap-loader": "^0.3.5",
138 | "karma-webpack": "^1.7.0",
139 | "mocha": "^2.3.3",
140 | "node-sass": "^3.4.2",
141 | "phantomjs": "^1.9.18",
142 | "phantomjs-polyfill": "0.0.1",
143 | "react-a11y": "^0.3.3",
144 | "react-addons-test-utils": "^15.0.2",
145 | "react-transform-catch-errors": "^1.0.0",
146 | "react-transform-hmr": "^1.0.1",
147 | "redbox-react": "^1.1.1",
148 | "redux-devtools": "^3.0.0-beta-3",
149 | "redux-devtools-dock-monitor": "^1.0.0-beta-3",
150 | "redux-devtools-log-monitor": "^1.0.0-beta-3",
151 | "sass-loader": "^3.1.2",
152 | "sinon": "^1.17.2",
153 | "strip-loader": "^0.1.0",
154 | "style-loader": "^0.13.0",
155 | "timekeeper": "0.0.5",
156 | "webpack": "^1.12.9",
157 | "webpack-dev-middleware": "^1.4.0",
158 | "webpack-hot-middleware": "^2.5.0"
159 | },
160 | "engines": {
161 | "node": "6.3.1"
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Redux Universal Hot Example
2 |
3 | [](https://travis-ci.org/erikras/react-redux-universal-hot-example)
4 | [](https://david-dm.org/erikras/react-redux-universal-hot-example)
5 | [](https://david-dm.org/erikras/react-redux-universal-hot-example#info=devDependencies)
6 | [](https://discord.gg/0ZcbPKXt5bZZb1Ko)
7 | [](https://react-redux.herokuapp.com)
8 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=E2LK57ZQ9YRMN)
9 |
10 | ---
11 |
12 | ## About
13 |
14 | This is a starter boilerplate app I've put together using the following technologies:
15 |
16 | * ~~Isomorphic~~ [Universal](https://medium.com/@mjackson/universal-javascript-4761051b7ae9) rendering
17 | * Both client and server make calls to load data from separate API server
18 | * [React](https://github.com/facebook/react)
19 | * [React Router](https://github.com/rackt/react-router)
20 | * [Express](http://expressjs.com)
21 | * [Babel](http://babeljs.io) for ES6 and ES7 magic
22 | * [Webpack](http://webpack.github.io) for bundling
23 | * [Webpack Dev Middleware](http://webpack.github.io/docs/webpack-dev-middleware.html)
24 | * [Webpack Hot Middleware](https://github.com/glenjamin/webpack-hot-middleware)
25 | * [Redux](https://github.com/rackt/redux)'s futuristic [Flux](https://facebook.github.io/react/blog/2014/05/06/flux.html) implementation
26 | * [Redux Dev Tools](https://github.com/gaearon/redux-devtools) for next generation DX (developer experience). Watch [Dan Abramov's talk](https://www.youtube.com/watch?v=xsSnOQynTHs).
27 | * [React Router Redux](https://github.com/reactjs/react-router-redux) Redux/React Router bindings.
28 | * [ESLint](http://eslint.org) to maintain a consistent code style
29 | * [redux-form](https://github.com/erikras/redux-form) to manage form state in Redux
30 | * [lru-memoize](https://github.com/erikras/lru-memoize) to speed up form validation
31 | * [multireducer](https://github.com/erikras/multireducer) to combine single reducers into one key-based reducer
32 | * [style-loader](https://github.com/webpack/style-loader), [sass-loader](https://github.com/jtangelder/sass-loader) and [less-loader](https://github.com/webpack/less-loader) to allow import of stylesheets in plain css, sass and less,
33 | * [bootstrap-sass-loader](https://github.com/shakacode/bootstrap-sass-loader) and [font-awesome-webpack](https://github.com/gowravshekar/font-awesome-webpack) to customize Bootstrap and FontAwesome
34 | * [react-helmet](https://github.com/nfl/react-helmet) to manage title and meta tag information on both server and client
35 | * [webpack-isomorphic-tools](https://github.com/halt-hammerzeit/webpack-isomorphic-tools) to allow require() work for statics both on client and server
36 | * [mocha](https://mochajs.org/) to allow writing unit tests for the project.
37 |
38 | 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.
39 |
40 | ## Installation
41 |
42 | ```bash
43 | npm install
44 | ```
45 |
46 | ## Running Dev Server
47 |
48 | ```bash
49 | npm run dev
50 | ```
51 |
52 | The first time it may take a little while to generate the first `webpack-assets.json` and complain with a few dozen `[webpack-isomorphic-tools] (waiting for the first Webpack build to finish)` printouts, but be patient. Give it 30 seconds.
53 |
54 | ### Using Redux DevTools
55 |
56 | [Redux Devtools](https://github.com/gaearon/redux-devtools) are enabled by default in development.
57 |
58 | - CTRL+H Toggle DevTools Dock
59 | - CTRL+Q Move DevTools Dock Position
60 | - see [redux-devtools-dock-monitor](https://github.com/gaearon/redux-devtools-dock-monitor) for more detailed information.
61 |
62 | If you have the
63 | [Redux DevTools chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) installed it will automatically be used on the client-side instead.
64 |
65 | If you want to disable the dev tools during development, set `__DEVTOOLS__` to `false` in `/webpack/dev.config.js`.
66 | DevTools are not enabled during production.
67 |
68 | ## Building and Running Production Server
69 |
70 | ```bash
71 | npm run build
72 | npm run start
73 | ```
74 |
75 | ## Demo
76 |
77 | 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).
78 |
79 | ## Documentation
80 |
81 | * [Exploring the Demo App](docs/ExploringTheDemoApp/ExploringTheDemoApp.md) is a guide that can be used before you install the kit.
82 | * [Installing the Kit](docs/InstallingTheKit/InstallingTheKit.md) guides you through installation and running the development server locally.
83 | * [Adding Text to the Home Page](docs/AddingToHomePage/AddingToHomePage.md) guides you through adding "Hello, World!" to the home page.
84 | * [Adding A Page](docs/AddingAPage/AddingAPage.md) guides you through adding a new page.
85 | * [React Tutorial - Converting Reflux to Redux](http://engineering.wework.com/process/2015/10/01/react-reflux-to-redux/), by Matt Star
86 | If you are the kind of person that learns best by following along a tutorial, I can recommend Matt Star's overview and examples.
87 |
88 |
89 | ## Explanation
90 |
91 | What initially gets run is `bin/server.js`, which does little more than enable ES6 and ES7 awesomeness in the
92 | server-side node code. It then initiates `server.js`. In `server.js` we proxy any requests to `/api/*` to the
93 | [API server](#api-server), running at `localhost:3030`. All the data fetching calls from the client go to `/api/*`.
94 | Aside from serving the favicon and static content from `/static`, the only thing `server.js` does is initiate delegate
95 | rendering to `react-router`. At the bottom of `server.js`, we listen to port `3000` and initiate the API server.
96 |
97 | #### Routing and HTML return
98 |
99 | The primary section of `server.js` generates an HTML page with the contents returned by `react-router`. First we instantiate an `ApiClient`, a facade that both server and client code use to talk to the API server. On the server side, `ApiClient` is given the request object so that it can pass along the session cookie to the API server to maintain session state. We pass this API client facade to the `redux` middleware so that the action creators have access to it.
100 |
101 | Then we perform [server-side data fetching](#server-side-data-fetching), wait for the data to be loaded, and render the page with the now-fully-loaded `redux` state.
102 |
103 | The last interesting bit of the main routing section of `server.js` is that we swap in the hashed script and css from the `webpack-assets.json` that the Webpack Dev Server – or the Webpack build process on production – has spit out on its last run. You won't have to deal with `webpack-assets.json` manually because [webpack-isomorphic-tools](https://github.com/halt-hammerzeit/webpack-isomorphic-tools) take care of that.
104 |
105 | We also spit out the `redux` state into a global `window.__data` variable in the webpage to be loaded by the client-side `redux` code.
106 |
107 | #### Server-side Data Fetching
108 |
109 | The [redux-connect](https://www.npmjs.com/package/redux-connect) package exposes an API to return promises that need to be fulfilled before a route is rendered. It exposes a `` container, which wraps our render tree on both [server](https://github.com/erikras/react-redux-universal-hot-example/blob/master/src/server.js) and [client](https://github.com/erikras/react-redux-universal-hot-example/blob/master/src/client.js). More documentation is available on the [redux-connect](https://www.npmjs.com/package/redux-connect) page.
110 |
111 | #### Client Side
112 |
113 | The client side entry point is reasonably named `client.js`. All it does is load the routes, initiate `react-router`, rehydrate the redux state from the `window.__data` passed in from the server, and render the page over top of the server-rendered DOM. This makes React enable all its event listeners without having to re-render the DOM.
114 |
115 | #### Redux Middleware
116 |
117 | The middleware, [`clientMiddleware.js`](https://github.com/erikras/react-redux-universal-hot-example/blob/master/src/redux/middleware/clientMiddleware.js), serves two functions:
118 |
119 | 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.
120 | 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/rackt/redux/issues/99), which you may prefer, but to the author of this example, the middleware way feels cleanest.
121 |
122 | #### Redux Modules... *What the Duck*?
123 |
124 | The `src/redux/modules` folder contains "modules" to help
125 | isolate concerns within a Redux application (aka [Ducks](https://github.com/erikras/ducks-modular-redux), a Redux Style Proposal that I came up with). I encourage you to read the
126 | [Ducks Docs](https://github.com/erikras/ducks-modular-redux) and provide feedback.
127 |
128 | #### API Server
129 |
130 | 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.
131 |
132 | #### Getting data and actions into components
133 |
134 | 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.
135 |
136 | #### Images
137 |
138 | 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):
139 |
140 | ```javascript
141 | let logoImage = require('./logo.png');
142 | ```
143 |
144 | #### Styles
145 |
146 | 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 `render()` function in your React Component, and then you use the classnames returned from that import. Like so:
147 |
148 | ```javascript
149 | render() {
150 | const styles = require('./App.scss');
151 | ...
152 | ```
153 |
154 | Then you set the `className` of your element to match one of the CSS classes in your SCSS file, and you're good to go!
155 |
156 | ```jsx
157 |
...
158 | ```
159 |
160 | #### Alternative to Local Styles
161 |
162 | If you'd like to use plain inline styles this is possible with a few modifications to your webpack configuration.
163 |
164 | **1. Configure Isomorphic Tools to Accept CSS**
165 |
166 | In `webpack-isomorphic-tools.js` add **css** to the list of style module extensions
167 |
168 | ```javascript
169 | style_modules: {
170 | extensions: ['less','scss','css'],
171 | ```
172 |
173 | **2. Add a CSS loader to webpack dev config**
174 |
175 | In `dev.config.js` modify **module loaders** to include a test and loader for css
176 |
177 | ```javascript
178 | module: {
179 | loaders: [
180 | { test: /\.css$/, loader: 'style-loader!css-loader'},
181 | ```
182 |
183 | **3. Add a CSS loader to the webpack prod config**
184 |
185 | You must use the **ExtractTextPlugin** in this loader. In `prod.config.js` modify **module loaders** to include a test and loader for css
186 |
187 | ```javascript
188 | module: {
189 | loaders: [
190 | { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader')},
191 | ```
192 |
193 | **Now you may simply omit assigning the `required` stylesheet to a variable and keep it at the top of your `render()` function.**
194 |
195 | ```javascript
196 | render() {
197 | require('./App.css');
198 | require('aModule/dist/style.css');
199 | ...
200 | ```
201 |
202 | **NOTE** In order to use this method with **scss or less** files one more modification must be made. In both `dev.config.js` and `prod.config.js` in the loaders for less and scss files remove
203 |
204 | 1. `modules`
205 | 2. `localIdentName...`
206 |
207 | Before:
208 | ```javascript
209 | { test: /\.less$/, loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!less?outputStyle=expanded&sourceMap' },
210 | ```
211 | After:
212 | ```javascript
213 | { test: /\.less$/, loader: 'style!css?importLoaders=2&sourceMap!autoprefixer?browsers=last 2 version!less?outputStyle=expanded&sourceMap' },
214 | ```
215 |
216 | After this modification to both loaders you will be able to use scss and less files in the same way as css files.
217 |
218 | #### Unit Tests
219 |
220 | The project uses [Mocha](https://mochajs.org/) to run your unit tests, it uses [Karma](http://karma-runner.github.io/0.13/index.html) as the test runner, it enables the feature that you are able to render your tests to the browser (e.g: Firefox, Chrome etc.), which means you are able to use the [Test Utilities](http://facebook.github.io/react/docs/test-utils.html) from Facebook api like `renderIntoDocument()`.
221 |
222 | To run the tests in the project, just simply run `npm test` if you have `Chrome` installed, it will be automatically launched as a test service for you.
223 |
224 | To keep watching your test suites that you are working on, just set `singleRun: false` in the `karma.conf.js` file. Please be sure set it to `true` if you are running `npm test` on a continuous integration server (travis-ci, etc).
225 |
226 | ## Deployment on Heroku
227 |
228 | To get this project to work on Heroku, you need to:
229 |
230 | 1. Remove the `"PORT": 8080` line from the `betterScripts` / `start-prod` section of `package.json`.
231 | 2. `heroku config:set NODE_ENV=production`
232 | 3. `heroku config:set NODE_PATH=./src`
233 | 4. `heroku config:set NPM_CONFIG_PRODUCTION=false`
234 | * This is to enable webpack to run the build on deploy.
235 |
236 | The first deploy might take a while, but after that your `node_modules` dir should be cached.
237 |
238 | ## FAQ
239 |
240 | This project moves fast and has an active community, so if you have a question that is not answered below please visit our [Discord channel](https://discord.gg/0ZcbPKXt5bZZb1Ko) or file an issue.
241 |
242 |
243 | ## Roadmap
244 |
245 | Although this isn't a library, we recently started versioning to make it easier to track breaking changes and emerging best practices.
246 |
247 | * [Inline Styles](docs/InlineStyles.md) - CSS is dead
248 |
249 | ## Contributing
250 |
251 | I am more than happy to accept external contributions to the project in the form of feedback, bug reports and even better - pull requests :)
252 |
253 | If you would like to submit a pull request, please make an effort to follow the guide in [CONTRIBUTING.md](CONTRIBUTING.md).
254 |
255 | ---
256 | Thanks for checking this out.
257 |
258 | – Erik Rasmussen, [@erikras](https://twitter.com/erikras)
259 |
--------------------------------------------------------------------------------