19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/02-react-node/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "02-react-node",
3 | "version": "1.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node index"
7 | },
8 | "license": "MIT",
9 | "devDependencies": {
10 | "babel-preset-es2015": "^6.6.0",
11 | "babel-preset-react": "^6.5.0",
12 | "babel-register": "^6.8.0"
13 | },
14 | "dependencies": {
15 | "react": "^15.3.1",
16 | "react-dom": "^15.3.1"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/lewis617/react-redux-book.git"
21 | },
22 | "bugs": {
23 | "url": "https://github.com/lewis617/react-redux-book/issues"
24 | },
25 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
26 | }
27 |
--------------------------------------------------------------------------------
/13-counter-test/actions/index.js:
--------------------------------------------------------------------------------
1 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
2 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
3 |
4 | export function increment() {
5 | return {
6 | type: INCREMENT_COUNTER,
7 | };
8 | }
9 |
10 | export function decrement() {
11 | return {
12 | type: DECREMENT_COUNTER,
13 | };
14 | }
15 |
16 | export function incrementIfOdd() {
17 | return (dispatch, getState) => {
18 | const { counter } = getState();
19 |
20 | if (counter % 2 === 0) {
21 | return;
22 | }
23 |
24 | dispatch(increment());
25 | };
26 | }
27 |
28 | export function incrementAsync(delay = 1000) {
29 | return dispatch => {
30 | setTimeout(() => {
31 | dispatch(increment());
32 | }, delay);
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/19-3-isomophic-counter/server/dev-server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var Express = require('express');
3 |
4 | var webpack = require('webpack');
5 | var webpackDevMiddleware = require('webpack-dev-middleware');
6 | var webpackHotMiddleware = require('webpack-hot-middleware');
7 | var webpackConfig = require('../webpack.config');
8 |
9 | var app = new Express();
10 | var port = 3001;
11 |
12 | var compiler = webpack(webpackConfig);
13 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath }));
14 | app.use(webpackHotMiddleware(compiler));
15 |
16 | app.listen(port, (error) => {
17 | if (error) {
18 | console.error(error)
19 | } else {
20 | console.info(`==> 🚧 Webpack development server listening on port ${port}.`)
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/23-26-production/webpack/webpack-dev-server.js:
--------------------------------------------------------------------------------
1 | var Express = require('express');
2 |
3 | var webpack = require('webpack');
4 | var webpackDevMiddleware = require('webpack-dev-middleware');
5 | var webpackHotMiddleware = require('webpack-hot-middleware');
6 | var webpackConfig = require('./dev.config');
7 |
8 | var app = new Express();
9 | var port = require('../src/config').port + 1;
10 |
11 | var compiler = webpack(webpackConfig);
12 | app.use(webpackDevMiddleware(compiler, {noInfo: true, publicPath: webpackConfig.output.publicPath}));
13 | app.use(webpackHotMiddleware(compiler));
14 |
15 | app.listen(port, (error) => {
16 | if (error) {
17 | console.error(error)
18 | } else {
19 | console.info(`==> 🚧 Webpack development server listening on port ${port}.`)
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/06-state-props-context/src/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | function Content(props) {
4 | return Content组件的props.value:{props.value}
;
5 | }
6 |
7 | Content.propTypes = {
8 | value: PropTypes.number.isRequired
9 | };
10 |
11 | export default class Counter extends Component {
12 | constructor() {
13 | super();
14 | this.state = { value: 0 };
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
23 |
24 | Counter组件的内部状态:
25 |
{JSON.stringify(this.state, null, 2)}
26 |
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/13-counter-test/test/reducers/counter.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import counter from '../../reducers/counter';
3 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../actions';
4 |
5 | describe('reducers', () => {
6 | describe('counter', () => {
7 | it('should handle initial state', () => {
8 | expect(counter(undefined, {})).toBe(0);
9 | });
10 |
11 | it('should handle INCREMENT_COUNTER', () => {
12 | expect(counter(1, { type: INCREMENT_COUNTER })).toBe(2);
13 | });
14 |
15 | it('should handle DECREMENT_COUNTER', () => {
16 | expect(counter(1, { type: DECREMENT_COUNTER })).toBe(0);
17 | });
18 |
19 | it('should handle unknown action type', () => {
20 | expect(counter(1, { type: 'unknown' })).toBe(1);
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/18-universal/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'cheap-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './client/index.js'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurrenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [
21 | {
22 | test: /\.js$/,
23 | loader: 'babel',
24 | exclude: /node_modules/,
25 | include: __dirname,
26 | query: {
27 | presets: [ 'react-hmre' ]
28 | }
29 | }
30 | ]
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/19-2-isomorphic-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "19-2-isomorphic-client",
3 | "version": "1.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "http-server -p 3000",
7 | "build": "webpack"
8 | },
9 | "license": "MIT",
10 | "dependencies": {
11 | "file-loader": "^0.9.0",
12 | "http-server": "^0.9.0",
13 | "url-loader": "^0.5.7",
14 | "webpack-isomorphic-tools": "^2.2.48"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/lewis617/react-redux-book.git"
19 | },
20 | "bugs": {
21 | "url": "https://github.com/lewis617/react-redux-book/issues"
22 | },
23 | "homepage": "https://github.com/lewis617/react-redux-book#readme",
24 | "devDependencies": {
25 | "webpack": "^1.13.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/19-3-isomophic-counter/webpack-isomorphic-tools.js:
--------------------------------------------------------------------------------
1 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
2 |
3 | var config = {
4 | assets: {
5 | images: {extensions: ['png']},
6 | style_modules: {
7 | extensions: ['css'],
8 | filter: function(module, regex, options, log) {
9 | return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
10 | },
11 | path: function(module, options, log) {
12 | return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
13 | },
14 | parser: function(module, options, log) {
15 | return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
16 | }
17 | }
18 | }
19 | };
20 |
21 | module.exports = config;
22 |
--------------------------------------------------------------------------------
/20-universal-router/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'cheap-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './client/index.js'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurrenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [
21 | {
22 | test: /\.js$/,
23 | loader: 'babel',
24 | exclude: /node_modules/,
25 | include: __dirname,
26 | query: {
27 | presets: [ 'react-hmre' ]
28 | }
29 | }
30 | ]
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/14-15-todomvc/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import TodoTextInput from './TodoTextInput';
3 |
4 | class Header extends Component {
5 | constructor() {
6 | super();
7 | this.handleSave = this.handleSave.bind(this);
8 | }
9 |
10 | handleSave(text) {
11 | if (text.length !== 0) {
12 | this.props.addTodo(text);
13 | }
14 | }
15 |
16 | render() {
17 | return (
18 |
26 | );
27 | }
28 | }
29 |
30 | Header.propTypes = {
31 | addTodo: PropTypes.func.isRequired,
32 | };
33 |
34 | export default Header;
35 |
--------------------------------------------------------------------------------
/11-counter-connect/actions/index.js:
--------------------------------------------------------------------------------
1 | // 这里将action对象的type属性值写成了常量,便于reducer引用,减少了出错的概率。
2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
4 |
5 | export function increment() {
6 | return {
7 | type: INCREMENT_COUNTER,
8 | };
9 | }
10 |
11 | export function decrement() {
12 | return {
13 | type: DECREMENT_COUNTER,
14 | };
15 | }
16 |
17 | export function incrementIfOdd() {
18 | return (dispatch, getState) => {
19 | const { counter } = getState();
20 |
21 | if (counter % 2 === 0) {
22 | return;
23 | }
24 |
25 | dispatch(increment());
26 | };
27 | }
28 |
29 | export function incrementAsync(delay = 1000) {
30 | return dispatch => {
31 | setTimeout(() => {
32 | dispatch(increment());
33 | }, delay);
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/05-jsx/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/10-counter/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/16-async/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/04-dev-server/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/13-counter-test/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/14-15-todomvc/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/16-async/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunkMiddleware from 'redux-thunk';
3 | import createLogger from 'redux-logger';
4 | import rootReducer from '../reducers';
5 |
6 | export default function configureStore(initialState) {
7 | const store = createStore(
8 | rootReducer,
9 | initialState,
10 | compose(
11 | applyMiddleware(thunkMiddleware, createLogger()),
12 | window.devToolsExtension ? window.devToolsExtension() : f => f
13 | )
14 | );
15 |
16 | if (module.hot) {
17 | // Enable Webpack hot module replacement for reducers
18 | module.hot.accept('../reducers', () => {
19 | const nextRootReducer = require('../reducers').default;
20 | store.replaceReducer(nextRootReducer);
21 | });
22 | }
23 |
24 | return store;
25 | }
26 |
--------------------------------------------------------------------------------
/17-real-world/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/22-bootstrap/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/07-element-instance/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/11-counter-connect/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/12-undo-devtools/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/06-state-props-context/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var webpack = require('webpack');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config');
6 |
7 | var app = new (require('express'))();
8 | var port = 3000;
9 |
10 | var compiler = webpack(config);
11 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
12 | app.use(webpackHotMiddleware(compiler));
13 |
14 | app.get("/", function(req, res) {
15 | res.sendFile(__dirname + '/index.html')
16 | });
17 |
18 | app.listen(port, function(error) {
19 | if (error) {
20 | console.error(error)
21 | } else {
22 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/14-15-todomvc/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurrenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin()
18 | ],
19 | module: {
20 | loaders: [
21 | {
22 | test: /\.js$/,
23 | loaders: [ 'babel' ],
24 | exclude: /node_modules/,
25 | include: __dirname
26 | },
27 | {
28 | test: /\.css?$/,
29 | loaders: [ 'style', 'raw' ],
30 | include: __dirname
31 | }
32 | ]
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/23-26-production/bin/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var path = require('path');
3 | var rootDir = path.resolve(__dirname, '..');
4 |
5 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools');
6 |
7 | global.__SERVER__ = true;
8 | global.__DISABLE_SSR__ = false;
9 | global.__COOKIE__ = null;
10 |
11 | if (process.env.NODE_ENV === 'production') {
12 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(
13 | require('../webpack/webpack-isomorphic-tools'))
14 | .server(rootDir, function () {
15 | require('../build/server');
16 | });
17 | }
18 | else {
19 | require('babel-register');
20 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(
21 | require('../webpack/webpack-isomorphic-tools'))
22 | .development()
23 | .server(rootDir, function () {
24 | require('../src/server');
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/13-counter-test/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | function Counter({ increment, incrementIfOdd, incrementAsync, decrement, counter }) {
4 | return (
5 |
6 | Clicked: {counter} times
7 | {' '}
8 |
9 | {' '}
10 |
11 | {' '}
12 |
13 | {' '}
14 |
15 |
16 | );
17 | }
18 |
19 | Counter.propTypes = {
20 | counter: PropTypes.number.isRequired,
21 | increment: PropTypes.func.isRequired,
22 | incrementIfOdd: PropTypes.func.isRequired,
23 | incrementAsync: PropTypes.func.isRequired,
24 | decrement: PropTypes.func.isRequired
25 | };
26 |
27 | export default Counter;
28 |
--------------------------------------------------------------------------------
/11-counter-connect/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | function Counter({ counter, increment, decrement, incrementIfOdd, incrementAsync }) {
4 | return (
5 |
6 | Clicked: {counter} times
7 | {' '}
8 |
9 | {' '}
10 |
11 | {' '}
12 |
13 | {' '}
14 |
15 |
16 | );
17 | }
18 |
19 | Counter.propTypes = {
20 | counter: PropTypes.number.isRequired,
21 | increment: PropTypes.func.isRequired,
22 | incrementIfOdd: PropTypes.func.isRequired,
23 | incrementAsync: PropTypes.func.isRequired,
24 | decrement: PropTypes.func.isRequired
25 | };
26 |
27 | export default Counter;
28 |
--------------------------------------------------------------------------------
/18-universal/common/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | function Counter({ increment, incrementIfOdd, incrementAsync, decrement, counter }) {
4 | return (
5 |
6 | Clicked: {counter} times
7 | {' '}
8 |
9 | {' '}
10 |
11 | {' '}
12 |
13 | {' '}
14 |
15 |
16 | );
17 | }
18 |
19 | Counter.propTypes = {
20 | increment: PropTypes.func.isRequired,
21 | incrementIfOdd: PropTypes.func.isRequired,
22 | incrementAsync: PropTypes.func.isRequired,
23 | decrement: PropTypes.func.isRequired,
24 | counter: PropTypes.number.isRequired,
25 | };
26 |
27 | export default Counter;
28 |
--------------------------------------------------------------------------------
/07-element-instance/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 | import App from './src/App';
4 |
5 | console.log('首次挂载');
6 | let instance = render(, document.getElementById('root'));
7 |
8 | window.renderComponent = () => {
9 | console.log('挂载');
10 | instance = render(, document.getElementById('root'));
11 | };
12 |
13 | window.setState = () => {
14 | console.log('更新');
15 | instance.setState({ foo: 'bar' });
16 | };
17 |
18 | window.unmountComponentAtNode = () => {
19 | console.log('卸载');
20 | unmountComponentAtNode(document.getElementById('root'));
21 | };
22 |
23 | console.log('JSX中的闭合标签是ReactElement');
24 | console.log(hello world
);
25 | console.log();
26 |
27 | console.log('组件、ReactElement与组件实例');
28 | console.log(App);
29 | console.log();
30 | console.log(instance);
31 |
--------------------------------------------------------------------------------
/17-real-world/store/configureStore.dev.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import createLogger from 'redux-logger';
4 | import api from '../middleware/api';
5 | import rootReducer from '../reducers';
6 | import DevTools from '../containers/DevTools';
7 |
8 | export default function configureStore(initialState) {
9 | const store = createStore(
10 | rootReducer,
11 | initialState,
12 | compose(
13 | applyMiddleware(thunk, api, createLogger()),
14 | DevTools.instrument()
15 | )
16 | );
17 |
18 | if (module.hot) {
19 | // Enable Webpack hot module replacement for reducers
20 | module.hot.accept('../reducers', () => {
21 | const nextRootReducer = require('../reducers').default;
22 | store.replaceReducer(nextRootReducer);
23 | });
24 | }
25 |
26 | return store;
27 | }
28 |
--------------------------------------------------------------------------------
/21-async-router/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'cheap-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './client/index.js'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurrenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.DefinePlugin({
19 | __SERVER__: false
20 | })
21 | ],
22 | module: {
23 | loaders: [
24 | {
25 | test: /\.js$/,
26 | loader: 'babel',
27 | exclude: /node_modules/,
28 | include: __dirname,
29 | query: {
30 | presets: [ 'react-hmre' ]
31 | }
32 | }
33 | ]
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/03-react-browser/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "03-react-browser",
3 | "version": "1.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "build": "webpack",
7 | "start": "webpack && http-server -p 3000"
8 | },
9 | "license": "MIT",
10 | "devDependencies": {
11 | "babel-core": "^6.8.0",
12 | "babel-loader": "^6.2.4",
13 | "babel-preset-es2015": "^6.6.0",
14 | "babel-preset-react": "^6.5.0",
15 | "webpack": "^1.13.0"
16 | },
17 | "dependencies": {
18 | "http-server": "^0.9.0",
19 | "react": "^15.3.1",
20 | "react-dom": "^15.3.1"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/lewis617/react-redux-book.git"
25 | },
26 | "bugs": {
27 | "url": "https://github.com/lewis617/react-redux-book/issues"
28 | },
29 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
30 | }
31 |
--------------------------------------------------------------------------------
/17-real-world/components/Repo.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 |
4 | function Repo({ repo, owner }) {
5 | const { login } = owner;
6 | const { name, description } = repo;
7 |
8 | return (
9 |
10 |
11 |
12 | {name}
13 |
14 | {' by '}
15 |
16 | {login}
17 |
18 |
19 | {description &&
20 |
{description}
21 | }
22 |
23 | );
24 | }
25 |
26 | Repo.propTypes = {
27 | repo: PropTypes.shape({
28 | name: PropTypes.string.isRequired,
29 | description: PropTypes.string,
30 | }).isRequired,
31 | owner: PropTypes.shape({
32 | login: PropTypes.string.isRequired,
33 | }).isRequired,
34 | };
35 |
36 | export default Repo;
37 |
--------------------------------------------------------------------------------
/21-async-router/common/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import { asyncMiddleware } from 'redux-amrc';
4 | import rootReducer from '../reducers';
5 |
6 | export default function configureStore(initialState) {
7 | const store = createStore(
8 | rootReducer,
9 | initialState,
10 | compose(
11 | applyMiddleware(thunk, asyncMiddleware),
12 | typeof window === 'object' &&
13 | typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : f => f
14 | )
15 | );
16 |
17 | if (module.hot) {
18 | // Enable Webpack hot module replacement for reducers
19 | module.hot.accept('../reducers', () => {
20 | const nextRootReducer = require('../reducers').default;
21 | store.replaceReducer(nextRootReducer);
22 | });
23 | }
24 |
25 | return store;
26 | }
27 |
--------------------------------------------------------------------------------
/23-26-production/test/reducers/counter.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import counter from '../../src/reducers/counter';
3 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../src/actions/counter';
4 |
5 | describe('reducers', () => {
6 | describe('counter', () => {
7 | it('should handle INCREMENT_COUNTER action', () => {
8 | expect(counter({
9 | value: 0
10 | }, { type: INCREMENT_COUNTER })).toEqual({
11 | value: 1
12 | });
13 | });
14 |
15 | it('should handle DECREMENT_COUNTER action', () => {
16 | expect(counter({
17 | value: 1
18 | }, { type: DECREMENT_COUNTER })).toEqual({
19 | value: 0
20 | });
21 | });
22 |
23 | it('should ignore unknown actions', () => {
24 | expect(counter({
25 | value: 0
26 | }, { type: 'unknown' })).toEqual({
27 | value: 0
28 | });
29 | });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/11-counter-connect/containers/Connect4.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { increment, decrement, incrementIfOdd, incrementAsync } from '../actions';
4 |
5 | function Counter({ counter, dispatch }) {
6 | return (
7 |
8 | Clicked: {counter} times
9 | {' '}
10 |
11 | {' '}
12 |
13 | {' '}
14 |
15 | {' '}
16 |
17 |
18 | );
19 | }
20 |
21 | Counter.propTypes = {
22 | counter: PropTypes.number.isRequired,
23 | dispatch: PropTypes.func.isRequired
24 | };
25 |
26 | export default connect(
27 | state => ({ counter: state.counter })
28 | )(Counter);
29 |
--------------------------------------------------------------------------------
/18-universal/common/actions/index.js:
--------------------------------------------------------------------------------
1 | export const SET_COUNTER = 'SET_COUNTER';
2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
4 |
5 | export function set(value) {
6 | return {
7 | type: SET_COUNTER,
8 | payload: value,
9 | };
10 | }
11 |
12 | export function increment() {
13 | return {
14 | type: INCREMENT_COUNTER,
15 | };
16 | }
17 |
18 | export function decrement() {
19 | return {
20 | type: DECREMENT_COUNTER,
21 | };
22 | }
23 |
24 | export function incrementIfOdd() {
25 | return (dispatch, getState) => {
26 | const { counter } = getState();
27 |
28 | if (counter % 2 === 0) {
29 | return;
30 | }
31 |
32 | dispatch(increment());
33 | };
34 | }
35 |
36 | export function incrementAsync(delay = 1000) {
37 | return dispatch => {
38 | setTimeout(() => {
39 | dispatch(increment());
40 | }, delay);
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/23-26-production/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | import 'isomorphic-fetch';
2 | import config from '../config';
3 |
4 | function handle401(res) {
5 | if (res.status === 401 && !__SERVER__) {
6 | window.location.reload();
7 | }
8 | return res;
9 | }
10 |
11 | function handleErrors(res) {
12 | if (!res.ok) {
13 | throw new Error(res.statusText);
14 | }
15 | return res.json();
16 | }
17 |
18 | export function customFetch(url, option) {
19 | const prefix = __SERVER__ ? 'http://' + config.apiHost + ':' + config.apiPort : '/api';
20 |
21 | let opt = option || {};
22 | if (__SERVER__) {
23 | opt = {
24 | ...opt,
25 | headers: {
26 | ...opt.headers,
27 | cookie: __COOKIE__
28 | }
29 | };
30 | } else {
31 | opt = {
32 | ...opt,
33 | credentials: 'same-origin'
34 | };
35 | }
36 |
37 | return fetch(prefix + url, opt)
38 | .then(handle401)
39 | .then(handleErrors);
40 | }
41 |
--------------------------------------------------------------------------------
/20-universal-router/common/actions/index.js:
--------------------------------------------------------------------------------
1 | export const SET_COUNTER = 'SET_COUNTER';
2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
4 |
5 | export function set(value) {
6 | return {
7 | type: SET_COUNTER,
8 | payload: value,
9 | };
10 | }
11 |
12 | export function increment() {
13 | return {
14 | type: INCREMENT_COUNTER,
15 | };
16 | }
17 |
18 | export function decrement() {
19 | return {
20 | type: DECREMENT_COUNTER,
21 | };
22 | }
23 |
24 | export function incrementIfOdd() {
25 | return (dispatch, getState) => {
26 | const { counter } = getState();
27 |
28 | if (counter % 2 === 0) {
29 | return;
30 | }
31 |
32 | dispatch(increment());
33 | };
34 | }
35 |
36 | export function incrementAsync(delay = 1000) {
37 | return dispatch => {
38 | setTimeout(() => {
39 | dispatch(increment());
40 | }, delay);
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/23-26-production/src/client.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import { Provider } from 'react-redux';
5 | import { match, Router, browserHistory } from 'react-router';
6 | import configureStore from './utils/configureStore';
7 | import getRoutes from './routes';
8 |
9 | const initialState = window.__INITIAL_STATE__;
10 | const store = configureStore(initialState);
11 | const rootElement = document.getElementById('app');
12 | const history = browserHistory;
13 | const routes = getRoutes(store);
14 |
15 | match({ history, routes }, (err, redirect, renderProps) => {
16 | if (redirect) {
17 | history.replace(redirect);
18 | } else if (err) {
19 | history.goBack();
20 | console.error(err.stack);
21 | } else {
22 | render(
23 |
24 |
25 | ,
26 | rootElement
27 | );
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/12-undo-devtools/containers/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import * as ActionCreators from '../actions';
4 |
5 | function Counter({ increment, decrement, undo, redo, value }) {
6 | return (
7 |
8 | Clicked: {value} times
9 | {' '}
10 |
11 | {' '}
12 |
13 | {' '}
14 |
15 | {' '}
16 |
17 |
18 | );
19 | }
20 |
21 | Counter.propTypes = {
22 | increment: PropTypes.func.isRequired,
23 | decrement: PropTypes.func.isRequired,
24 | undo: PropTypes.func.isRequired,
25 | redo: PropTypes.func.isRequired,
26 | value: PropTypes.number.isRequired
27 | };
28 |
29 | export default connect(
30 | state => ({ value: state.counter.present }),
31 | ActionCreators
32 | )(Counter);
33 |
--------------------------------------------------------------------------------
/19-3-isomophic-counter/common/actions/index.js:
--------------------------------------------------------------------------------
1 | export const SET_COUNTER = 'SET_COUNTER';
2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
4 |
5 | export function set(value) {
6 | return {
7 | type: SET_COUNTER,
8 | payload: value,
9 | };
10 | }
11 |
12 | export function increment() {
13 | return {
14 | type: INCREMENT_COUNTER,
15 | };
16 | }
17 |
18 | export function decrement() {
19 | return {
20 | type: DECREMENT_COUNTER,
21 | };
22 | }
23 |
24 | export function incrementIfOdd() {
25 | return (dispatch, getState) => {
26 | const { counter } = getState();
27 |
28 | if (counter % 2 === 0) {
29 | return;
30 | }
31 |
32 | dispatch(increment());
33 | };
34 | }
35 |
36 | export function incrementAsync(delay = 1000) {
37 | return dispatch => {
38 | setTimeout(() => {
39 | dispatch(increment());
40 | }, delay);
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/05-jsx/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "05-jsx",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "react": "^15.3.1",
10 | "react-dom": "^15.3.1"
11 | },
12 | "devDependencies": {
13 | "babel-core": "^6.3.15",
14 | "babel-loader": "^6.2.0",
15 | "babel-preset-es2015": "^6.3.13",
16 | "babel-preset-react": "^6.3.13",
17 | "babel-preset-react-hmre": "^1.1.1",
18 | "express": "^4.13.3",
19 | "webpack": "^1.9.11",
20 | "webpack-dev-middleware": "^1.2.0",
21 | "webpack-hot-middleware": "^2.9.1"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/lewis617/react-redux-book.git"
26 | },
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/lewis617/react-redux-book/issues"
30 | },
31 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
32 | }
33 |
--------------------------------------------------------------------------------
/21-async-router/client/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import { Provider } from 'react-redux';
5 | import { Router, browserHistory, match } from 'react-router';
6 |
7 | import configureStore from '../common/store/configureStore';
8 | import getRoutes from '../common/routes';
9 |
10 | const initialState = window.__INITIAL_STATE__;
11 | const store = configureStore(initialState);
12 | const rootElement = document.getElementById('app');
13 | const routes = getRoutes(store);
14 | const history = browserHistory;
15 |
16 | match({ history, routes }, (err, redirect, renderProps) => {
17 | if (redirect) {
18 | history.replace(redirect);
19 | } else if (err) {
20 | history.goBack();
21 | console.error(err.stack);
22 | } else {
23 | render(
24 |
25 |
26 | ,
27 | rootElement
28 | );
29 | }
30 | });
31 |
--------------------------------------------------------------------------------
/04-dev-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "04-dev-server",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "react": "^15.3.1",
10 | "react-dom": "^15.3.1"
11 | },
12 | "devDependencies": {
13 | "babel-core": "^6.3.15",
14 | "babel-loader": "^6.2.0",
15 | "babel-preset-es2015": "^6.3.13",
16 | "babel-preset-react": "^6.3.13",
17 | "babel-preset-react-hmre": "^1.1.1",
18 | "express": "^4.13.3",
19 | "webpack": "^1.9.11",
20 | "webpack-dev-middleware": "^1.2.0",
21 | "webpack-hot-middleware": "^2.9.1"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/lewis617/react-redux-book.git"
26 | },
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/lewis617/react-redux-book/issues"
30 | },
31 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
32 | }
33 |
--------------------------------------------------------------------------------
/14-15-todomvc/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import Header from '../components/Header';
5 | import MainSection from '../components/MainSection';
6 | import * as TodoActions from '../actions';
7 |
8 | function App({ todos, actions }) {
9 | return (
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | App.propTypes = {
18 | todos: PropTypes.array.isRequired,
19 | actions: PropTypes.object.isRequired,
20 | };
21 |
22 | function mapStateToProps(state) {
23 | return {
24 | todos: state.todos,
25 | };
26 | }
27 |
28 | function mapDispatchToProps(dispatch) {
29 | return {
30 | actions: bindActionCreators(TodoActions, dispatch),
31 | };
32 | }
33 |
34 | export default connect(
35 | mapStateToProps,
36 | mapDispatchToProps
37 | )(App);
38 |
--------------------------------------------------------------------------------
/07-element-instance/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "07-element-instance",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "react": "^15.3.1",
10 | "react-dom": "^15.3.1"
11 | },
12 | "devDependencies": {
13 | "babel-core": "^6.3.15",
14 | "babel-loader": "^6.2.0",
15 | "babel-preset-es2015": "^6.3.13",
16 | "babel-preset-react": "^6.3.13",
17 | "babel-preset-react-hmre": "^1.1.1",
18 | "express": "^4.13.3",
19 | "webpack": "^1.9.11",
20 | "webpack-dev-middleware": "^1.2.0",
21 | "webpack-hot-middleware": "^2.9.1"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/lewis617/react-redux-book.git"
26 | },
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/lewis617/react-redux-book/issues"
30 | },
31 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
32 | }
33 |
--------------------------------------------------------------------------------
/06-state-props-context/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "06-state-props-context",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "react": "^15.3.1",
10 | "react-dom": "^15.3.1"
11 | },
12 | "devDependencies": {
13 | "babel-core": "^6.3.15",
14 | "babel-loader": "^6.2.0",
15 | "babel-preset-es2015": "^6.3.13",
16 | "babel-preset-react": "^6.3.13",
17 | "babel-preset-react-hmre": "^1.1.1",
18 | "express": "^4.13.3",
19 | "webpack": "^1.9.11",
20 | "webpack-dev-middleware": "^1.2.0",
21 | "webpack-hot-middleware": "^2.9.1"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/lewis617/react-redux-book.git"
26 | },
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/lewis617/react-redux-book/issues"
30 | },
31 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
32 | }
33 |
--------------------------------------------------------------------------------
/10-counter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "10-counter",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "react": "^15.3.1",
10 | "react-dom": "^15.3.1",
11 | "redux": "^3.2.1"
12 | },
13 | "devDependencies": {
14 | "babel-core": "^6.3.15",
15 | "babel-loader": "^6.2.0",
16 | "babel-preset-es2015": "^6.3.13",
17 | "babel-preset-react": "^6.3.13",
18 | "babel-preset-react-hmre": "^1.1.1",
19 | "babel-register": "^6.3.13",
20 | "express": "^4.13.3",
21 | "webpack": "^1.9.11",
22 | "webpack-dev-middleware": "^1.2.0",
23 | "webpack-hot-middleware": "^2.9.1"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/lewis617/react-redux-book.git"
28 | },
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/lewis617/react-redux-book/issues"
32 | },
33 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
34 | }
35 |
--------------------------------------------------------------------------------
/19-3-isomophic-counter/common/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | function Counter({ increment, incrementIfOdd, incrementAsync, decrement, counter }) {
4 | return (
5 |
6 |
11 | Clicked: {counter} times
12 | {' '}
13 |
14 | {' '}
15 |
16 | {' '}
17 |
18 | {' '}
19 |
20 |
21 | );
22 | }
23 |
24 | Counter.propTypes = {
25 | increment: PropTypes.func.isRequired,
26 | incrementIfOdd: PropTypes.func.isRequired,
27 | incrementAsync: PropTypes.func.isRequired,
28 | decrement: PropTypes.func.isRequired,
29 | counter: PropTypes.number.isRequired,
30 | };
31 |
32 | export default Counter;
33 |
--------------------------------------------------------------------------------
/23-26-production/src/actions/counter.js:
--------------------------------------------------------------------------------
1 | import { ASYNC } from 'redux-amrc';
2 | import { customFetch } from '../utils/utils';
3 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
4 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
5 |
6 | export function increment() {
7 | return {
8 | type: INCREMENT_COUNTER
9 | };
10 | }
11 |
12 | export function decrement() {
13 | return {
14 | type: DECREMENT_COUNTER
15 | };
16 | }
17 |
18 | export function incrementIfOdd() {
19 | return (dispatch, getState) => {
20 | const { async } = getState();
21 |
22 | if (async.counter.value % 2 === 0) {
23 | return;
24 | }
25 |
26 | dispatch(increment());
27 | };
28 | }
29 |
30 | export function incrementAsync(delay = 1000) {
31 | return dispatch => {
32 | setTimeout(() => {
33 | dispatch(increment());
34 | }, delay);
35 | };
36 | }
37 |
38 | export function loadCounter() {
39 | return {
40 | [ASYNC]: {
41 | key: 'counter',
42 | promise: () => customFetch('/counter')
43 | }
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/23-26-production/src/components/Table/CustomPagerComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Pagination } from 'react-bootstrap';
3 |
4 | export default class CustomPagerComponent extends Component {
5 | static propTypes = {
6 | setPage: PropTypes.any,
7 | currentPage: PropTypes.any,
8 | previous: PropTypes.any,
9 | next: PropTypes.any,
10 | maxPage: PropTypes.any
11 | };
12 | static defaultProps = {
13 | maxPage: 0,
14 | currentPage: 0
15 | };
16 |
17 | handleSelect = (eventKey) => {
18 | this.props.setPage(eventKey - 1);
19 | };
20 |
21 | render() {
22 | return (
23 |
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/23-26-production/src/components/Charts/Line.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import ReactHighcharts from 'react-highcharts';
3 |
4 | function Line(props) {
5 | if (!props.statistic) return 数据异常
;
6 |
7 | const { categories, series } = props.statistic.chart;
8 | const config = {
9 | credits: {
10 | enabled: false
11 | },
12 | title: {
13 | text: 'Monthly Average Temperature',
14 | x: -20
15 | },
16 | subtitle: {
17 | text: 'Source: WorldClimate.com',
18 | x: -20
19 | },
20 | xAxis: {
21 | categories
22 | },
23 | yAxis: {
24 | title: {
25 | text: 'Temperature (°C)'
26 | }
27 | },
28 | tooltip: {
29 | valueSuffix: '°C'
30 | },
31 | legend: {
32 | layout: 'vertical',
33 | align: 'right',
34 | verticalAlign: 'middle',
35 | borderWidth: 0
36 | },
37 | series
38 | };
39 | return (
40 |
41 | );
42 | }
43 |
44 | Line.propTypes = {
45 | statistic: PropTypes.any
46 | };
47 |
48 | export default Line;
49 |
--------------------------------------------------------------------------------
/23-26-production/src/containers/Statistic/Statistic.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Grid, Row, Col } from 'react-bootstrap';
4 | import Helmet from 'react-helmet';
5 | import { Line, Column, Table } from '../../components';
6 |
7 | @connect(
8 | state => ({ statistic: state.async.statistic })
9 | )
10 | class Statistic extends Component { // eslint-disable-line
11 | render() {
12 | const styles = require('./Statistic.scss');
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 | }
35 |
36 | export default Statistic;
37 |
--------------------------------------------------------------------------------
/06-state-props-context/src/Messagelist1.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | function Button(props) {
4 | return (
5 |
8 | );
9 | }
10 |
11 | Button.propTypes = {
12 | color: PropTypes.string.isRequired,
13 | children: PropTypes.string.isRequired
14 | };
15 |
16 | function Message(props) {
17 | return (
18 |
19 | {props.text}
20 |
21 | );
22 | }
23 |
24 | Message.propTypes = {
25 | text: PropTypes.string.isRequired,
26 | color: PropTypes.string.isRequired
27 | };
28 |
29 | function MessageList() {
30 | const color = 'gray';
31 | const messages = [
32 | { text: 'Hello React' },
33 | { text: 'Hello Redux' }
34 | ];
35 | const children = messages.map((message, key) =>
36 |
37 | );
38 | return (
39 |
40 |
通过props将color逐层传递给里面的Button组件
41 |
44 |
);
45 | }
46 |
47 | export default MessageList;
48 |
--------------------------------------------------------------------------------
/12-undo-devtools/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "12-undo-devtools",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "react": "^15.3.1",
10 | "react-dom": "^15.3.1",
11 | "react-redux": "^4.2.1",
12 | "redux": "^3.2.1",
13 | "redux-thunk": "^2.1.0"
14 | },
15 | "devDependencies": {
16 | "babel-core": "^6.3.15",
17 | "babel-loader": "^6.2.0",
18 | "babel-preset-es2015": "^6.3.13",
19 | "babel-preset-react": "^6.3.13",
20 | "babel-preset-react-hmre": "^1.1.1",
21 | "babel-register": "^6.3.13",
22 | "express": "^4.13.3",
23 | "redux-undo": "^0.6.1",
24 | "webpack": "^1.9.11",
25 | "webpack-dev-middleware": "^1.2.0",
26 | "webpack-hot-middleware": "^2.9.1"
27 | },
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/lewis617/react-redux-book.git"
31 | },
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/lewis617/react-redux-book/issues"
35 | },
36 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
37 | }
38 |
--------------------------------------------------------------------------------
/23-26-production/src/utils/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducer from '../reducers';
4 | import { asyncMiddleware } from 'redux-amrc';
5 |
6 | let createStoreWithMiddleware;
7 |
8 | if (process.env.NODE_ENV === 'production') {
9 | createStoreWithMiddleware = compose(
10 | applyMiddleware(thunk, asyncMiddleware),
11 | )(createStore);
12 | } else {
13 | createStoreWithMiddleware = compose(
14 | applyMiddleware(thunk, asyncMiddleware),
15 | typeof window === 'object' &&
16 | typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : f => f
17 | )(createStore);
18 | }
19 |
20 | export default function configureStore(initialState) {
21 | const store = createStoreWithMiddleware(rootReducer, initialState);
22 |
23 | if (module.hot) {
24 | // Enable Webpack hot module replacement for reducers
25 | module.hot.accept('../reducers', () => {
26 | const nextRootReducer = require('../reducers/index').default;
27 | store.replaceReducer(nextRootReducer);
28 | });
29 | }
30 |
31 | return store;
32 | }
33 |
--------------------------------------------------------------------------------
/23-26-production/src/components/Charts/Column.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import ReactHighcharts from 'react-highcharts';
3 |
4 | function Column(props) {
5 | if (!props.statistic) return 数据异常
;
6 |
7 | const { categories, series } = props.statistic.chart;
8 | const config = {
9 | credits: {
10 | enabled: false
11 | },
12 | chart: {
13 | type: 'column'
14 | },
15 | title: {
16 | text: 'Monthly Average Temperature',
17 | x: -20
18 | },
19 | subtitle: {
20 | text: 'Source: WorldClimate.com',
21 | x: -20
22 | },
23 | xAxis: {
24 | categories
25 | },
26 | yAxis: {
27 | title: {
28 | text: 'Temperature (°C)'
29 | }
30 | },
31 | tooltip: {
32 | valueSuffix: '°C'
33 | },
34 | legend: {
35 | layout: 'vertical',
36 | align: 'right',
37 | verticalAlign: 'middle',
38 | borderWidth: 0
39 | },
40 | series
41 | };
42 | return (
43 |
44 | );
45 | }
46 |
47 | Column.propTypes = {
48 | statistic: PropTypes.any
49 | };
50 |
51 | export default Column;
52 |
--------------------------------------------------------------------------------
/23-26-production/webpack/webpack-isomorphic-tools.js:
--------------------------------------------------------------------------------
1 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
2 |
3 | var config = {
4 | assets: {
5 | images: {extensions: ['png']},
6 | style_modules: {
7 | extensions: ['css', 'scss'],
8 | filter: function (module, regex, options, log) {
9 | if (options.development) {
10 | return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
11 | } else {
12 | return regex.test(module.name);
13 | }
14 | },
15 | path: function (module, options, log) {
16 | if (options.development) {
17 | return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
18 | } else {
19 | return module.name;
20 | }
21 | },
22 | parser: function (module, options, log) {
23 | if (options.development) {
24 | return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
25 | } else {
26 | return module.source;
27 | }
28 | }
29 | }
30 | }
31 | };
32 |
33 | module.exports = config;
34 |
--------------------------------------------------------------------------------
/08-start-redux/src/app.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 |
3 | function counter(state = 0, action) {
4 | switch (action.type) {
5 | case 'INCREMENT':
6 | return state + 1;
7 | case 'DECREMENT':
8 | return state - 1;
9 | default:
10 | return state;
11 | }
12 | }
13 | /**
14 | * 使用这个reducer纯函数代替上面的函数,会发现:
15 | * 当state为对象时,如果在reducer中修改state,
16 | * 将会导致新旧state指向一个地址
17 | */
18 | // function counter(state = { val: 0 }, action) {
19 | // switch (action.type) {
20 | // case 'INCREMENT':
21 | // state.val++;
22 | // return state;
23 | // case 'DECREMENT':
24 | // state.val--;
25 | // return state;
26 | // default:
27 | // return state;
28 | // }
29 | // }
30 |
31 |
32 | const store = createStore(counter);
33 |
34 | let currentValue = store.getState();
35 |
36 | const listener = () => {
37 | const previousValue = currentValue;
38 | currentValue = store.getState();
39 | console.log('pre state:', previousValue, 'next state:', currentValue);
40 | };
41 |
42 | store.subscribe(listener);
43 |
44 |
45 | store.dispatch({ type: 'INCREMENT' });
46 |
47 | store.dispatch({ type: 'INCREMENT' });
48 |
49 | store.dispatch({ type: 'DECREMENT' });
50 |
--------------------------------------------------------------------------------
/11-counter-connect/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "11-counter-connect",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "react": "^15.3.1",
10 | "react-dom": "^15.3.1",
11 | "react-redux": "^4.2.1",
12 | "redux": "^3.2.1",
13 | "redux-thunk": "^2.1.0"
14 | },
15 | "devDependencies": {
16 | "babel-core": "^6.3.15",
17 | "babel-loader": "^6.2.0",
18 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
19 | "babel-preset-es2015": "^6.3.13",
20 | "babel-preset-react": "^6.3.13",
21 | "babel-preset-react-hmre": "^1.1.1",
22 | "babel-preset-stage-0": "^6.5.0",
23 | "babel-register": "^6.3.13",
24 | "express": "^4.13.3",
25 | "webpack": "^1.9.11",
26 | "webpack-dev-middleware": "^1.2.0",
27 | "webpack-hot-middleware": "^2.9.1"
28 | },
29 | "repository": {
30 | "type": "git",
31 | "url": "git+https://github.com/lewis617/react-redux-book.git"
32 | },
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/lewis617/react-redux-book/issues"
36 | },
37 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
38 | }
39 |
--------------------------------------------------------------------------------
/21-async-router/common/actions/index.js:
--------------------------------------------------------------------------------
1 | import 'isomorphic-fetch';
2 | import { ASYNC } from 'redux-amrc';
3 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
4 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
5 |
6 | export function increment() {
7 | return {
8 | type: INCREMENT_COUNTER,
9 | };
10 | }
11 |
12 | export function decrement() {
13 | return {
14 | type: DECREMENT_COUNTER,
15 | };
16 | }
17 |
18 | export function incrementIfOdd() {
19 | return (dispatch, getState) => {
20 | const { async } = getState();
21 |
22 | if (async.counter.value % 2 === 0) {
23 | return;
24 | }
25 |
26 | dispatch(increment());
27 | };
28 | }
29 |
30 | export function incrementAsync(delay = 1000) {
31 | return dispatch => {
32 | setTimeout(() => {
33 | dispatch(increment());
34 | }, delay);
35 | };
36 | }
37 |
38 | export function load() {
39 | return {
40 | [ASYNC]: {
41 | key: 'counter',
42 | promise: () => fetch('http://localhost:3000/api/counter')
43 | .then(res => {
44 | if (!res.ok) {
45 | throw new Error(res.statusText);
46 | }
47 | return res.json();
48 | })
49 | }
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/19-3-isomophic-counter/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
5 |
6 | module.exports = {
7 | context: path.resolve(__dirname),
8 | devtool: 'cheap-eval-source-map',
9 | entry: [
10 | 'webpack-hot-middleware/client?path=http://localhost:3001/__webpack_hmr',
11 | './client/index.js'
12 | ],
13 | output: {
14 | path: path.join(__dirname, 'dist'),
15 | filename: 'bundle.js',
16 | publicPath: 'http://localhost:3001/static/'
17 | },
18 | plugins: [
19 | new webpack.optimize.OccurrenceOrderPlugin(),
20 | new webpack.HotModuleReplacementPlugin(),
21 | new WebpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools')).development()
22 | ],
23 | module: {
24 | loaders: [
25 | {
26 | test: /\.js$/,
27 | loader: 'babel',
28 | exclude: /node_modules/,
29 | include: __dirname,
30 | query: {
31 | presets: ['react-hmre']
32 | }
33 | },
34 | { test: /\.png$/, loader: 'url-loader?limit=10240' },
35 | { test: /\.css$/, loader: 'style-loader!css-loader?modules' }
36 | ]
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/16-async/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "16-async",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "babel-polyfill": "^6.3.14",
10 | "isomorphic-fetch": "^2.1.1",
11 | "react": "^15.3.1",
12 | "react-dom": "^15.3.1",
13 | "react-redux": "^4.2.1",
14 | "redux": "^3.2.1",
15 | "redux-logger": "^2.4.0",
16 | "redux-thunk": "^2.1.0"
17 | },
18 | "devDependencies": {
19 | "babel-core": "^6.3.15",
20 | "babel-loader": "^6.2.0",
21 | "babel-preset-es2015": "^6.3.13",
22 | "babel-preset-react": "^6.3.13",
23 | "babel-preset-react-hmre": "^1.1.1",
24 | "expect": "^1.6.0",
25 | "express": "^4.13.3",
26 | "node-libs-browser": "^1.0.0",
27 | "webpack": "^1.9.11",
28 | "webpack-dev-middleware": "^1.2.0",
29 | "webpack-hot-middleware": "^2.9.1"
30 | },
31 | "repository": {
32 | "type": "git",
33 | "url": "git+https://github.com/lewis617/react-redux-book.git"
34 | },
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/lewis617/react-redux-book/issues"
38 | },
39 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
40 | }
41 |
--------------------------------------------------------------------------------
/18-universal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "18-universal",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server/index.js"
7 | },
8 | "dependencies": {
9 | "babel-polyfill": "^6.3.14",
10 | "babel-register": "^6.4.3",
11 | "express": "^4.13.3",
12 | "qs": "^6.2.1",
13 | "react": "^15.3.1",
14 | "react-dom": "^15.3.1",
15 | "react-redux": "^4.2.1",
16 | "redux": "^3.2.1",
17 | "redux-thunk": "^2.1.0",
18 | "serve-static": "^1.10.0"
19 | },
20 | "devDependencies": {
21 | "babel-core": "^6.3.15",
22 | "babel-loader": "^6.2.0",
23 | "babel-preset-es2015": "^6.3.13",
24 | "babel-preset-react": "^6.3.13",
25 | "babel-preset-react-hmre": "^1.1.1",
26 | "babel-runtime": "^6.3.13",
27 | "webpack": "^1.11.0",
28 | "webpack-dev-middleware": "^1.4.0",
29 | "webpack-hot-middleware": "^2.9.1"
30 | },
31 | "repository": {
32 | "type": "git",
33 | "url": "git+https://github.com/lewis617/react-redux-book.git"
34 | },
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/lewis617/react-redux-book/issues"
38 | },
39 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
40 | }
41 |
--------------------------------------------------------------------------------
/20-universal-router/common/containers/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import { connect } from 'react-redux';
4 | import * as CounterActions from '../actions';
5 |
6 | function Counter({ increment, incrementIfOdd, incrementAsync, decrement, counter }) {
7 | return (
8 |
9 | Clicked: {counter} times
10 | {' '}
11 |
12 | {' '}
13 |
14 | {' '}
15 |
16 | {' '}
17 |
18 |
19 | );
20 | }
21 |
22 | Counter.propTypes = {
23 | increment: PropTypes.func.isRequired,
24 | incrementIfOdd: PropTypes.func.isRequired,
25 | incrementAsync: PropTypes.func.isRequired,
26 | decrement: PropTypes.func.isRequired,
27 | counter: PropTypes.number.isRequired,
28 | };
29 |
30 | function mapStateToProps(state) {
31 | return {
32 | counter: state.counter
33 | };
34 | }
35 |
36 | function mapDispatchToProps(dispatch) {
37 | return bindActionCreators(CounterActions, dispatch);
38 | }
39 |
40 | export default connect(mapStateToProps, mapDispatchToProps)(Counter);
41 |
--------------------------------------------------------------------------------
/11-counter-connect/containers/Connect5.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import * as ActionCreators from '../actions';
4 |
5 | // 装饰器应该写在类声明上面
6 | @connect(
7 | state => ({ counter: state.counter }),
8 | ActionCreators
9 | )
10 | class Counter extends Component { // eslint-disable-line
11 |
12 | static propTypes = {
13 | counter: PropTypes.number.isRequired,
14 | increment: PropTypes.func.isRequired,
15 | incrementIfOdd: PropTypes.func.isRequired,
16 | incrementAsync: PropTypes.func.isRequired,
17 | decrement: PropTypes.func.isRequired
18 | };
19 |
20 | render() {
21 | const { counter, increment, decrement, incrementIfOdd, incrementAsync } = this.props;
22 | return (
23 |
24 | Clicked: {counter} times
25 | {' '}
26 |
27 | {' '}
28 |
29 | {' '}
30 |
31 | {' '}
32 | {/* 这里必须写成箭头函数,否则incrementAsync中的delay参数将会是SyntheticEvent的实例*/}
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | export default Counter;
40 |
--------------------------------------------------------------------------------
/23-26-production/src/actions/auth.js:
--------------------------------------------------------------------------------
1 | import { ASYNC } from 'redux-amrc';
2 | import { customFetch } from '../utils/utils';
3 |
4 | export function shouldLoadAuth(state) {
5 | if (!state.async.loadState.user) return true;
6 | const loaded = state.async.loadState.user.loaded;
7 | return !loaded;
8 | }
9 |
10 | export function loadAuth() {
11 | return {
12 | [ASYNC]: {
13 | key: 'user',
14 | promise: () => customFetch('/loadAuth')
15 | }
16 | };
17 | }
18 |
19 | export function loadAuthIfNeeded() {
20 | return (dispatch, getState) => {
21 | if (shouldLoadAuth(getState())) {
22 | return dispatch(loadAuth());
23 | }
24 | return Promise.resolve();
25 | };
26 | }
27 |
28 | export function login(name) {
29 | const url = '/login';
30 | const option = {
31 | method: 'post',
32 | headers: {
33 | Accept: 'application/json',
34 | 'Content-Type': 'application/json'
35 | },
36 | body: JSON.stringify({
37 | name
38 | })
39 | };
40 | return {
41 | [ASYNC]: {
42 | key: 'user',
43 | promise: () => customFetch(url, option)
44 | }
45 | };
46 | }
47 |
48 | export function logout() {
49 | return {
50 | [ASYNC]: {
51 | key: 'user',
52 | promise: () => customFetch('/logout')
53 | }
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/07-element-instance/src/App.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-useless-constructor */
2 | import React from 'react';
3 |
4 | const suffix = '被调用,this指向:';
5 |
6 | export default class App extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | // this.handler = this.handler.bind(this)
10 | }
11 |
12 | componentDidMount() {
13 | console.log(`componentDidMount${suffix}`, this);
14 | }
15 |
16 | componentWillReceiveProps() {
17 | console.log(`componentWillReceiveProps${suffix}`, this);
18 | }
19 |
20 | shouldComponentUpdate() {
21 | console.log(`shouldComponentUpdate${suffix}`, this);
22 | return true;
23 | }
24 |
25 | componentDidUpdate() {
26 | console.log(`componentDidUpdate${suffix}`, this);
27 | }
28 |
29 | componentWillUnmount() {
30 | console.log(`componentWillUnmount${suffix}`, this);
31 | }
32 |
33 | handler() {
34 | console.log(`handler${suffix}`, this);
35 | }
36 |
37 | render() {
38 | console.log(`render${suffix}`, this);
39 |
40 | this.handler();
41 | window.handler = this.handler;
42 | window.handler();
43 |
44 | return (
45 |
46 |
Hello world
47 |
不清楚组件、ReactElement、组件实例以及组件中的this是什么?打开控制台看看就明白了!
48 |
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/23-26-production/src/components/Table/Table.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import Griddle from 'griddle-react';
3 | import CustomPagerComponent from './CustomPagerComponent';
4 |
5 | const customComponent = props => ({props.data + '°C'}
);
6 |
7 | customComponent.propTypes = {
8 | data: PropTypes.number.isRequired
9 | };
10 |
11 | function Table(props) {
12 | require('./Table.scss');
13 | if (!props.statistic) return 数据异常
;
14 | const columnMetadata = [
15 | {
16 | columnName: 'Month'
17 | },
18 | {
19 | columnName: 'Tokyo',
20 | customComponent
21 | },
22 | {
23 | columnName: 'New York',
24 | customComponent
25 | },
26 | {
27 | columnName: 'Berlin',
28 | customComponent
29 | },
30 | {
31 | columnName: 'London',
32 | customComponent
33 | }
34 | ];
35 | return (
36 |
45 | );
46 | }
47 |
48 | Table.propTypes = {
49 | statistic: PropTypes.any
50 | };
51 |
52 | export default Table;
53 |
--------------------------------------------------------------------------------
/11-counter-connect/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { createStore, applyMiddleware } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import thunk from 'redux-thunk';
6 | import counter from './reducers';
7 | import Connect1 from './containers/Connect1';
8 | import Connect2 from './containers/Connect2';
9 | import Connect3 from './containers/Connect3';
10 | import Connect4 from './containers/Connect4';
11 | import Connect5 from './containers/Connect5';
12 |
13 | const store = createStore(counter, applyMiddleware(thunk));
14 | const rootEl = document.getElementById('root');
15 |
16 | ReactDOM.render(
17 |
18 |
19 |
使用react-redux连接
20 |
21 | -
22 | connect()的前两个参数分别为函数和对象:
23 |
24 |
25 | -
26 | connect()的前两个参数均为函数:
27 |
28 |
29 | -
30 | connect()的前两个参数均为函数,但使用了bindActionCreators:
31 |
32 |
33 | -
34 | connect()的第二个参数为空:
35 |
36 |
37 | -
38 | connect()的装饰器写法:
39 |
40 |
41 |
42 |
43 | , rootEl);
44 |
--------------------------------------------------------------------------------
/06-state-props-context/src/Messagelist2.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | function Button(props, context) {
4 | return (
5 |
8 | );
9 | }
10 |
11 | Button.propTypes = {
12 | children: PropTypes.string.isRequired
13 | };
14 |
15 | Button.contextTypes = {
16 | color: PropTypes.string.isRequired
17 | };
18 |
19 | function Message(props) {
20 | return (
21 |
22 | {props.text}
23 |
24 | );
25 | }
26 |
27 | Message.propTypes = {
28 | text: PropTypes.string.isRequired
29 | };
30 |
31 | class MessageList extends Component {
32 | getChildContext() {
33 | return { color: 'gray' };
34 | }
35 |
36 | render() {
37 | const messages = [
38 | { text: 'Hello React' },
39 | { text: 'Hello Redux' }
40 | ];
41 | const children = messages.map((message, key) =>
42 |
43 | );
44 | return (
45 |
46 |
通过context将color跨级传递给里面的Button组件
47 |
50 |
51 | );
52 | }
53 | }
54 |
55 | MessageList.childContextTypes = {
56 | color: PropTypes.string.isRequired
57 | };
58 |
59 | export default MessageList;
60 |
--------------------------------------------------------------------------------
/10-counter/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | class Counter extends Component {
4 | constructor(props) {
5 | // 子类在获取this前必须调用super
6 | super(props);
7 | this.incrementAsync = this.incrementAsync.bind(this);
8 | this.incrementIfOdd = this.incrementIfOdd.bind(this);
9 | }
10 |
11 | incrementIfOdd() {
12 | if (this.props.value % 2 !== 0) {
13 | this.props.onIncrement();
14 | }
15 | }
16 |
17 | incrementAsync() {
18 | setTimeout(this.props.onIncrement, 1000);
19 | }
20 |
21 | render() {
22 | const { value, onIncrement, onDecrement } = this.props;
23 | return (
24 |
25 | Clicked: {value} times
26 | {' '}
27 |
28 | {' '}
29 |
30 | {' '}
31 |
32 | {' '}
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | Counter.propTypes = {
40 | // value必须为数字,且必须存在
41 | value: PropTypes.number.isRequired,
42 | // onIncrement必须为fucntion,且必须存在
43 | onIncrement: PropTypes.func.isRequired,
44 | onDecrement: PropTypes.func.isRequired
45 | };
46 |
47 | export default Counter;
48 |
--------------------------------------------------------------------------------
/20-universal-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "20-universal-router",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server/index.js"
7 | },
8 | "dependencies": {
9 | "babel-polyfill": "^6.3.14",
10 | "babel-register": "^6.4.3",
11 | "express": "^4.13.3",
12 | "react": "^15.3.1",
13 | "react-dom": "^15.3.1",
14 | "react-redux": "^4.2.1",
15 | "react-router": "^2.4.1",
16 | "redux": "^3.2.1",
17 | "redux-thunk": "^2.1.0",
18 | "serve-favicon": "^2.3.0",
19 | "serve-static": "^1.10.0"
20 | },
21 | "devDependencies": {
22 | "babel-core": "^6.3.15",
23 | "babel-loader": "^6.2.0",
24 | "babel-preset-es2015": "^6.3.13",
25 | "babel-preset-react": "^6.3.13",
26 | "babel-preset-react-hmre": "^1.1.1",
27 | "babel-preset-stage-0": "^6.5.0",
28 | "babel-runtime": "^6.3.13",
29 | "webpack": "^1.11.0",
30 | "webpack-dev-middleware": "^1.4.0",
31 | "webpack-hot-middleware": "^2.9.1"
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/lewis617/react-redux-book.git"
36 | },
37 | "license": "MIT",
38 | "bugs": {
39 | "url": "https://github.com/lewis617/react-redux-book/issues"
40 | },
41 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 《React 与 Redux 开发实例精解》
2 |
3 | [](https://travis-ci.org/lewis617/react-redux-book)
4 |
5 |
6 | 
7 |
8 | ## 关于 React 与 Redux
9 |
10 | React 与 Redux, 一个快如鬼魅,一个清晰明了,各个巨头在生产环境对其进行了无数次的测试,强大的社区又为其提供了无数个场景的解决方案,是目前国际上最主流,最先进的前端技术选型。
11 |
12 | ## 关于这本书
13 |
14 | 《React 与 Redux 开发实例精解》这本书不仅讲解了 React 与 Redux 的基础和实战,更注重 Universal 渲染、函数式编程和项目架构的介绍。笔者一直在360的生产环境中使用这本书上的技术,感觉非常靠谱。希望读者可以喜欢这本书,也希望这本书能帮到更多的人,更希望国内有更多的基于 React 与 Redux 搭建的优秀项目出现!
15 |
16 | ## 本书的目标读者
17 |
18 | 本书适合有一定的 ES6/7、Node 开发经验,想要使用 React 与 Redux 开发应用的前/后端程序员阅读参考。零基础的同学请先补习 ES6/7 和 Node 的基础知识。
19 |
20 | ## 本书的推荐读法
21 |
22 | - **跑例子:** 本书名为《React 与 Redux 开发实例精解》,因此请务必将[示例代码](https://github.com/lewis617/react-redux-book)克隆到本地,一边运行例子,一边阅读本书。
23 | - **看文档:** 本书因篇幅有限等原因,无法对每一项技术的讲解都做到完整而详尽。因此,你还需要根据书中的提示和推荐,去阅读参考相应技术的官方文档。
24 | - **提问题:** 如果在阅读过程中遇到解决不了的问题,可以到 [GitHub](https://github.com/lewis617/react-redux-book) 提交 Issue,或给我发邮件(lewis617@163.com),我会第一时间解答你的问题。
25 |
26 | ## 售书链接
27 |
28 | [京东](https://item.jd.com/12010463.html)
29 |
30 | [当当](http://product.dangdang.com/24145390.html)
31 |
32 | ## 常见问题及解决办法
33 |
34 | [23章例子跑不起来](https://github.com/lewis617/react-redux-book/issues/2)
35 |
36 | ## License
37 |
38 | MIT
39 |
--------------------------------------------------------------------------------
/23-26-production/src/api/controllers/statistic.js:
--------------------------------------------------------------------------------
1 | export default app => {
2 | app.get('/statistic', (req, res) => {
3 | const categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
4 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
5 | const series = [{
6 | name: 'Tokyo',
7 | data: [7.0, 6.9, 9.5, 14.5, 18.2, 21.5, 25.2, 26.5, 23.3, 18.3, 13.9, 9.6]
8 | }, {
9 | name: 'New York',
10 | data: [-0.2, 0.8, 5.7, 11.3, 17.0, 22.0, 24.8, 24.1, 20.1, 14.1, 8.6, 2.5]
11 | }, {
12 | name: 'Berlin',
13 | data: [-0.9, 0.6, 3.5, 8.4, 13.5, 17.0, 18.6, 17.9, 14.3, 9.0, 3.9, 1.0]
14 | }, {
15 | name: 'London',
16 | data: [3.9, 4.2, 5.7, 8.5, 11.9, 15.2, 17.0, 16.6, 14.2, 10.3, 6.6, 4.8]
17 | }];
18 |
19 | const table = categories.map(
20 | (Month, key) => ({
21 | Month,
22 | Tokyo: series[0].data[key],
23 | 'New York': series[1].data[key],
24 | Berlin: series[2].data[key],
25 | London: series[3].data[key]
26 | })
27 | );
28 |
29 | setTimeout(() => {
30 | if (Math.random() < 0.33) {
31 | res.status(500).end();
32 | } else {
33 | res.json({
34 | chart: {
35 | categories,
36 | series
37 | },
38 | table
39 | });
40 | }
41 | }, 1000);
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/21-async-router/common/containers/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import * as actionCreators from '../actions';
4 |
5 | @connect(
6 | state => ({ state }),
7 | actionCreators
8 | )
9 | class Counter extends Component { // eslint-disable-line
10 |
11 | static propTypes = {
12 | increment: PropTypes.func.isRequired,
13 | incrementIfOdd: PropTypes.func.isRequired,
14 | incrementAsync: PropTypes.func.isRequired,
15 | decrement: PropTypes.func.isRequired,
16 | load: PropTypes.func.isRequired,
17 | state: PropTypes.object.isRequired
18 | };
19 |
20 | render() {
21 | const {
22 | increment, decrement, incrementIfOdd, incrementAsync, state, load
23 | } = this.props;
24 | return (
25 |
26 |
27 | {' '}
28 |
29 | {' '}
30 |
31 | {' '}
32 |
33 | {' '}
34 |
35 |
36 | 程序当前的state:
37 |
{JSON.stringify(state, null, 2)}
38 |
39 | );
40 | }
41 | }
42 |
43 | export default Counter;
44 |
--------------------------------------------------------------------------------
/09-redux-thunk/src/app.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 |
4 | function increment() {
5 | return { type: 'INCREMENT' };
6 | }
7 | function decrement() {
8 | return { type: 'DECREMENT' };
9 | }
10 | function incrementIfOdd() {
11 | return (dispatch, getState) => {
12 | const value = getState();
13 | if (value % 2 === 0) {
14 | return;
15 | }
16 |
17 | dispatch(increment());
18 | };
19 | }
20 | function incrementAsync(delay = 1000) {
21 | return dispatch => {
22 | setTimeout(() => {
23 | dispatch(increment());
24 | }, delay);
25 | };
26 | }
27 |
28 | function counter(state = 0, action) {
29 | switch (action.type) {
30 | case 'INCREMENT':
31 | return state + 1;
32 | case 'DECREMENT':
33 | return state - 1;
34 | default:
35 | return state;
36 | }
37 | }
38 |
39 |
40 | const store = createStore(counter, applyMiddleware(thunk));
41 |
42 | let currentValue = store.getState();
43 | store.subscribe(() => {
44 | const previousValue = currentValue;
45 | currentValue = store.getState();
46 | console.log('pre state:', previousValue, 'next state:', currentValue);
47 | }
48 | );
49 |
50 | store.dispatch(increment());
51 |
52 | store.dispatch(incrementIfOdd());
53 |
54 | store.dispatch(incrementAsync());
55 |
56 | store.dispatch(decrement());
57 |
--------------------------------------------------------------------------------
/14-15-todomvc/test/actions/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import * as types from '../../constants/ActionTypes';
3 | import * as actions from '../../actions';
4 |
5 | describe('todo actions', () => {
6 | it('addTodo should create ADD_TODO action', () => {
7 | expect(actions.addTodo('Use Redux')).toEqual({
8 | type: types.ADD_TODO,
9 | text: 'Use Redux',
10 | });
11 | });
12 |
13 | it('deleteTodo should create DELETE_TODO action', () => {
14 | expect(actions.deleteTodo(1)).toEqual({
15 | type: types.DELETE_TODO,
16 | id: 1,
17 | });
18 | });
19 |
20 | it('editTodo should create EDIT_TODO action', () => {
21 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
22 | type: types.EDIT_TODO,
23 | id: 1,
24 | text: 'Use Redux everywhere',
25 | });
26 | });
27 |
28 | it('completeTodo should create COMPLETE_TODO action', () => {
29 | expect(actions.completeTodo(1)).toEqual({
30 | type: types.COMPLETE_TODO,
31 | id: 1,
32 | });
33 | });
34 |
35 | it('completeAll should create COMPLETE_ALL action', () => {
36 | expect(actions.completeAll()).toEqual({
37 | type: types.COMPLETE_ALL,
38 | });
39 | });
40 |
41 | it('clearCompleted should create CLEAR_COMPLETED action', () => {
42 | expect(actions.clearCompleted()).toEqual({
43 | type: types.CLEAR_COMPLETED,
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/22-bootstrap/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "22-bootstrap",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "react": "^15.3.1",
10 | "react-bootstrap": "^0.30.3",
11 | "react-dom": "^15.3.1"
12 | },
13 | "devDependencies": {
14 | "autoprefixer": "^6.3.6",
15 | "babel-core": "^6.3.15",
16 | "babel-loader": "^6.2.0",
17 | "babel-preset-es2015": "^6.3.13",
18 | "babel-preset-react": "^6.3.13",
19 | "babel-preset-react-hmre": "^1.1.1",
20 | "bootstrap-loader": "^1.0.10",
21 | "bootstrap-sass": "^3.3.6",
22 | "css-loader": "^0.25.0",
23 | "express": "^4.13.3",
24 | "extract-text-webpack-plugin": "^1.0.1",
25 | "file-loader": "^0.9.0",
26 | "node-sass": "^3.7.0",
27 | "postcss-loader": "^0.13.0",
28 | "resolve-url-loader": "^1.4.3",
29 | "sass-loader": "^4.0.2",
30 | "style-loader": "^0.13.1",
31 | "url-loader": "^0.5.7",
32 | "webpack": "^1.11.0",
33 | "webpack-dev-middleware": "^1.2.0",
34 | "webpack-hot-middleware": "^2.9.1"
35 | },
36 | "repository": {
37 | "type": "git",
38 | "url": "git+https://github.com/lewis617/react-redux-book.git"
39 | },
40 | "license": "MIT",
41 | "bugs": {
42 | "url": "https://github.com/lewis617/react-redux-book/issues"
43 | },
44 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
45 | }
46 |
--------------------------------------------------------------------------------
/21-async-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "21-async-middleware",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server/index.js"
7 | },
8 | "dependencies": {
9 | "babel-polyfill": "^6.3.14",
10 | "babel-register": "^6.4.3",
11 | "express": "^4.13.3",
12 | "isomorphic-fetch": "^2.2.1",
13 | "react": "^15.3.1",
14 | "react-dom": "^15.3.1",
15 | "react-redux": "^4.2.1",
16 | "react-router": "^2.4.1",
17 | "redux": "^3.2.1",
18 | "redux-amrc": "^1.0.4",
19 | "redux-thunk": "^2.1.0",
20 | "serve-favicon": "^2.3.0",
21 | "serve-static": "^1.10.0"
22 | },
23 | "devDependencies": {
24 | "babel-core": "^6.3.15",
25 | "babel-loader": "^6.2.0",
26 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
27 | "babel-preset-es2015": "^6.3.13",
28 | "babel-preset-react": "^6.3.13",
29 | "babel-preset-react-hmre": "^1.1.1",
30 | "babel-preset-stage-0": "^6.5.0",
31 | "babel-runtime": "^6.3.13",
32 | "webpack": "^1.11.0",
33 | "webpack-dev-middleware": "^1.4.0",
34 | "webpack-hot-middleware": "^2.9.1"
35 | },
36 | "repository": {
37 | "type": "git",
38 | "url": "git+https://github.com/lewis617/react-redux-book.git"
39 | },
40 | "license": "MIT",
41 | "bugs": {
42 | "url": "https://github.com/lewis617/react-redux-book/issues"
43 | },
44 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
45 | }
46 |
--------------------------------------------------------------------------------
/17-real-world/components/List.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | export default class List extends Component {
4 | renderLoadMore() {
5 | const { isFetching, onLoadMoreClick } = this.props;
6 | return (
7 |
10 | );
11 | }
12 |
13 | render() {
14 | const {
15 | isFetching, nextPageUrl, pageCount,
16 | items, renderItem, loadingLabel,
17 | } = this.props;
18 |
19 | const isEmpty = items.length === 0;
20 | if (isEmpty && isFetching) {
21 | return {loadingLabel}
;
22 | }
23 |
24 | const isLastPage = !nextPageUrl;
25 | if (isEmpty && isLastPage) {
26 | return Nothing here!
;
27 | }
28 |
29 | return (
30 |
31 | {items.map(renderItem)}
32 | {pageCount > 0 && !isLastPage && this.renderLoadMore()}
33 |
34 | );
35 | }
36 | }
37 |
38 | List.propTypes = {
39 | loadingLabel: PropTypes.string.isRequired,
40 | pageCount: PropTypes.number,
41 | renderItem: PropTypes.func.isRequired,
42 | items: PropTypes.array.isRequired,
43 | isFetching: PropTypes.bool.isRequired,
44 | onLoadMoreClick: PropTypes.func.isRequired,
45 | nextPageUrl: PropTypes.string,
46 | };
47 |
48 | List.defaultProps = {
49 | isFetching: true,
50 | loadingLabel: 'Loading...',
51 | };
52 |
--------------------------------------------------------------------------------
/17-real-world/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "17-real-world",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "dependencies": {
9 | "babel-polyfill": "^6.3.14",
10 | "humps": "^1.1.0",
11 | "isomorphic-fetch": "^2.1.1",
12 | "lodash": "^4.0.0",
13 | "normalizr": "^2.0.0",
14 | "react": "^15.3.1",
15 | "react-dom": "^15.3.1",
16 | "react-redux": "^4.2.1",
17 | "react-router": "2.8.1",
18 | "react-router-redux": "^4.0.0-rc.1",
19 | "redux": "^3.2.1",
20 | "redux-logger": "^2.4.0",
21 | "redux-thunk": "^2.1.0"
22 | },
23 | "devDependencies": {
24 | "babel-core": "^6.3.15",
25 | "babel-loader": "^6.2.0",
26 | "babel-preset-es2015": "^6.3.13",
27 | "babel-preset-react": "^6.3.13",
28 | "babel-preset-react-hmre": "^1.1.1",
29 | "concurrently": "^2.2.0",
30 | "express": "^4.13.3",
31 | "redux-devtools": "^3.1.0",
32 | "redux-devtools-dock-monitor": "^1.0.1",
33 | "redux-devtools-log-monitor": "^1.0.3",
34 | "webpack": "^1.9.11",
35 | "webpack-dev-middleware": "^1.2.0",
36 | "webpack-hot-middleware": "^2.9.1"
37 | },
38 | "repository": {
39 | "type": "git",
40 | "url": "git+https://github.com/lewis617/react-redux-book.git"
41 | },
42 | "license": "MIT",
43 | "bugs": {
44 | "url": "https://github.com/lewis617/react-redux-book/issues"
45 | },
46 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
47 | }
48 |
--------------------------------------------------------------------------------
/19-3-isomophic-counter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "19-3-isomophic-counter",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "concurrently -k \"node server/dev-server.js\" \"node server/index.js\""
7 | },
8 | "dependencies": {
9 | "babel-polyfill": "^6.3.14",
10 | "babel-register": "^6.4.3",
11 | "express": "^4.13.3",
12 | "qs": "^6.2.1",
13 | "react": "^15.3.1",
14 | "react-dom": "^15.3.1",
15 | "react-redux": "^4.2.1",
16 | "redux": "^3.2.1",
17 | "redux-thunk": "^2.1.0",
18 | "serve-static": "^1.10.0"
19 | },
20 | "devDependencies": {
21 | "babel-core": "^6.3.15",
22 | "babel-loader": "^6.2.0",
23 | "babel-preset-es2015": "^6.3.13",
24 | "babel-preset-react": "^6.3.13",
25 | "babel-preset-react-hmre": "^1.1.1",
26 | "babel-runtime": "^6.3.13",
27 | "css-loader": "^0.25.0",
28 | "file-loader": "^0.9.0",
29 | "concurrently": "^2.1.0",
30 | "style-loader": "^0.13.1",
31 | "url-loader": "^0.5.7",
32 | "webpack": "^1.11.0",
33 | "webpack-dev-middleware": "^1.4.0",
34 | "webpack-hot-middleware": "^2.9.1",
35 | "webpack-isomorphic-tools": "2.5.8"
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "git+https://github.com/lewis617/react-redux-book.git"
40 | },
41 | "license": "MIT",
42 | "bugs": {
43 | "url": "https://github.com/lewis617/react-redux-book/issues"
44 | },
45 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
46 | }
47 |
--------------------------------------------------------------------------------
/13-counter-test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "13-counter-test",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js",
8 | "test:watch": "npm test -- --watch"
9 | },
10 | "dependencies": {
11 | "react": "^15.3.1",
12 | "react-dom": "^15.3.1",
13 | "react-redux": "^4.2.1",
14 | "redux": "^3.2.1",
15 | "redux-thunk": "^2.1.0"
16 | },
17 | "devDependencies": {
18 | "babel-core": "^6.3.15",
19 | "babel-loader": "^6.2.0",
20 | "babel-preset-es2015": "^6.3.13",
21 | "babel-preset-react": "^6.3.13",
22 | "babel-preset-react-hmre": "^1.1.1",
23 | "babel-register": "^6.3.13",
24 | "cross-env": "^2.0.1",
25 | "enzyme": "^2.0.0",
26 | "expect": "^1.6.0",
27 | "express": "^4.13.3",
28 | "jsdom": "^9.5.0",
29 | "mocha": "^3.0.2",
30 | "node-libs-browser": "^1.0.0",
31 | "react-addons-test-utils": "^15.3.1",
32 | "redux-mock-store": "^1.1.4",
33 | "webpack": "^1.9.11",
34 | "webpack-dev-middleware": "^1.2.0",
35 | "webpack-hot-middleware": "^2.9.1"
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "git+https://github.com/lewis617/react-redux-book.git"
40 | },
41 | "license": "MIT",
42 | "bugs": {
43 | "url": "https://github.com/lewis617/react-redux-book/issues"
44 | },
45 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
46 | }
47 |
--------------------------------------------------------------------------------
/23-26-production/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 | import { Main, Home, Counter, NotFound, Forms, Statistic, Login } from './containers';
4 | import { loadCounter } from './actions/counter';
5 | import { loadStatistic } from './actions/statistic';
6 | import { loadAuthIfNeeded } from './actions/auth';
7 |
8 | const preload = promise => (nextState, replace, cb) => {
9 | if (__SERVER__ || nextState.location.action === 'PUSH') {
10 | promise().then(() => cb());
11 | } else {
12 | cb();
13 | }
14 | };
15 |
16 | export default store => {
17 | const counterPromise = () => store.dispatch(loadCounter());
18 | const statisticPromise = () => store.dispatch(loadStatistic());
19 | const authPromise = () => store.dispatch(loadAuthIfNeeded());
20 | const requireLogin = (nextState, replace, cb) => {
21 | const user = store.getState().async.user;
22 | if (!user) {
23 | replace('/');
24 | }
25 | cb();
26 | };
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | >
35 |
36 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/14-15-todomvc/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_TODO, DELETE_TODO, EDIT_TODO,
3 | COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED,
4 | } from '../constants/ActionTypes';
5 |
6 | const initialState = [
7 | {
8 | text: 'Use Redux',
9 | completed: false,
10 | id: 0,
11 | },
12 | ];
13 |
14 | export default function todos(state = initialState, action) {
15 | switch (action.type) {
16 | case ADD_TODO:
17 | return [
18 | {
19 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
20 | completed: false,
21 | text: action.text,
22 | },
23 | ...state,
24 | ];
25 |
26 | case DELETE_TODO:
27 | return state.filter(todo =>
28 | todo.id !== action.id
29 | );
30 |
31 | case EDIT_TODO:
32 | return state.map(todo => (
33 | todo.id === action.id ?
34 | Object.assign({}, todo, { text: action.text }) :
35 | todo
36 | ));
37 |
38 | case COMPLETE_TODO:
39 | return state.map(todo => (
40 | todo.id === action.id ?
41 | Object.assign({}, todo, { completed: !todo.completed }) :
42 | todo
43 | ));
44 |
45 | case COMPLETE_ALL: {
46 | const areAllMarked = state.every(todo => todo.completed);
47 | return state.map(todo => Object.assign({}, todo, {
48 | completed: !areAllMarked,
49 | }));
50 | }
51 |
52 | case CLEAR_COMPLETED:
53 | return state.filter(todo => todo.completed === false);
54 |
55 | default:
56 | return state;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/22-bootstrap/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var autoprefixer = require('autoprefixer');
4 |
5 | module.exports = {
6 | devtool: 'cheap-module-eval-source-map',
7 | entry: [
8 | 'webpack-hot-middleware/client',
9 | 'bootstrap-loader',
10 | './index.js'
11 | ],
12 | output: {
13 | path: path.join(__dirname, 'dist'),
14 | filename: 'bundle.js',
15 | publicPath: '/static/'
16 | },
17 | plugins: [
18 | new webpack.optimize.OccurrenceOrderPlugin(),
19 | new webpack.HotModuleReplacementPlugin()
20 | ],
21 | module: {
22 | loaders: [
23 | {
24 | test: /\.js$/,
25 | loaders: [ 'babel' ],
26 | exclude: /node_modules/,
27 | include: __dirname
28 | },
29 | {
30 | test: /\.css$/,
31 | loaders: [
32 | 'style',
33 | 'css?modules&importLoaders=1&localIdentName=[name]__[local]__[hash:base64:5]',
34 | 'postcss'
35 | ]
36 | },
37 | {
38 | test: /\.scss$/,
39 | loaders: [
40 | 'style',
41 | 'css?modules&importLoaders=2&localIdentName=[name]__[local]__[hash:base64:5]',
42 | 'postcss',
43 | 'sass'
44 | ]
45 | },
46 | {
47 | test: /\.woff2?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
48 | loader: "url?limit=10000"
49 | },
50 | {
51 | test: /\.(ttf|eot|svg)(\?[\s\S]+)?$/,
52 | loader: 'file'
53 | }
54 | ]
55 | },
56 | postcss: [autoprefixer({ browsers: ['last 2 versions'] })]
57 | };
58 |
--------------------------------------------------------------------------------
/16-async/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import {
3 | SELECT_REDDIT, INVALIDATE_REDDIT,
4 | REQUEST_POSTS, RECEIVE_POSTS,
5 | } from '../actions';
6 |
7 | function selectedReddit(state = 'reactjs', action) {
8 | switch (action.type) {
9 | case SELECT_REDDIT:
10 | return action.reddit;
11 | default:
12 | return state;
13 | }
14 | }
15 |
16 | function posts(state = {
17 | isFetching: false,
18 | didInvalidate: false,
19 | items: [],
20 | }, action) {
21 | switch (action.type) {
22 | case INVALIDATE_REDDIT:
23 | return Object.assign({}, state, {
24 | didInvalidate: true,
25 | });
26 | case REQUEST_POSTS:
27 | return Object.assign({}, state, {
28 | isFetching: true,
29 | didInvalidate: false,
30 | });
31 | case RECEIVE_POSTS:
32 | return Object.assign({}, state, {
33 | isFetching: false,
34 | didInvalidate: false,
35 | items: action.posts,
36 | lastUpdated: action.receivedAt,
37 | });
38 | default:
39 | return state;
40 | }
41 | }
42 |
43 | function postsByReddit(state = { }, action) {
44 | switch (action.type) {
45 | case INVALIDATE_REDDIT:
46 | case RECEIVE_POSTS:
47 | case REQUEST_POSTS:
48 | return Object.assign({}, state, {
49 | [action.reddit]: posts(state[action.reddit], action),
50 | });
51 | default:
52 | return state;
53 | }
54 | }
55 |
56 | const rootReducer = combineReducers({
57 | postsByReddit,
58 | selectedReddit,
59 | });
60 |
61 | export default rootReducer;
62 |
--------------------------------------------------------------------------------
/14-15-todomvc/test/components/Header.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import React from 'react';
3 | import TestUtils from 'react-addons-test-utils';
4 | import Header from '../../components/Header';
5 | import TodoTextInput from '../../components/TodoTextInput';
6 |
7 | function setup() {
8 | const props = {
9 | addTodo: expect.createSpy(),
10 | };
11 |
12 | const renderer = TestUtils.createRenderer();
13 | renderer.render();
14 | const output = renderer.getRenderOutput();
15 |
16 | return {
17 | props,
18 | output,
19 | renderer,
20 | };
21 | }
22 |
23 | describe('components', () => {
24 | describe('Header', () => {
25 | it('should render correctly', () => {
26 | const { output } = setup();
27 |
28 | expect(output.type).toBe('header');
29 | expect(output.props.className).toBe('header');
30 |
31 | const [h1, input] = output.props.children;
32 |
33 | expect(h1.type).toBe('h1');
34 | expect(h1.props.children).toBe('todos');
35 |
36 | expect(input.type).toBe(TodoTextInput);
37 | expect(input.props.newTodo).toBe(true);
38 | expect(input.props.placeholder).toBe('What needs to be done?');
39 | });
40 |
41 | it('should call addTodo if length of text is greater than 0', () => {
42 | const { output, props } = setup();
43 | const input = output.props.children[1];
44 | input.props.onSave('');
45 | expect(props.addTodo.calls.length).toBe(0);
46 | input.props.onSave('Use Redux');
47 | expect(props.addTodo.calls.length).toBe(1);
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/16-async/actions/index.js:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-fetch';
2 |
3 | export const REQUEST_POSTS = 'REQUEST_POSTS';
4 | export const RECEIVE_POSTS = 'RECEIVE_POSTS';
5 | export const SELECT_REDDIT = 'SELECT_REDDIT';
6 | export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT';
7 |
8 | export function selectReddit(reddit) {
9 | return {
10 | type: SELECT_REDDIT,
11 | reddit,
12 | };
13 | }
14 |
15 | export function invalidateReddit(reddit) {
16 | return {
17 | type: INVALIDATE_REDDIT,
18 | reddit,
19 | };
20 | }
21 |
22 | function requestPosts(reddit) {
23 | return {
24 | type: REQUEST_POSTS,
25 | reddit,
26 | };
27 | }
28 |
29 | function receivePosts(reddit, json) {
30 | return {
31 | type: RECEIVE_POSTS,
32 | reddit,
33 | posts: json.data.children.map(child => child.data),
34 | receivedAt: Date.now(),
35 | };
36 | }
37 |
38 | function fetchPosts(reddit) {
39 | return dispatch => {
40 | dispatch(requestPosts(reddit));
41 | return fetch(`https://www.reddit.com/r/${reddit}.json`)
42 | .then(response => response.json())
43 | .then(json => dispatch(receivePosts(reddit, json)));
44 | };
45 | }
46 |
47 | function shouldFetchPosts(state, reddit) {
48 | const posts = state.postsByReddit[reddit];
49 | if (!posts) {
50 | return true;
51 | }
52 | if (posts.isFetching) {
53 | return false;
54 | }
55 | return posts.didInvalidate;
56 | }
57 |
58 | export function fetchPostsIfNeeded(reddit) {
59 | return (dispatch, getState) => {
60 | if (shouldFetchPosts(getState(), reddit)) {
61 | return dispatch(fetchPosts(reddit));
62 | }
63 | return null;
64 | };
65 | }
66 |
--------------------------------------------------------------------------------
/05-jsx/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | function Demo1() {
4 | return (
5 |
6 | 类似HTML
7 | 可以嵌套,可以自定义属性
8 |
9 | );
10 | }
11 |
12 | function Demo2() {
13 | const name = 'JSX';
14 | const func = () => {
15 | let result = 'hello ';
16 | if (name) {
17 | result += name;
18 | } else {
19 | result += 'world';
20 | }
21 | return result;
22 | };
23 | return (
24 |
25 | JavaScript表达式
26 | hello {name || 'world'}
27 |
28 | hello {name && 'world'}
29 |
30 |
31 | {func()}
32 |
33 |
34 | );
35 | }
36 |
37 | function Demo3() {
38 | return (
39 |
40 | 样式
41 | 内联样式不是字符串,而是对象
42 |
43 | );
44 | }
45 |
46 | function Demo4() {
47 | return (
48 |
49 | 注释
50 | {/* 注释... */}
51 | 标签子节点内的注释应该写在大括号中
52 |
53 | );
54 | }
55 |
56 | function Demo5() {
57 | const arr = [
58 | 数组
,
59 | 数组会自动展开。注意,数组中每一项元素需要添加key属性
,
60 | ];
61 | return ({arr});
62 | }
63 |
64 | export default class App extends Component { // eslint-disable-line
65 | render() {
66 | return (
67 |
68 |
JSX语法
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/14-15-todomvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "14-15-todomvc",
3 | "version": "0.0.0",
4 | "description": "React Redux example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js",
8 | "test:watch": "npm test -- --watch"
9 | },
10 | "dependencies": {
11 | "babel-polyfill": "^6.3.14",
12 | "classnames": "^2.1.2",
13 | "react": "^15.3.1",
14 | "react-dom": "^15.3.1",
15 | "react-redux": "^4.2.1",
16 | "redux": "^3.2.1"
17 | },
18 | "devDependencies": {
19 | "babel-core": "^6.3.15",
20 | "babel-loader": "^6.2.0",
21 | "babel-preset-es2015": "^6.3.13",
22 | "babel-preset-react": "^6.3.13",
23 | "babel-preset-react-hmre": "^1.1.1",
24 | "babel-register": "^6.3.13",
25 | "cross-env": "^2.0.1",
26 | "expect": "^1.8.0",
27 | "express": "^4.13.3",
28 | "jsdom": "^9.5.0",
29 | "mocha": "^3.0.2",
30 | "node-libs-browser": "^1.0.0",
31 | "raw-loader": "^0.5.1",
32 | "react-addons-test-utils": "^15.3.1",
33 | "style-loader": "^0.13.1",
34 | "todomvc-app-css": "^2.0.1",
35 | "webpack": "^1.9.11",
36 | "webpack-dev-middleware": "^1.2.0",
37 | "webpack-hot-middleware": "^2.9.1"
38 | },
39 | "directories": {
40 | "test": "test"
41 | },
42 | "repository": {
43 | "type": "git",
44 | "url": "git+https://github.com/lewis617/react-redux-book.git"
45 | },
46 | "license": "MIT",
47 | "bugs": {
48 | "url": "https://github.com/lewis617/react-redux-book/issues"
49 | },
50 | "homepage": "https://github.com/lewis617/react-redux-book#readme"
51 | }
52 |
--------------------------------------------------------------------------------
/17-real-world/reducers/index.js:
--------------------------------------------------------------------------------
1 | import * as ActionTypes from '../actions';
2 | import merge from 'lodash/merge';
3 | import paginate from './paginate';
4 | import { routerReducer as routing } from 'react-router-redux';
5 | import { combineReducers } from 'redux';
6 |
7 | // Updates an entity cache in response to any action with response.entities.
8 | function entities(state = { users: {}, repos: {} }, action) {
9 | if (action.response && action.response.entities) {
10 | return merge({}, state, action.response.entities);
11 | }
12 |
13 | return state;
14 | }
15 |
16 | // Updates error message to notify about the failed fetches.
17 | function errorMessage(state = null, action) {
18 | const { type, error } = action;
19 |
20 | if (type === ActionTypes.RESET_ERROR_MESSAGE) {
21 | return null;
22 | } else if (error) {
23 | return action.error;
24 | }
25 |
26 | return state;
27 | }
28 |
29 | // Updates the pagination data for different actions.
30 | const pagination = combineReducers({
31 | starredByUser: paginate({
32 | mapActionToKey: action => action.login,
33 | types: [
34 | ActionTypes.STARRED_REQUEST,
35 | ActionTypes.STARRED_SUCCESS,
36 | ActionTypes.STARRED_FAILURE,
37 | ],
38 | }),
39 | stargazersByRepo: paginate({
40 | mapActionToKey: action => action.fullName,
41 | types: [
42 | ActionTypes.STARGAZERS_REQUEST,
43 | ActionTypes.STARGAZERS_SUCCESS,
44 | ActionTypes.STARGAZERS_FAILURE,
45 | ],
46 | }),
47 | });
48 |
49 | const rootReducer = combineReducers({
50 | entities,
51 | pagination,
52 | errorMessage,
53 | routing,
54 | });
55 |
56 | export default rootReducer;
57 |
--------------------------------------------------------------------------------
/23-26-production/src/utils/Html.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import ReactDOM from 'react-dom/server';
3 | import serialize from 'serialize-javascript';
4 | import Helmet from 'react-helmet';
5 |
6 | function Html(props) {
7 | const { assets, component, store } = props;
8 | const content = component ? ReactDOM.renderToString(component) : '';
9 | const head = Helmet.rewind();
10 |
11 | return (
12 |
13 |
14 | {head.base.toComponent()}
15 | {head.title.toComponent()}
16 | {head.meta.toComponent()}
17 | {head.link.toComponent()}
18 | {head.script.toComponent()}
19 |
20 |
21 |
22 | {Object.keys(assets.styles).map((style, key) =>
23 |
28 | )}
29 |
30 |
31 |
32 |
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | Html.propTypes = {
45 | assets: PropTypes.object,
46 | component: PropTypes.node,
47 | store: PropTypes.object
48 | };
49 |
50 | export default Html;
51 |
--------------------------------------------------------------------------------
/14-15-todomvc/components/TodoTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import classnames from 'classnames';
3 |
4 | class TodoTextInput extends Component {
5 | constructor(props, context) {
6 | super(props, context);
7 | this.state = {
8 | text: this.props.text || '',
9 | };
10 | this.handleBlur = this.handleBlur.bind(this);
11 | this.handleChange = this.handleChange.bind(this);
12 | this.handleSubmit = this.handleSubmit.bind(this);
13 | }
14 |
15 | handleSubmit(e) {
16 | const text = e.target.value.trim();
17 | if (e.which === 13) {
18 | this.props.onSave(text);
19 | if (this.props.newTodo) {
20 | this.setState({ text: '' });
21 | }
22 | }
23 | }
24 |
25 | handleChange(e) {
26 | this.setState({ text: e.target.value });
27 | }
28 |
29 | handleBlur(e) {
30 | if (!this.props.newTodo) {
31 | this.props.onSave(e.target.value);
32 | }
33 | }
34 |
35 | render() {
36 | return (
37 |
51 | );
52 | }
53 | }
54 |
55 | TodoTextInput.propTypes = {
56 | onSave: PropTypes.func.isRequired,
57 | text: PropTypes.string,
58 | placeholder: PropTypes.string,
59 | editing: PropTypes.bool,
60 | newTodo: PropTypes.bool,
61 | };
62 |
63 | export default TodoTextInput;
64 |
--------------------------------------------------------------------------------
/13-counter-test/test/containers/App.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import React from 'react';
3 | import { mount } from 'enzyme';
4 | import { Provider } from 'react-redux';
5 | import App from '../../containers/App';
6 | import configureStore from '../../store/configureStore';
7 |
8 | function setup(initialState) {
9 | const store = configureStore(initialState);
10 | const app = mount(
11 |
12 |
13 |
14 | );
15 | return {
16 | app,
17 | buttons: app.find('button'),
18 | p: app.find('p')
19 | };
20 | }
21 |
22 | describe('containers', () => {
23 | describe('App', () => {
24 | it('should display initial count', () => {
25 | const { p } = setup();
26 | expect(p.text()).toMatch(/^Clicked: 0 times/);
27 | });
28 |
29 | it('should display updated count after increment button click', () => {
30 | const { buttons, p } = setup();
31 | buttons.at(0).simulate('click');
32 | expect(p.text()).toMatch(/^Clicked: 1 times/);
33 | });
34 |
35 | it('should display updated count after decrement button click', () => {
36 | const { buttons, p } = setup();
37 | buttons.at(1).simulate('click');
38 | expect(p.text()).toMatch(/^Clicked: -1 times/);
39 | });
40 |
41 | it('shouldnt change if even and if odd button clicked', () => {
42 | const { buttons, p } = setup();
43 | buttons.at(2).simulate('click');
44 | expect(p.text()).toMatch(/^Clicked: 0 times/);
45 | });
46 |
47 | it('should change if odd and if odd button clicked', () => {
48 | const { buttons, p } = setup({ counter: 1 });
49 | buttons.at(2).simulate('click');
50 | expect(p.text()).toMatch(/^Clicked: 2 times/);
51 | });
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/13-counter-test/test/actions/counter.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import configureStore from 'redux-mock-store';
3 | import thunk from 'redux-thunk';
4 | import * as actions from '../../actions';
5 |
6 | const middlewares = [thunk];
7 | const mockStore = configureStore(middlewares);
8 |
9 |
10 | describe('actions', () => {
11 | describe('counter', () => {
12 | it('increment should create increment action', () => {
13 | expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER });
14 | });
15 |
16 | it('decrement should create decrement action', () => {
17 | expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER });
18 | });
19 |
20 | it('incrementIfOdd should create increment action', () => {
21 | const expectedActions = [
22 | { type: actions.INCREMENT_COUNTER }
23 | ];
24 | const store = mockStore({ counter: 1 });
25 | store.dispatch(actions.incrementIfOdd());
26 | expect(store.getActions()).toEqual(expectedActions);
27 | });
28 |
29 | it('incrementIfOdd shouldnt create increment action if counter is even', () => {
30 | const expectedActions = [];
31 | const store = mockStore({ counter: 2 });
32 | store.dispatch(actions.incrementIfOdd());
33 | expect(store.getActions()).toEqual(expectedActions);
34 | });
35 |
36 | it('incrementAsync should create increment action', done => {
37 | const expectedActions = [
38 | { type: actions.INCREMENT_COUNTER }
39 | ];
40 | const store = mockStore({ counter: 0 });
41 | store.dispatch(actions.incrementAsync(100));
42 | setTimeout(() => {
43 | expect(store.getActions()).toEqual(expectedActions);
44 | done();
45 | }, 100);
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/13-counter-test/test/components/Counter.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import React from 'react';
3 | import { shallow } from 'enzyme';
4 | import Counter from '../../components/Counter';
5 |
6 | function setup(counter = 0) {
7 | const actions = {
8 | increment: expect.createSpy(),
9 | incrementIfOdd: expect.createSpy(),
10 | incrementAsync: expect.createSpy(),
11 | decrement: expect.createSpy()
12 | };
13 | const component = shallow(
14 |
15 | );
16 |
17 | return {
18 | component,
19 | actions,
20 | buttons: component.find('button'),
21 | p: component.find('p')
22 | };
23 | }
24 |
25 | describe('components', () => {
26 | describe('Counter', () => {
27 | it('should display count', () => {
28 | const { p } = setup();
29 | expect(p.text()).toMatch(/^Clicked: 0 times/);
30 | });
31 |
32 | it('first button should call increment', () => {
33 | const { buttons, actions } = setup();
34 | buttons.at(0).simulate('click');
35 | expect(actions.increment).toHaveBeenCalled();
36 | });
37 |
38 | it('second button should call decrement', () => {
39 | const { buttons, actions } = setup();
40 | buttons.at(1).simulate('click');
41 | expect(actions.decrement).toHaveBeenCalled();
42 | });
43 |
44 | it('third button should call incrementIfOdd', () => {
45 | const { buttons, actions } = setup(43);
46 | buttons.at(2).simulate('click');
47 | expect(actions.incrementIfOdd).toHaveBeenCalled();
48 | });
49 |
50 | it('fourth button should call incrementAsync', () => {
51 | const { buttons, actions } = setup();
52 | buttons.at(3).simulate('click');
53 | expect(actions.incrementAsync).toHaveBeenCalled();
54 | });
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/19-3-isomophic-counter/server/server.js:
--------------------------------------------------------------------------------
1 | import Express from 'express';
2 | import qs from 'qs';
3 |
4 | import React from 'react';
5 | import { renderToString } from 'react-dom/server';
6 | import { Provider } from 'react-redux';
7 |
8 | import configureStore from '../common/store/configureStore';
9 | import App from '../common/containers/App';
10 | import { fetchCounter } from '../common/api/counter';
11 |
12 | const app = new Express();
13 | const port = 3000;
14 |
15 | function renderFullPage(html, initialState) {
16 | return `
17 |
18 |
19 |
20 | Redux Universal Example
21 |
22 |
23 | ${html}
24 |
27 |
28 |
29 |
30 | `;
31 | }
32 |
33 | function handleRender(req, res) {
34 | webpackIsomorphicTools.refresh();
35 |
36 | fetchCounter(apiResult => {
37 | const params = qs.parse(req.query);
38 | const counter = parseInt(params.counter, 10) || apiResult || 0;
39 |
40 | const initialState = { counter };
41 |
42 | const store = configureStore(initialState);
43 |
44 | const html = renderToString(
45 |
46 |
47 |
48 | );
49 |
50 | const finalState = store.getState();
51 |
52 | res.send(renderFullPage(html, finalState));
53 | });
54 | }
55 |
56 | app.use(handleRender);
57 |
58 | app.listen(port, (error) => {
59 | if (error) {
60 | console.error(error);
61 | } else {
62 | console.info(`==> 🌎 Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`);
63 | }
64 | });
65 |
--------------------------------------------------------------------------------
/17-real-world/components/Explore.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | const GITHUB_REPO = 'https://github.com/reactjs/redux';
4 |
5 | export default class Explore extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.handleKeyUp = this.handleKeyUp.bind(this);
9 | this.handleGoClick = this.handleGoClick.bind(this);
10 | }
11 |
12 | componentWillReceiveProps(nextProps) {
13 | if (nextProps.value !== this.props.value) {
14 | this.setInputValue(nextProps.value);
15 | }
16 | }
17 |
18 | getInputValue() {
19 | return this.refs.input.value;
20 | }
21 |
22 | setInputValue(val) {
23 | // Generally mutating DOM is a bad idea in React components,
24 | // but doing this for a single uncontrolled field is less fuss
25 | // than making it controlled and maintaining a state for it.
26 | this.refs.input.value = val;
27 | }
28 |
29 | handleKeyUp(e) {
30 | if (e.keyCode === 13) {
31 | this.handleGoClick();
32 | }
33 | }
34 |
35 | handleGoClick() {
36 | this.props.onChange(this.getInputValue());
37 | }
38 |
39 | render() {
40 | return (
41 |
42 |
Type a username or repo full name and hit 'Go':
43 |
49 |
52 |
53 | Code on Github.
54 |
55 |
56 | Move the DevTools with Ctrl+W or hide them with Ctrl+H.
57 |
58 |
59 | );
60 | }
61 | }
62 |
63 | Explore.propTypes = {
64 | value: PropTypes.string.isRequired,
65 | onChange: PropTypes.func.isRequired,
66 | };
67 |
--------------------------------------------------------------------------------
/22-bootstrap/.bootstraprc:
--------------------------------------------------------------------------------
1 | ---
2 | # Bootstrap版本
3 | bootstrapVersion: 3
4 |
5 | # Bootstrap3 中 自定义图标字体路径的开关
6 | useCustomIconFontPath: false
7 |
8 | # Webpack 加载器列表,注意顺序
9 | styleLoaders:
10 | - style
11 | - css
12 | - sass
13 |
14 | # 导出样式到单独的css文件
15 | # 可以根据 NODE_ENV 这个环境变量设置此项
16 | # 也可以在webpack中配置
17 | # entry: 'bootstrap-loader/extractStyles'
18 | extractStyles: false
19 | # env:
20 | # development:
21 | # extractStyles: false
22 | # production:
23 | # extractStyles: true
24 |
25 | # 自定义Bootstrap 变量,在Bootstrap加载前加载,用于重写默认变量,相当于自定义Bootstrap
26 | preBootstrapCustomizations: ./src/theme/bootstrap/pre-customizations.scss
27 |
28 | # 自定义Bootstrap 变量,在Bootstrap加载后加载,用于重写Bootstrap变量,相当于重写Bootstrap
29 | bootstrapCustomizations: ./src/theme/bootstrap/customizations.scss
30 |
31 | # 在此处导入自定义样式
32 | # appStyles: ./app/styles/app.scss
33 |
34 | ### Bootstrap 样式
35 | styles:
36 |
37 | # Mixins
38 | mixins: true
39 |
40 | # Reset and dependencies
41 | normalize: true
42 | print: true
43 | glyphicons: true
44 |
45 | # Core CSS
46 | scaffolding: true
47 | type: true
48 | code: true
49 | grid: true
50 | tables: true
51 | forms: true
52 | buttons: true
53 |
54 | # Components
55 | component-animations: true
56 | dropdowns: true
57 | button-groups: true
58 | input-groups: true
59 | navs: true
60 | navbar: true
61 | breadcrumbs: true
62 | pagination: true
63 | pager: true
64 | labels: true
65 | badges: true
66 | jumbotron: true
67 | thumbnails: true
68 | alerts: true
69 | progress-bars: true
70 | media: true
71 | list-group: true
72 | panels: true
73 | wells: true
74 | responsive-embed: true
75 | close: true
76 |
77 | # Components w/ JavaScript
78 | modals: true
79 | tooltip: true
80 | popovers: true
81 | carousel: true
82 |
83 | # Utility classes
84 | utilities: true
85 | responsive-utilities: true
86 |
87 | ### Bootstrap scripts
88 | scripts: false
89 |
--------------------------------------------------------------------------------
/23-26-production/.bootstraprc:
--------------------------------------------------------------------------------
1 | ---
2 | # Bootstrap版本
3 | bootstrapVersion: 3
4 |
5 | # Bootstrap3 中 自定义图标字体路径的开关
6 | useCustomIconFontPath: false
7 |
8 | # Webpack 加载器列表,注意顺序
9 | styleLoaders:
10 | - style
11 | - css
12 | - sass
13 |
14 | # 导出样式到单独的css文件
15 | # 可以根据 NODE_ENV 这个环境变量设置此项
16 | # 也可以在webpack中配置
17 | # entry: 'bootstrap-loader/extractStyles'
18 | extractStyles: false
19 | # env:
20 | # development:
21 | # extractStyles: false
22 | # production:
23 | # extractStyles: true
24 |
25 | # 自定义Bootstrap 变量,在Bootstrap加载前加载,用于重写默认变量,相当于自定义Bootstrap
26 | preBootstrapCustomizations: ./src/theme/bootstrap/pre-customizations.scss
27 |
28 | # 自定义Bootstrap 变量,在Bootstrap加载后加载,用于重写Bootstrap变量,相当于重写Bootstrap
29 | bootstrapCustomizations: ./src/theme/bootstrap/customizations.scss
30 |
31 | # 在此处导入自定义样式
32 | # appStyles: ./app/styles/app.scss
33 |
34 | ### Bootstrap 样式
35 | styles:
36 |
37 | # Mixins
38 | mixins: true
39 |
40 | # Reset and dependencies
41 | normalize: true
42 | print: true
43 | glyphicons: true
44 |
45 | # Core CSS
46 | scaffolding: true
47 | type: true
48 | code: true
49 | grid: true
50 | tables: true
51 | forms: true
52 | buttons: true
53 |
54 | # Components
55 | component-animations: true
56 | dropdowns: true
57 | button-groups: true
58 | input-groups: true
59 | navs: true
60 | navbar: true
61 | breadcrumbs: true
62 | pagination: true
63 | pager: true
64 | labels: true
65 | badges: true
66 | jumbotron: true
67 | thumbnails: true
68 | alerts: true
69 | progress-bars: true
70 | media: true
71 | list-group: true
72 | panels: true
73 | wells: true
74 | responsive-embed: true
75 | close: true
76 |
77 | # Components w/ JavaScript
78 | modals: true
79 | tooltip: true
80 | popovers: true
81 | carousel: true
82 |
83 | # Utility classes
84 | utilities: true
85 | responsive-utilities: true
86 |
87 | ### Bootstrap scripts
88 | scripts: false
89 |
--------------------------------------------------------------------------------
/23-26-production/test/actions/counter.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import configureStore from 'redux-mock-store';
3 | import thunk from 'redux-thunk';
4 | import * as actions from '../../src/actions/counter';
5 |
6 | const middlewares = [thunk];
7 | const mockStore = configureStore(middlewares);
8 |
9 |
10 | describe('actions', () => {
11 | describe('counter', () => {
12 | it('increment should create increment action', () => {
13 | expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER });
14 | });
15 |
16 | it('decrement should create decrement action', () => {
17 | expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER });
18 | });
19 |
20 | it('incrementIfOdd should create increment action', () => {
21 | const expectedActions = [
22 | { type: actions.INCREMENT_COUNTER }
23 | ];
24 | const getState = { async: { counter: { value: 1 } } };
25 | const store = mockStore(getState);
26 | store.dispatch(actions.incrementIfOdd());
27 | expect(store.getActions()).toEqual(expectedActions);
28 | });
29 |
30 | it('incrementIfOdd shouldnt create increment action if counter is even', () => {
31 | const expectedActions = [];
32 | const getState = { async: { counter: { value: 2 } } };
33 | const store = mockStore(getState);
34 | store.dispatch(actions.incrementIfOdd());
35 | expect(store.getActions()).toEqual(expectedActions);
36 | });
37 |
38 | it('incrementAsync should create increment action', done => {
39 | const expectedActions = [
40 | { type: actions.INCREMENT_COUNTER }
41 | ];
42 | const getState = { async: { counter: { value: 0 } } };
43 | const store = mockStore(getState);
44 | store.dispatch(actions.incrementAsync(100));
45 | setTimeout(() => {
46 | expect(store.getActions()).toEqual(expectedActions);
47 | done();
48 | }, 100);
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/23-26-production/src/containers/Login/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import Helmet from 'react-helmet';
4 | import * as authActions from '../../actions/auth';
5 |
6 | @connect(
7 | state => ({ user: state.async.user }),
8 | authActions
9 | )
10 | export default class Login extends Component {
11 | static propTypes = {
12 | user: PropTypes.any,
13 | login: PropTypes.func,
14 | logout: PropTypes.func
15 | };
16 |
17 | handleSubmit = (event) => {
18 | event.preventDefault();
19 | const input = this.refs.username;
20 | this.props.login(input.value);
21 | input.value = '';
22 | };
23 |
24 | render() {
25 | const { user, logout } = this.props;
26 | const styles = require('./Login.scss');
27 | return (
28 |
29 |
30 |
登录
31 | {!user &&
32 |
33 |
45 |
46 | 此操作将输入的用户名传给API服务器的session。
47 |
48 |
49 | }
50 | {user &&
51 |
52 |
{user.name},您已登录成功!
53 |
54 |
57 |
58 |
59 | }
60 |
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/17-real-world/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { browserHistory } from 'react-router';
4 | import Explore from '../components/Explore';
5 | import { resetErrorMessage } from '../actions';
6 |
7 | class App extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.handleChange = this.handleChange.bind(this);
11 | this.handleDismissClick = this.handleDismissClick.bind(this);
12 | }
13 |
14 | handleDismissClick(e) {
15 | this.props.resetErrorMessage();
16 | e.preventDefault();
17 | }
18 |
19 | handleChange(nextValue) {
20 | browserHistory.push(`/${nextValue}`);
21 | }
22 |
23 | renderErrorMessage() {
24 | const { errorMessage } = this.props;
25 | if (!errorMessage) {
26 | return null;
27 | }
28 |
29 | return (
30 |
31 | {errorMessage}
32 | {' '}
33 | (
37 | Dismiss
38 | )
39 |
40 | );
41 | }
42 |
43 | render() {
44 | const { children, inputValue } = this.props;
45 | return (
46 |
47 |
51 |
52 | {this.renderErrorMessage()}
53 | {children}
54 |
55 | );
56 | }
57 | }
58 |
59 | App.propTypes = {
60 | // Injected by React Redux
61 | errorMessage: PropTypes.string,
62 | resetErrorMessage: PropTypes.func.isRequired,
63 | inputValue: PropTypes.string.isRequired,
64 | // Injected by React Router
65 | children: PropTypes.node,
66 | };
67 |
68 | function mapStateToProps(state, ownProps) {
69 | return {
70 | errorMessage: state.errorMessage,
71 | inputValue: ownProps.location.pathname.substring(1),
72 | };
73 | }
74 |
75 | export default connect(mapStateToProps, {
76 | resetErrorMessage,
77 | })(App);
78 |
--------------------------------------------------------------------------------