├── 18-universal ├── index.js ├── .babelrc ├── server │ └── index.js ├── common │ ├── reducers │ │ ├── index.js │ │ └── counter.js │ ├── api │ │ └── counter.js │ ├── containers │ │ └── App.js │ ├── store │ │ └── configureStore.js │ ├── components │ │ └── Counter.js │ └── actions │ │ └── index.js ├── client │ └── index.js ├── webpack.config.js └── package.json ├── 20-universal-router ├── index.js ├── .babelrc ├── server │ └── index.js ├── static │ └── favicon.ico ├── common │ ├── containers │ │ ├── Home.js │ │ ├── Main.js │ │ └── Counter.js │ ├── reducers │ │ ├── index.js │ │ └── counter.js │ ├── routes.js │ ├── store │ │ └── configureStore.js │ └── actions │ │ └── index.js ├── client │ └── index.js ├── webpack.config.js └── package.json ├── 21-async-router ├── index.js ├── server │ └── index.js ├── static │ └── favicon.ico ├── .babelrc ├── common │ ├── containers │ │ ├── Home.js │ │ ├── Main.js │ │ └── Counter.js │ ├── reducers │ │ ├── index.js │ │ └── counter.js │ ├── api │ │ └── counter.js │ ├── routes.js │ ├── store │ │ └── configureStore.js │ └── actions │ │ └── index.js ├── webpack.config.js ├── client │ └── index.js └── package.json ├── 23-26-production ├── README.md ├── test │ ├── .eslintrc.json │ ├── setup.js │ ├── reducers │ │ └── counter.spec.js │ └── actions │ │ └── counter.spec.js ├── src │ ├── containers │ │ ├── Main │ │ │ └── Main.scss │ │ ├── Statistic │ │ │ ├── Statistic.scss │ │ │ └── Statistic.js │ │ ├── Forms │ │ │ └── Forms.scss │ │ ├── Counter │ │ │ ├── Counter.css │ │ │ └── Counter.png │ │ ├── NotFound │ │ │ └── NotFound.js │ │ ├── Login │ │ │ ├── Login.scss │ │ │ └── Login.js │ │ └── index.js │ ├── components │ │ ├── Table │ │ │ ├── Table.scss │ │ │ ├── CustomPagerComponent.js │ │ │ └── Table.js │ │ ├── Spin │ │ │ ├── Spin.scss │ │ │ └── Spin.js │ │ ├── index.js │ │ └── Charts │ │ │ ├── Line.js │ │ │ └── Column.js │ ├── api │ │ ├── controllers │ │ │ ├── loadAuth.js │ │ │ ├── logout.js │ │ │ ├── login.js │ │ │ ├── forms.js │ │ │ ├── counter.js │ │ │ ├── index.js │ │ │ └── statistic.js │ │ └── api.js │ ├── theme │ │ ├── font-awesome │ │ │ ├── font-awesome.config.scss │ │ │ ├── font-awesome.config.js │ │ │ └── font-awesome.config.prod.js │ │ └── bootstrap │ │ │ ├── pre-customizations.scss │ │ │ └── customizations.scss │ ├── actions │ │ ├── statistic.js │ │ ├── forms.js │ │ ├── counter.js │ │ └── auth.js │ ├── reducers │ │ ├── index.js │ │ └── counter.js │ ├── config.js │ ├── utils │ │ ├── utils.js │ │ ├── configureStore.js │ │ └── Html.js │ ├── client.js │ └── routes.js ├── static │ └── favicon.ico ├── .eslintignore ├── .babelrc ├── .gitignore ├── bin │ ├── api.js │ └── server.js ├── .editorconfig ├── .eslintrc.json ├── webpack │ ├── webpack-dev-server.js │ └── webpack-isomorphic-tools.js └── .bootstraprc ├── 19-3-isomophic-counter ├── index.js ├── .babelrc ├── common │ ├── components │ │ ├── Counter.css │ │ ├── Counter.png │ │ └── Counter.js │ ├── reducers │ │ ├── index.js │ │ └── counter.js │ ├── api │ │ └── counter.js │ ├── containers │ │ └── App.js │ ├── store │ │ └── configureStore.js │ └── actions │ │ └── index.js ├── server │ ├── index.js │ ├── dev-server.js │ └── server.js ├── client │ └── index.js ├── webpack-isomorphic-tools.js ├── webpack.config.js └── package.json ├── 08-start-redux ├── .babelrc ├── index.js ├── package.json └── src │ └── app.js ├── 09-redux-thunk ├── .babelrc ├── index.js ├── package.json └── src │ └── app.js ├── 22-bootstrap ├── src │ ├── App.scss │ └── theme │ │ └── bootstrap │ │ ├── pre-customizations.scss │ │ └── customizations.scss ├── .babelrc ├── index.js ├── index.html ├── server.js ├── package.json ├── webpack.config.js └── .bootstraprc ├── 02-react-node ├── .babelrc ├── index.js ├── src │ ├── App.js │ └── server.js └── package.json ├── 03-react-browser ├── .babelrc ├── src │ ├── App.js │ └── client.js ├── index.html ├── webpack.config.js └── package.json ├── 13-counter-test ├── test │ ├── .eslintrc │ ├── setup.js │ ├── reducers │ │ └── counter.spec.js │ ├── containers │ │ └── App.spec.js │ ├── actions │ │ └── counter.spec.js │ └── components │ │ └── Counter.spec.js ├── .babelrc ├── reducers │ ├── index.js │ └── counter.js ├── index.html ├── containers │ └── App.js ├── index.js ├── store │ └── configureStore.js ├── webpack.config.js ├── actions │ └── index.js ├── server.js ├── components │ └── Counter.js └── package.json ├── 14-15-todomvc ├── test │ ├── .eslintrc │ ├── setup.js │ ├── actions │ │ └── todos.spec.js │ └── components │ │ └── Header.spec.js ├── .babelrc ├── constants │ ├── TodoFilters.js │ └── ActionTypes.js ├── reducers │ ├── index.js │ └── todos.js ├── index.html ├── index.js ├── store │ └── configureStore.js ├── actions │ └── index.js ├── components │ ├── Header.js │ └── TodoTextInput.js ├── server.js ├── webpack.config.js ├── containers │ └── App.js └── package.json ├── front-cover.jpg ├── 19-2-isomorphic-client ├── index.js ├── Counter.png ├── index.html ├── webpack.config.js └── package.json ├── 19-1-isomorphic-server ├── Counter.png ├── index.js └── package.json ├── .eslintignore ├── .travis.yml ├── 05-jsx ├── .babelrc ├── index.js ├── index.html ├── webpack.config.js ├── server.js ├── package.json └── src │ └── App.js ├── 10-counter ├── .babelrc ├── index.html ├── reducers │ └── index.js ├── index.js ├── webpack.config.js ├── server.js ├── package.json └── components │ └── Counter.js ├── 16-async ├── .babelrc ├── index.html ├── components │ ├── Posts.js │ └── Picker.js ├── index.js ├── webpack.config.js ├── server.js ├── store │ └── configureStore.js ├── package.json ├── reducers │ └── index.js └── actions │ └── index.js ├── 04-dev-server ├── .babelrc ├── index.js ├── index.html ├── src │ └── App.js ├── webpack.config.js ├── server.js └── package.json ├── 17-real-world ├── .babelrc ├── containers │ ├── Root.js │ ├── DevTools.js │ ├── Root.prod.js │ ├── Root.dev.js │ └── App.js ├── store │ ├── configureStore.js │ ├── configureStore.prod.js │ └── configureStore.dev.js ├── index.html ├── routes.js ├── index.js ├── components │ ├── User.js │ ├── Repo.js │ ├── List.js │ └── Explore.js ├── webpack.config.js ├── server.js ├── package.json └── reducers │ └── index.js ├── 07-element-instance ├── .babelrc ├── index.html ├── webpack.config.js ├── server.js ├── index.js ├── package.json └── src │ └── App.js ├── 12-undo-devtools ├── .babelrc ├── index.html ├── reducers │ ├── counter.js │ └── index.js ├── actions │ └── index.js ├── index.js ├── webpack.config.js ├── server.js ├── containers │ └── Counter.js └── package.json ├── 06-state-props-context ├── .babelrc ├── index.js ├── index.html ├── src │ ├── App.js │ ├── Counter.js │ ├── Messagelist1.js │ └── Messagelist2.js ├── webpack.config.js ├── server.js └── package.json ├── .gitignore ├── 11-counter-connect ├── index.html ├── .babelrc ├── containers │ ├── Connect1.js │ ├── Connect3.js │ ├── Connect2.js │ ├── Connect4.js │ └── Connect5.js ├── reducers │ ├── index.js │ └── counter.js ├── webpack.config.js ├── actions │ └── index.js ├── server.js ├── components │ └── Counter.js ├── package.json └── index.js ├── .editorconfig ├── .eslintrc.json ├── package.json └── README.md /18-universal/index.js: -------------------------------------------------------------------------------- 1 | require('./client'); 2 | -------------------------------------------------------------------------------- /20-universal-router/index.js: -------------------------------------------------------------------------------- 1 | require('./client'); 2 | -------------------------------------------------------------------------------- /21-async-router/index.js: -------------------------------------------------------------------------------- 1 | require('./client'); 2 | -------------------------------------------------------------------------------- /23-26-production/README.md: -------------------------------------------------------------------------------- 1 | # React Redux Book 2 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/index.js: -------------------------------------------------------------------------------- 1 | require('./client'); 2 | -------------------------------------------------------------------------------- /08-start-redux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /09-redux-thunk/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /22-bootstrap/src/App.scss: -------------------------------------------------------------------------------- 1 | .buttons { 2 | display: flex; 3 | } -------------------------------------------------------------------------------- /02-react-node/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /18-universal/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /03-react-browser/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /08-start-redux/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./src/app.js'); 3 | -------------------------------------------------------------------------------- /09-redux-thunk/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./src/app.js'); 3 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /02-react-node/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./src/server.js'); 3 | -------------------------------------------------------------------------------- /13-counter-test/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /14-15-todomvc/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /18-universal/server/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./server'); 3 | -------------------------------------------------------------------------------- /20-universal-router/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react","stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /20-universal-router/server/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./server'); 3 | -------------------------------------------------------------------------------- /23-26-production/test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /front-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewis617/react-redux-book/HEAD/front-cover.jpg -------------------------------------------------------------------------------- /19-2-isomorphic-client/index.js: -------------------------------------------------------------------------------- 1 | document.write(`Counter`); 2 | -------------------------------------------------------------------------------- /21-async-router/server/index.js: -------------------------------------------------------------------------------- 1 | __SERVER__ = true; 2 | require('babel-register'); 3 | require('./server'); 4 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/common/components/Counter.css: -------------------------------------------------------------------------------- 1 | .counter { 2 | width: 50px; 3 | margin: -10px 10px; 4 | } 5 | -------------------------------------------------------------------------------- /23-26-production/src/containers/Main/Main.scss: -------------------------------------------------------------------------------- 1 | :global(.navbar) { 2 | margin-bottom: 0; 3 | border-radius: 0; 4 | } 5 | -------------------------------------------------------------------------------- /19-1-isomorphic-server/Counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewis617/react-redux-book/HEAD/19-1-isomorphic-server/Counter.png -------------------------------------------------------------------------------- /19-2-isomorphic-client/Counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewis617/react-redux-book/HEAD/19-2-isomorphic-client/Counter.png -------------------------------------------------------------------------------- /21-async-router/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewis617/react-redux-book/HEAD/21-async-router/static/favicon.ico -------------------------------------------------------------------------------- /23-26-production/src/containers/Statistic/Statistic.scss: -------------------------------------------------------------------------------- 1 | .statistic{ 2 | :global(.row){ 3 | margin-top: 20px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /23-26-production/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewis617/react-redux-book/HEAD/23-26-production/static/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | **/static/** 3 | **/build/** 4 | **/node_modules/** 5 | **/webpack*.js 6 | **/webpack/** 7 | **/api/** 8 | -------------------------------------------------------------------------------- /02-react-node/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function App() { 4 | return

hello world

; 5 | } 6 | -------------------------------------------------------------------------------- /20-universal-router/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewis617/react-redux-book/HEAD/20-universal-router/static/favicon.ico -------------------------------------------------------------------------------- /23-26-production/.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | **/static/** 3 | **/build/** 4 | **/node_modules/** 5 | **/webpack*.js 6 | **/webpack/** 7 | -------------------------------------------------------------------------------- /23-26-production/src/components/Table/Table.scss: -------------------------------------------------------------------------------- 1 | :global{ 2 | .footer-container { 3 | background-color: white; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" 5 | - "5" 6 | - "stable" 7 | 8 | script: 9 | - npm run lint 10 | -------------------------------------------------------------------------------- /03-react-browser/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function App() { 4 | return

hello world

; 5 | } 6 | -------------------------------------------------------------------------------- /23-26-production/src/containers/Forms/Forms.scss: -------------------------------------------------------------------------------- 1 | .forms{ 2 | :global(h4){ 3 | text-align: center; 4 | } 5 | margin-top: 20px; 6 | } 7 | -------------------------------------------------------------------------------- /21-async-router/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react","stage-0"], 3 | "plugins": [ 4 | "transform-decorators-legacy" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /23-26-production/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react","stage-0"], 3 | "plugins": [ 4 | "transform-decorators-legacy" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /05-jsx/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /10-counter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /16-async/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /23-26-production/src/containers/Counter/Counter.css: -------------------------------------------------------------------------------- 1 | .counterPage{ 2 | margin-top: 20px; 3 | } 4 | .counter{ 5 | width:50px; 6 | margin: 10px; 7 | } 8 | -------------------------------------------------------------------------------- /23-26-production/src/containers/Counter/Counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewis617/react-redux-book/HEAD/23-26-production/src/containers/Counter/Counter.png -------------------------------------------------------------------------------- /04-dev-server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /13-counter-test/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /14-15-todomvc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /17-real-world/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/common/components/Counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewis617/react-redux-book/HEAD/19-3-isomophic-counter/common/components/Counter.png -------------------------------------------------------------------------------- /22-bootstrap/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /07-element-instance/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /12-undo-devtools/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /06-state-props-context/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /14-15-todomvc/constants/TodoFilters.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all'; 2 | export const SHOW_COMPLETED = 'show_completed'; 3 | export const SHOW_ACTIVE = 'show_active'; 4 | -------------------------------------------------------------------------------- /05-jsx/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './src/App'; 4 | 5 | render(, document.querySelector('#root')); 6 | 7 | -------------------------------------------------------------------------------- /23-26-production/src/api/controllers/loadAuth.js: -------------------------------------------------------------------------------- 1 | export default app => { 2 | app.get('/loadAuth', (req, res) => { 3 | res.json(req.session.user || null); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /04-dev-server/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './src/App'; 4 | 5 | render((), document.querySelector('#root')); 6 | -------------------------------------------------------------------------------- /23-26-production/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | 3 | node_modules 4 | 5 | .idea 6 | 7 | webpack-assets.json 8 | 9 | dist 10 | 11 | .vscode 12 | 13 | jsconfig.json 14 | 15 | build 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | 3 | node_modules 4 | 5 | .idea 6 | 7 | webpack-assets.json 8 | 9 | dist 10 | 11 | .vscode 12 | 13 | jsconfig.json 14 | 15 | build 16 | 17 | yarn.lock 18 | -------------------------------------------------------------------------------- /03-react-browser/src/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './App'; 4 | 5 | render((), document.querySelector('#app')); 6 | -------------------------------------------------------------------------------- /17-real-world/containers/Root.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./Root.prod'); 3 | } else { 4 | module.exports = require('./Root.dev'); 5 | } 6 | -------------------------------------------------------------------------------- /06-state-props-context/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './src/App'; 4 | 5 | render((), document.querySelector('#root')); 6 | -------------------------------------------------------------------------------- /21-async-router/common/containers/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | Home 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /22-bootstrap/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './src/App'; 4 | 5 | render(( 6 | 7 | ), document.querySelector('#root')); 8 | -------------------------------------------------------------------------------- /23-26-production/src/components/Spin/Spin.scss: -------------------------------------------------------------------------------- 1 | .spin{ 2 | position:fixed; 3 | top:50%; 4 | left:50%; 5 | transform: translate(-50%, -50%); 6 | color: #9d9d9d; 7 | z-index: 999; 8 | } 9 | -------------------------------------------------------------------------------- /20-universal-router/common/containers/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | Home 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /14-15-todomvc/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import todos from './todos'; 3 | 4 | const rootReducer = combineReducers({ 5 | todos, 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /17-real-world/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.prod'); 3 | } else { 4 | module.exports = require('./configureStore.dev'); 5 | } 6 | -------------------------------------------------------------------------------- /23-26-production/src/theme/font-awesome/font-awesome.config.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration file for font-awesome-webpack 3 | * 4 | */ 5 | 6 | // Example: 7 | //$fa-inverse: #eee; 8 | //$fa-border-color: #ddd; 9 | -------------------------------------------------------------------------------- /02-react-node/src/server.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderToString } from 'react-dom/server'; 3 | import App from './App'; 4 | 5 | const appHTML = renderToString(); 6 | 7 | console.log(appHTML); 8 | -------------------------------------------------------------------------------- /13-counter-test/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counter from './counter'; 3 | 4 | const rootReducer = combineReducers({ 5 | counter, 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /18-universal/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counter from './counter'; 3 | 4 | const rootReducer = combineReducers({ 5 | counter, 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /23-26-production/bin/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | if (process.env.NODE_ENV === 'production') { 3 | require('../build/api/api'); 4 | } 5 | else { 6 | require('babel-register'); 7 | require('../src/api/api'); 8 | } 9 | -------------------------------------------------------------------------------- /13-counter-test/test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom'; 2 | 3 | global.document = jsdom(''); 4 | global.window = document.defaultView; 5 | global.navigator = global.window.navigator; 6 | -------------------------------------------------------------------------------- /14-15-todomvc/test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom'; 2 | 3 | global.document = jsdom(''); 4 | global.window = document.defaultView; 5 | global.navigator = global.window.navigator; 6 | -------------------------------------------------------------------------------- /20-universal-router/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counter from './counter'; 3 | 4 | const rootReducer = combineReducers({ 5 | counter 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counter from './counter'; 3 | 4 | const rootReducer = combineReducers({ 5 | counter, 6 | }); 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /23-26-production/test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom'; 2 | 3 | global.document = jsdom(''); 4 | global.window = document.defaultView; 5 | global.navigator = global.window.navigator; 6 | -------------------------------------------------------------------------------- /05-jsx/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /04-dev-server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /10-counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /19-2-isomorphic-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /22-bootstrap/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /11-counter-connect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /12-undo-devtools/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /13-counter-test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /22-bootstrap/src/theme/bootstrap/pre-customizations.scss: -------------------------------------------------------------------------------- 1 | // Customize Bootstrap variables that get imported before the original Bootstrap variables. 2 | // Thus original Bootstrap variables can depend on values from here. 3 | $brand-primary: #5cb85c; 4 | -------------------------------------------------------------------------------- /23-26-production/src/theme/bootstrap/pre-customizations.scss: -------------------------------------------------------------------------------- 1 | // Customize Bootstrap variables that get imported before the original Bootstrap variables. 2 | // Thus original Bootstrap variables can depend on values from here. 3 | $brand-primary: #5bc0de; 4 | -------------------------------------------------------------------------------- /23-26-production/src/theme/font-awesome/font-awesome.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | styles: { 3 | mixins: true, 4 | core: true, 5 | icons: true, 6 | larger: true, 7 | path: true, 8 | animated: true 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /06-state-props-context/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /11-counter-connect/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react","stage-0"], 3 | "plugins": [ 4 | "transform-decorators-legacy" 5 | ], 6 | "env": { 7 | "development": { 8 | "presets": ["react-hmre"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /16-async/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux async example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /17-real-world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux real-world example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /23-26-production/src/api/controllers/logout.js: -------------------------------------------------------------------------------- 1 | export default app => { 2 | app.get('/logout', (req, res) => { 3 | let session = req.session; 4 | session.destroy(() => { 5 | session = null; 6 | }); 7 | res.json(null); 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /23-26-production/src/containers/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

404!

7 |

您要找的页面不存在!

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /14-15-todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux TodoMVC example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /10-counter/reducers/index.js: -------------------------------------------------------------------------------- 1 | export default function counter(state = 0, action) { 2 | switch (action.type) { 3 | case 'INCREMENT': 4 | return state + 1; 5 | case 'DECREMENT': 6 | return state - 1; 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /18-universal/common/api/counter.js: -------------------------------------------------------------------------------- 1 | function getRandomInt(min, max) { 2 | return Math.floor(Math.random() * (max - min)) + min; 3 | } 4 | 5 | export function fetchCounter(callback) { 6 | setTimeout(() => { 7 | callback(getRandomInt(1, 100)); 8 | }, 500); 9 | } 10 | -------------------------------------------------------------------------------- /13-counter-test/containers/App.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import * as ActionCreators from '../actions'; 3 | import Counter from '../components/Counter'; 4 | 5 | export default connect( 6 | state => ({ counter: state.counter }), 7 | ActionCreators 8 | )(Counter); 9 | -------------------------------------------------------------------------------- /03-react-browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Demo 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /11-counter-connect/containers/Connect1.js: -------------------------------------------------------------------------------- 1 | import Counter from '../components/Counter'; 2 | import { connect } from 'react-redux'; 3 | import * as ActionCreators from '../actions'; 4 | 5 | export default connect( 6 | state => ({ counter: state.counter }), 7 | ActionCreators 8 | )(Counter); 9 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/common/api/counter.js: -------------------------------------------------------------------------------- 1 | function getRandomInt(min, max) { 2 | return Math.floor(Math.random() * (max - min)) + min; 3 | } 4 | 5 | export function fetchCounter(callback) { 6 | setTimeout(() => { 7 | callback(getRandomInt(1, 100)); 8 | }, 500); 9 | } 10 | -------------------------------------------------------------------------------- /23-26-production/src/api/controllers/login.js: -------------------------------------------------------------------------------- 1 | export default app => { 2 | app.post('/login', (req, res) => { 3 | const user = { 4 | name: req.body.name 5 | }; 6 | const session = req.session; 7 | session.user = user; 8 | res.json(user); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /23-26-production/src/containers/Login/Login.scss: -------------------------------------------------------------------------------- 1 | .loginPage { 2 | input { 3 | padding: 5px 10px; 4 | border-radius: 5px; 5 | border: 1px solid #ccc; 6 | } 7 | form { 8 | margin: 30px 0; 9 | :global(.btn) { 10 | margin-left: 10px; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /03-react-browser/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/client', 3 | output: { 4 | path: __dirname + '/static/dist', 5 | filename: 'main.js' 6 | }, 7 | module: { 8 | loaders: [{ test: /\.js$/, exclude: /node_modules/, loaders: ['babel'] }] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /14-15-todomvc/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TODO = 'ADD_TODO'; 2 | export const DELETE_TODO = 'DELETE_TODO'; 3 | export const EDIT_TODO = 'EDIT_TODO'; 4 | export const COMPLETE_TODO = 'COMPLETE_TODO'; 5 | export const COMPLETE_ALL = 'COMPLETE_ALL'; 6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'; 7 | -------------------------------------------------------------------------------- /21-async-router/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducerCreator } from 'redux-amrc'; 3 | import counter from './counter'; 4 | 5 | const rootReducer = combineReducers({ 6 | async: reducerCreator({ counter }) 7 | }); 8 | 9 | export default rootReducer; 10 | -------------------------------------------------------------------------------- /23-26-production/src/actions/statistic.js: -------------------------------------------------------------------------------- 1 | import { ASYNC } from 'redux-amrc'; 2 | import { customFetch } from '../utils/utils'; 3 | 4 | export function loadStatistic() { 5 | return { 6 | [ASYNC]: { 7 | key: 'statistic', 8 | promise: () => customFetch('/statistic') 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /22-bootstrap/src/theme/bootstrap/customizations.scss: -------------------------------------------------------------------------------- 1 | // Customize variables 2 | // This gets loaded after bootstrap/variables is loaded, so you can refer to bootstrap variables 3 | // Here we're adjusting the hue of that default: 4 | $btn-primary-bg: adjust-hue($btn-primary-bg, +70deg); 5 | $btn-primary-border: $btn-primary-bg; 6 | -------------------------------------------------------------------------------- /23-26-production/src/containers/index.js: -------------------------------------------------------------------------------- 1 | export Main from './Main/Main'; 2 | export Home from './Home/Home'; 3 | export NotFound from './NotFound/NotFound'; 4 | export Counter from './Counter/Counter'; 5 | export Forms from './Forms/Forms'; 6 | export Statistic from './Statistic/Statistic'; 7 | export Login from './Login/Login'; 8 | -------------------------------------------------------------------------------- /23-26-production/src/theme/bootstrap/customizations.scss: -------------------------------------------------------------------------------- 1 | // Customize variables 2 | // This gets loaded after bootstrap/variables is loaded, so you can refer to bootstrap variables 3 | // Here we're adjusting the hue of that default: 4 | $btn-primary-bg: adjust-hue($btn-primary-bg, +70deg); 5 | $btn-primary-border: $btn-primary-bg; 6 | -------------------------------------------------------------------------------- /11-counter-connect/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counter from './counter'; 3 | 4 | /** 5 | * 虽然本例中只有一个reducer,但还是使用了`combineReducers`来进行合并,便于后期的拓展。 6 | * 在进行合并后,计数器的数值将被转移到`state.counter`中。 7 | */ 8 | 9 | const rootReducer = combineReducers({ 10 | counter, 11 | }); 12 | 13 | export default rootReducer; 14 | -------------------------------------------------------------------------------- /19-1-isomorphic-server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); 3 | 4 | var config = { assets: { images: { extensions: ['png'] } } }; 5 | 6 | new WebpackIsomorphicTools(config) 7 | .development() 8 | .server(__dirname, function () { 9 | console.log(require('./Counter.png')) 10 | }); 11 | -------------------------------------------------------------------------------- /16-async/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | function Posts({ posts }) { 4 | return ( 5 |
    6 | {posts.map((post, i) => 7 |
  • {post.title}
  • 8 | )} 9 |
10 | ); 11 | } 12 | 13 | Posts.propTypes = { 14 | posts: PropTypes.array.isRequired, 15 | }; 16 | 17 | export default Posts; 18 | -------------------------------------------------------------------------------- /23-26-production/src/theme/font-awesome/font-awesome.config.prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | styleLoader: require('extract-text-webpack-plugin') 3 | .extract('style-loader', 'css-loader!sass-loader'), 4 | styles: { 5 | mixins: true, 6 | core: true, 7 | icons: true, 8 | larger: true, 9 | path: true, 10 | animated: true 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /11-counter-connect/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions'; 2 | 3 | export default function counter(state = 0, action) { 4 | switch (action.type) { 5 | case INCREMENT_COUNTER: 6 | return state + 1; 7 | case DECREMENT_COUNTER: 8 | return state - 1; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /12-undo-devtools/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions'; 2 | 3 | export default function counter(state = 0, action) { 4 | switch (action.type) { 5 | case INCREMENT_COUNTER: 6 | return state + 1; 7 | case DECREMENT_COUNTER: 8 | return state - 1; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /13-counter-test/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions'; 2 | 3 | export default function counter(state = 0, action) { 4 | switch (action.type) { 5 | case INCREMENT_COUNTER: 6 | return state + 1; 7 | case DECREMENT_COUNTER: 8 | return state - 1; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /04-dev-server/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | // export default function App() { 4 | // return

hello world

; 5 | // } 6 | export default class App extends Component { // eslint-disable-line 7 | render() { 8 | // console.log('Source Map Testing'); 9 | // throw new Error(); 10 | return

hello world

; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /23-26-production/src/api/controllers/forms.js: -------------------------------------------------------------------------------- 1 | export default app => { 2 | app.post('/forms', (req, res) => { 3 | const body = req.body; 4 | body.submitTime = new Date; 5 | setTimeout(() => { 6 | if (Math.random() < 0.33) { 7 | res.status(500).end(); 8 | } else { 9 | res.json(body); 10 | } 11 | }, 1000); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /11-counter-connect/containers/Connect3.js: -------------------------------------------------------------------------------- 1 | import Counter from '../components/Counter'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import * as ActionCreators from '../actions'; 5 | 6 | export default connect( 7 | state => ({ counter: state.counter }), 8 | dispatch => bindActionCreators(ActionCreators, dispatch) 9 | )(Counter); 10 | -------------------------------------------------------------------------------- /23-26-production/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducerCreator } from 'redux-amrc'; 3 | import { reducer as formReducer } from 'redux-form'; 4 | import counter from './counter'; 5 | 6 | const rootReducer = combineReducers({ 7 | async: reducerCreator({ counter }), 8 | form: formReducer 9 | }); 10 | 11 | export default rootReducer; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | 4 | [*] 5 | 6 | # Change these settings to your own preference 7 | indent_style = space 8 | indent_size = 2 9 | 10 | # We recommend you to keep these unchanged 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | max_line_length = 0 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /17-real-world/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /17-real-world/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import api from '../middleware/api'; 4 | import rootReducer from '../reducers'; 5 | 6 | export default function configureStore(initialState) { 7 | return createStore( 8 | rootReducer, 9 | initialState, 10 | applyMiddleware(thunk, api) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /23-26-production/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | 4 | [*] 5 | 6 | # Change these settings to your own preference 7 | indent_style = space 8 | indent_size = 2 9 | 10 | # We recommend you to keep these unchanged 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | max_line_length = 0 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /23-26-production/src/components/index.js: -------------------------------------------------------------------------------- 1 | export Spin from './Spin/Spin'; 2 | export SimpleForm from './ReduxForms/SimpleForm'; 3 | export SynchronousValidationForm from './ReduxForms/SynchronousValidationForm'; 4 | export AsynchronousBlurValidationForm from './ReduxForms/AsynchronousBlurValidationForm'; 5 | export Line from './Charts/Line'; 6 | export Column from './Charts/Column'; 7 | export Table from './Table/Table'; 8 | -------------------------------------------------------------------------------- /13-counter-test/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './containers/App'; 5 | import configureStore from './store/configureStore'; 6 | 7 | const store = configureStore(); 8 | const rootEl = document.getElementById('root'); 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , rootEl); 14 | -------------------------------------------------------------------------------- /16-async/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 App from './containers/App'; 6 | import configureStore from './store/configureStore'; 7 | 8 | const store = configureStore(); 9 | 10 | render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /07-element-instance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /17-real-world/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router'; 3 | import App from './containers/App'; 4 | import UserPage from './containers/UserPage'; 5 | import RepoPage from './containers/RepoPage'; 6 | 7 | export default ( 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /20-universal-router/common/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | import Counter from './containers/Counter'; 4 | import Main from './containers/Main'; 5 | import Home from './containers/Home'; 6 | 7 | export default () => ( 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /18-universal/common/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { SET_COUNTER, INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions'; 2 | 3 | export default function counter(state = 0, action) { 4 | switch (action.type) { 5 | case SET_COUNTER: 6 | return action.payload; 7 | case INCREMENT_COUNTER: 8 | return state + 1; 9 | case DECREMENT_COUNTER: 10 | return state - 1; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('babel-register'); 3 | 4 | var path = require('path'); 5 | var rootDir = path.resolve(__dirname, '..'); 6 | 7 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); 8 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack-isomorphic-tools')) 9 | .development() 10 | .server(rootDir, function() { 11 | require('./server') 12 | }); 13 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/common/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { SET_COUNTER, INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions'; 2 | 3 | export default function counter(state = 0, action) { 4 | switch (action.type) { 5 | case SET_COUNTER: 6 | return action.payload; 7 | case INCREMENT_COUNTER: 8 | return state + 1; 9 | case DECREMENT_COUNTER: 10 | return state - 1; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /20-universal-router/common/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { SET_COUNTER, INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions'; 2 | 3 | export default function counter(state = 0, action) { 4 | switch (action.type) { 5 | case SET_COUNTER: 6 | return action.payload; 7 | case INCREMENT_COUNTER: 8 | return state + 1; 9 | case DECREMENT_COUNTER: 10 | return state - 1; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /23-26-production/src/api/controllers/counter.js: -------------------------------------------------------------------------------- 1 | function getRandomInt(min, max) { 2 | return Math.floor(Math.random() * (max - min)) + min; 3 | } 4 | export default app => { 5 | app.get('/counter', (req, res) => { 6 | setTimeout(() => { 7 | if (Math.random() < 0.33) { 8 | res.status(500).end(); 9 | } else { 10 | res.json({ 11 | value: getRandomInt(1, 100) 12 | }); 13 | } 14 | }, 1000); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /14-15-todomvc/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 App from './containers/App'; 6 | import configureStore from './store/configureStore'; 7 | import 'todomvc-app-css/index.css'; 8 | 9 | const store = configureStore(); 10 | 11 | render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /21-async-router/common/api/counter.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | app.get('/api/counter', (req, res) => { 3 | setTimeout(function () { 4 | if (Math.random() < 0.33) { 5 | res.status(500).end(); 6 | } else { 7 | res.json({ 8 | value: getRandomInt(1, 100) 9 | }); 10 | } 11 | }, 1000) 12 | }) 13 | }; 14 | 15 | function getRandomInt(min, max) { 16 | return Math.floor(Math.random() * (max - min)) + min; 17 | } 18 | -------------------------------------------------------------------------------- /23-26-production/src/actions/forms.js: -------------------------------------------------------------------------------- 1 | import { ASYNC } from 'redux-amrc'; 2 | import { customFetch } from '../utils/utils'; 3 | 4 | export function post(data) { 5 | const key = 'forms'; 6 | const option = { 7 | method: 'post', 8 | headers: { 9 | 'Content-Type': 'application/json' 10 | }, 11 | body: JSON.stringify(data) 12 | }; 13 | return { 14 | [ASYNC]: { 15 | key, 16 | promise: () => customFetch('/forms', option) 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /17-real-world/containers/Root.prod.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import routes from '../routes'; 4 | import { Router } from 'react-router'; 5 | 6 | const Root = ({ store, history }) => ( 7 | 8 | 9 | 10 | ); 11 | 12 | Root.propTypes = { 13 | store: PropTypes.object.isRequired, 14 | history: PropTypes.object.isRequired, 15 | }; 16 | 17 | export default Root; 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "rules": { 9 | "import/no-unresolved": 0, 10 | "comma-dangle": 0, 11 | "no-console": 0, 12 | "no-alert": 0, 13 | "prefer-template": 0, 14 | "global-require": 0, 15 | "no-underscore-dangle": 0, 16 | "no-undef": 0, 17 | "react/jsx-space-before-closing": 0 18 | }, 19 | "parser": "babel-eslint", 20 | "plugins": [ 21 | "react" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /18-universal/common/containers/App.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import Counter from '../components/Counter'; 4 | import * as CounterActions from '../actions'; 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | counter: state.counter, 9 | }; 10 | } 11 | 12 | function mapDispatchToProps(dispatch) { 13 | return bindActionCreators(CounterActions, dispatch); 14 | } 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter); 17 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/common/containers/App.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import Counter from '../components/Counter'; 4 | import * as CounterActions from '../actions'; 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | counter: state.counter, 9 | }; 10 | } 11 | 12 | function mapDispatchToProps(dispatch) { 13 | return bindActionCreators(CounterActions, dispatch); 14 | } 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter); 17 | -------------------------------------------------------------------------------- /14-15-todomvc/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import rootReducer from '../reducers'; 3 | 4 | export default function configureStore(initialState) { 5 | const store = createStore(rootReducer, initialState); 6 | 7 | if (module.hot) { 8 | // Enable Webpack hot module replacement for reducers 9 | module.hot.accept('../reducers', () => { 10 | const nextReducer = require('../reducers').default; 11 | store.replaceReducer(nextReducer); 12 | }); 13 | } 14 | 15 | return store; 16 | } 17 | -------------------------------------------------------------------------------- /23-26-production/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "rules": { 9 | "import/no-unresolved": 0, 10 | "comma-dangle": 0, 11 | "no-console": 0, 12 | "no-alert": 0, 13 | "prefer-template": 0, 14 | "global-require": 0, 15 | "no-underscore-dangle": 0, 16 | "no-undef": 0, 17 | "react/jsx-space-before-closing": 0 18 | }, 19 | "parser": "babel-eslint", 20 | "plugins": [ 21 | "react" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /06-state-props-context/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Counter from './Counter'; 3 | import Messagelist1 from './Messagelist1'; 4 | import Messagelist2 from './Messagelist2'; 5 | 6 | export default class App extends Component { //eslint-disable-line 7 | render() { 8 | return ( 9 |
10 |

State与props

11 | 12 |
13 |

Props与context

14 | 15 | 16 |
17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /17-real-world/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { browserHistory } from 'react-router'; 5 | import { syncHistoryWithStore } from 'react-router-redux'; 6 | import Root from './containers/Root'; 7 | import configureStore from './store/configureStore'; 8 | 9 | const store = configureStore(); 10 | const history = syncHistoryWithStore(browserHistory, store); 11 | 12 | render( 13 | , 14 | document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /11-counter-connect/containers/Connect2.js: -------------------------------------------------------------------------------- 1 | import Counter from '../components/Counter'; 2 | import { connect } from 'react-redux'; 3 | import { increment, decrement, incrementIfOdd, incrementAsync } from '../actions'; 4 | 5 | export default connect( 6 | state => ({ counter: state.counter }), 7 | dispatch => ({ 8 | increment: () => dispatch(increment()), 9 | decrement: () => dispatch(decrement()), 10 | incrementIfOdd: () => dispatch(incrementIfOdd()), 11 | incrementAsync: () => dispatch(incrementAsync()), 12 | }) 13 | )(Counter); 14 | -------------------------------------------------------------------------------- /18-universal/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 configureStore from '../common/store/configureStore'; 6 | import App from '../common/containers/App'; 7 | 8 | const initialState = window.__INITIAL_STATE__; 9 | const store = configureStore(initialState); 10 | const rootElement = document.getElementById('app'); 11 | 12 | render( 13 | 14 | 15 | , 16 | rootElement 17 | ); 18 | -------------------------------------------------------------------------------- /21-async-router/common/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions'; 2 | 3 | const initialState = { 4 | value: 0 5 | }; 6 | 7 | export function counter(state = initialState, action) { 8 | switch (action.type) { 9 | case INCREMENT_COUNTER: 10 | return { 11 | value: state.value + 1 12 | }; 13 | case DECREMENT_COUNTER: 14 | return { 15 | value: state.value - 1 16 | }; 17 | default: 18 | return state; 19 | } 20 | } 21 | 22 | export default counter; 23 | -------------------------------------------------------------------------------- /23-26-production/src/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter'; 2 | 3 | const initialState = { 4 | value: 0 5 | }; 6 | 7 | function counter(state = initialState, action) { 8 | switch (action.type) { 9 | case INCREMENT_COUNTER: 10 | return { 11 | value: state.value + 1 12 | }; 13 | case DECREMENT_COUNTER: 14 | return { 15 | value: state.value - 1 16 | }; 17 | default: 18 | return state; 19 | } 20 | } 21 | 22 | export default counter; 23 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/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 configureStore from '../common/store/configureStore'; 6 | import App from '../common/containers/App'; 7 | 8 | const initialState = window.__INITIAL_STATE__; 9 | const store = configureStore(initialState); 10 | const rootElement = document.getElementById('app'); 11 | 12 | render( 13 | 14 | 15 | , 16 | rootElement 17 | ); 18 | -------------------------------------------------------------------------------- /12-undo-devtools/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import undoable, { includeAction } from 'redux-undo'; 3 | import counter from './counter'; 4 | import { INCREMENT_COUNTER, DECREMENT_COUNTER, UNDO_COUNTER, REDO_COUNTER } from '../actions'; 5 | 6 | const rootReducer = combineReducers({ 7 | counter: undoable(counter, { 8 | filter: includeAction([INCREMENT_COUNTER, DECREMENT_COUNTER]), 9 | limit: 10, 10 | debug: true, 11 | undoType: UNDO_COUNTER, 12 | redoType: REDO_COUNTER 13 | }) 14 | }); 15 | 16 | export default rootReducer; 17 | -------------------------------------------------------------------------------- /23-26-production/src/api/controllers/index.js: -------------------------------------------------------------------------------- 1 | import counter from './counter'; 2 | import forms from './forms'; 3 | import statistic from './statistic'; 4 | import loadAuth from './loadAuth'; 5 | import login from './login'; 6 | import logout from './logout'; 7 | 8 | export default app => { 9 | loadAuth(app); 10 | login(app); 11 | logout(app); 12 | 13 | app.use((req, res, next) => { 14 | if (req.session.user) { 15 | next(); 16 | } else { 17 | res.sendStatus(401); 18 | } 19 | }); 20 | 21 | counter(app); 22 | forms(app); 23 | statistic(app); 24 | }; 25 | -------------------------------------------------------------------------------- /17-real-world/containers/Root.dev.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import routes from '../routes'; 4 | import DevTools from './DevTools'; 5 | import { Router } from 'react-router'; 6 | 7 | const Root = ({ store, history }) => ( 8 | 9 |
10 | 11 | 12 |
13 |
14 | ); 15 | 16 | Root.propTypes = { 17 | store: PropTypes.object.isRequired, 18 | history: PropTypes.object.isRequired, 19 | }; 20 | 21 | export default Root; 22 | -------------------------------------------------------------------------------- /23-26-production/src/components/Spin/Spin.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | function Spin(props) { 5 | const styles = require('./Spin.scss'); 6 | return ( 7 |
8 | {props.loadingNumber > 0 && ( 9 | 10 | )} 11 |
12 | ); 13 | } 14 | 15 | Spin.propTypes = { 16 | loadingNumber: PropTypes.number.isRequired 17 | }; 18 | 19 | export default connect( 20 | state => ({ loadingNumber: state.async.loadingNumber || 0 }) 21 | )(Spin); 22 | -------------------------------------------------------------------------------- /19-1-isomorphic-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "19-1-isomorphic-server", 3 | "version": "1.0.0", 4 | "description": "React Redux example", 5 | "scripts": { 6 | "start": "node index.js" 7 | }, 8 | "dependencies": { 9 | "webpack-isomorphic-tools": "2.5.8" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/lewis617/react-redux-book.git" 14 | }, 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/lewis617/react-redux-book/issues" 18 | }, 19 | "homepage": "https://github.com/lewis617/react-redux-book#readme" 20 | } 21 | -------------------------------------------------------------------------------- /19-2-isomorphic-client/webpack.config.js: -------------------------------------------------------------------------------- 1 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); 2 | 3 | var config = { assets: { images: { extensions: ['png'] } } }; 4 | 5 | var webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(config); 6 | 7 | module.exports = { 8 | context: __dirname, 9 | entry: './index.js', 10 | output: { 11 | path: __dirname + '/dist', 12 | filename: 'main.js' 13 | }, 14 | module: { 15 | loaders: [ 16 | { test: /\.png$/, loader: 'url-loader?limit=10240' } 17 | ] 18 | }, 19 | plugins: [webpackIsomorphicToolsPlugin] 20 | }; 21 | -------------------------------------------------------------------------------- /21-async-router/common/containers/Main.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Link, IndexLink } from 'react-router'; 3 | 4 | function Main(props) { 5 | return ( 6 |
7 |
    8 |
  • Home
  • 9 |
  • Counter
  • 10 |
11 | {/* this will render the child routes */} 12 | {React.cloneElement(props.children, props)} 13 |
14 | ); 15 | } 16 | Main.propTypes = { 17 | children: PropTypes.any.isRequired 18 | }; 19 | 20 | export default Main; 21 | -------------------------------------------------------------------------------- /10-counter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore } from 'redux'; 4 | import Counter from './components/Counter'; 5 | import counter from './reducers'; 6 | 7 | const store = createStore(counter); 8 | const rootEl = document.getElementById('root'); 9 | 10 | function render() { 11 | ReactDOM.render( 12 | store.dispatch({ type: 'INCREMENT' })} 15 | onDecrement={() => store.dispatch({ type: 'DECREMENT' })} 16 | />, 17 | rootEl 18 | ); 19 | } 20 | 21 | render(); 22 | store.subscribe(render); 23 | -------------------------------------------------------------------------------- /12-undo-devtools/actions/index.js: -------------------------------------------------------------------------------- 1 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; 2 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; 3 | export const UNDO_COUNTER = 'UNDO_COUNTER'; 4 | export const REDO_COUNTER = 'REDO_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 undo() { 19 | return { 20 | type: UNDO_COUNTER 21 | }; 22 | } 23 | 24 | export function redo() { 25 | return { 26 | type: REDO_COUNTER 27 | }; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /12-undo-devtools/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, applyMiddleware, compose } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import thunk from 'redux-thunk'; 6 | import Counter from './containers/Counter'; 7 | import counter from './reducers'; 8 | 9 | const store = createStore(counter, compose( 10 | applyMiddleware(thunk), 11 | window.devToolsExtension ? window.devToolsExtension() : f => f 12 | )); 13 | const rootEl = document.getElementById('root'); 14 | 15 | ReactDOM.render( 16 | 17 | 18 | , rootEl); 19 | -------------------------------------------------------------------------------- /20-universal-router/common/containers/Main.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Link, IndexLink } from 'react-router'; 3 | 4 | function Main(props) { 5 | return ( 6 |
7 |
    8 |
  • Home
  • 9 |
  • Counter
  • 10 |
11 | {/* this will render the child routes */} 12 | {React.cloneElement(props.children, props)} 13 |
14 | ); 15 | } 16 | Main.propTypes = { 17 | children: PropTypes.any.isRequired 18 | }; 19 | 20 | export default Main; 21 | -------------------------------------------------------------------------------- /23-26-production/src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: process.env.HOST || 'localhost', 3 | port: process.env.PORT || (process.env.NODE_ENV === 'production' ? 8080 : 3000), 4 | apiHost: process.env.APIHOST || 'localhost', 5 | apiPort: process.env.APIPORT || 3030, 6 | app: { 7 | title: 'React Redux Book', 8 | description: 'React Redux Book Example', 9 | head: { 10 | titleTemplate: 'React Redux Book: %s', 11 | meta: [ 12 | { 13 | name: 'description', 14 | content: 'React Redux Book Example' 15 | }, 16 | { charset: 'utf-8' } 17 | ] 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /14-15-todomvc/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export function addTodo(text) { 4 | return { type: types.ADD_TODO, text }; 5 | } 6 | 7 | export function deleteTodo(id) { 8 | return { type: types.DELETE_TODO, id }; 9 | } 10 | 11 | export function editTodo(id, text) { 12 | return { type: types.EDIT_TODO, id, text }; 13 | } 14 | 15 | export function completeTodo(id) { 16 | return { type: types.COMPLETE_TODO, id }; 17 | } 18 | 19 | export function completeAll() { 20 | return { type: types.COMPLETE_ALL }; 21 | } 22 | 23 | export function clearCompleted() { 24 | return { type: types.CLEAR_COMPLETED }; 25 | } 26 | -------------------------------------------------------------------------------- /13-counter-test/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | 5 | export default function configureStore(initialState) { 6 | const store = createStore( 7 | rootReducer, 8 | initialState, 9 | applyMiddleware(thunk) 10 | ); 11 | 12 | if (module.hot) { 13 | // Enable Webpack hot module replacement for reducers 14 | module.hot.accept('../reducers', () => { 15 | const nextRootReducer = require('../reducers').default; 16 | store.replaceReducer(nextRootReducer); 17 | }); 18 | } 19 | 20 | return store; 21 | } 22 | -------------------------------------------------------------------------------- /18-universal/common/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | 5 | export default function configureStore(initialState) { 6 | const store = createStore( 7 | rootReducer, 8 | initialState, 9 | applyMiddleware(thunk) 10 | ); 11 | 12 | if (module.hot) { 13 | // Enable Webpack hot module replacement for reducers 14 | module.hot.accept('../reducers', () => { 15 | const nextRootReducer = require('../reducers').default; 16 | store.replaceReducer(nextRootReducer); 17 | }); 18 | } 19 | 20 | return store; 21 | } 22 | -------------------------------------------------------------------------------- /20-universal-router/common/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | 5 | export default function configureStore(initialState) { 6 | const store = createStore( 7 | rootReducer, 8 | initialState, 9 | applyMiddleware(thunk) 10 | ); 11 | 12 | if (module.hot) { 13 | // Enable Webpack hot module replacement for reducers 14 | module.hot.accept('../reducers', () => { 15 | const nextRootReducer = require('../reducers').default; 16 | store.replaceReducer(nextRootReducer); 17 | }); 18 | } 19 | 20 | return store; 21 | } 22 | -------------------------------------------------------------------------------- /19-3-isomophic-counter/common/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | 5 | export default function configureStore(initialState) { 6 | const store = createStore( 7 | rootReducer, 8 | initialState, 9 | applyMiddleware(thunk) 10 | ); 11 | 12 | if (module.hot) { 13 | // Enable Webpack hot module replacement for reducers 14 | module.hot.accept('../reducers', () => { 15 | const nextRootReducer = require('../reducers').default; 16 | store.replaceReducer(nextRootReducer); 17 | }); 18 | } 19 | 20 | return store; 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-book", 3 | "version": "1.0.0", 4 | "description": "React Redux example", 5 | "directories": { 6 | "doc": "doc", 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "lint": "eslint *-*/" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git@git.coding.net:lewis617/react-redux-book.git" 15 | }, 16 | "license": "MIT", 17 | "devDependencies": { 18 | "babel-eslint": "^6.0.4", 19 | "eslint": "^2.10.2", 20 | "eslint-config-airbnb": "^9.0.1", 21 | "eslint-plugin-import": "^1.8.0", 22 | "eslint-plugin-jsx-a11y": "^1.2.2", 23 | "eslint-plugin-react": "^5.1.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /08-start-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "08-start-redux", 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-register": "^6.8.0" 12 | }, 13 | "dependencies": { 14 | "redux": "^3.5.2" 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 | } 25 | -------------------------------------------------------------------------------- /23-26-production/src/api/api.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import session from 'express-session'; 4 | import config from '../config'; 5 | import controllers from './controllers'; 6 | 7 | const port = config.apiPort; 8 | const app = new Express(); 9 | 10 | app.use(bodyParser.json()); 11 | 12 | app.use(session({ 13 | secret: 'react redux book !!!!', 14 | resave: false, 15 | saveUninitialized: false, 16 | cookie: { maxAge: 60000 } 17 | })); 18 | 19 | controllers(app); 20 | 21 | app.listen(port, (error) => { 22 | if (error) { 23 | console.error(error); 24 | } else { 25 | console.info('==> 🌎 API Listening on port %s. ', port); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /09-redux-thunk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "09-redux-thunk", 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-register": "^6.8.0" 12 | }, 13 | "dependencies": { 14 | "redux": "^3.5.2", 15 | "redux-thunk": "^2.1.0" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/lewis617/react-redux-book.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/lewis617/react-redux-book/issues" 23 | }, 24 | "homepage": "https://github.com/lewis617/react-redux-book#readme" 25 | } 26 | -------------------------------------------------------------------------------- /05-jsx/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.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 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /16-async/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.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 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /04-dev-server/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.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 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /10-counter/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.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 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /17-real-world/components/User.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | function User({ user }) { 5 | const { login, avatarUrl, name } = user; 6 | 7 | return ( 8 |
9 | 10 | avatar 11 |

12 | {login} {name && ({name})} 13 |

14 | 15 |
16 | ); 17 | } 18 | 19 | User.propTypes = { 20 | user: PropTypes.shape({ 21 | login: PropTypes.string.isRequired, 22 | avatarUrl: PropTypes.string.isRequired, 23 | name: PropTypes.string, 24 | }).isRequired, 25 | }; 26 | 27 | export default User; 28 | -------------------------------------------------------------------------------- /17-real-world/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 | } 29 | }; 30 | -------------------------------------------------------------------------------- /11-counter-connect/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.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 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /12-undo-devtools/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.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 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /13-counter-test/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.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 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /06-state-props-context/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.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 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /07-element-instance/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.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 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /16-async/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | function Picker({ value, onChange, options }) { 4 | return ( 5 | 6 |

{value}

7 | 17 |
18 | ); 19 | } 20 | 21 | Picker.propTypes = { 22 | options: PropTypes.arrayOf( 23 | PropTypes.string.isRequired 24 | ).isRequired, 25 | value: PropTypes.string.isRequired, 26 | onChange: PropTypes.func.isRequired, 27 | }; 28 | 29 | export default Picker; 30 | -------------------------------------------------------------------------------- /20-universal-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 } 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(); 14 | const history = browserHistory; 15 | 16 | render( 17 | 18 | 19 | {routes} 20 | 21 | , 22 | rootElement 23 | ); 24 | -------------------------------------------------------------------------------- /21-async-router/common/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | import Counter from './containers/Counter'; 4 | import Main from './containers/Main'; 5 | import Home from './containers/Home'; 6 | import { load } from './actions'; 7 | 8 | export default store => { 9 | const preload = (nextState, replace, cb) => { 10 | if (__SERVER__ || nextState.location.action === 'PUSH') { 11 | store.dispatch(load()).then(() => cb()); 12 | } else { 13 | cb(); 14 | } 15 | }; 16 | return ( 17 | 18 | 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 |
19 |

todos

20 | 25 |
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 | Counter 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 |
24 | 36 |
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 |
      42 | {children} 43 |
    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 |
      48 | {children} 49 |
    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 | [![Build Status](https://travis-ci.org/lewis617/react-redux-book.svg?branch=master)](https://travis-ci.org/lewis617/react-redux-book) 4 | 5 | 6 | ![](https://raw.githubusercontent.com/lewis617/react-redux-book/master/front-cover.jpg) 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 | 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 |
    34 |
    35 | 40 |
    41 | 44 | 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 | --------------------------------------------------------------------------------