├── .gitignore ├── README.md ├── input-redux ├── README.MD ├── dist │ ├── app.js │ └── vendors.js ├── index.html ├── package.json ├── src │ ├── App.js │ ├── actions.js │ ├── index.js │ └── reducers.js └── webpack.config.js ├── redux-examples ├── async │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── components │ │ ├── Picker.js │ │ └── Posts.js │ ├── containers │ │ └── App.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ └── index.js │ ├── server.js │ ├── store │ │ └── configureStore.js │ └── webpack.config.js ├── counter │ ├── .babelrc │ ├── actions │ │ └── counter.js │ ├── components │ │ └── Counter.js │ ├── containers │ │ └── App.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ ├── counter.js │ │ └── index.js │ ├── server.js │ ├── store │ │ └── configureStore.js │ ├── test │ │ ├── .eslintrc │ │ ├── actions │ │ │ └── counter.spec.js │ │ ├── components │ │ │ └── Counter.spec.js │ │ ├── containers │ │ │ └── App.spec.js │ │ ├── reducers │ │ │ └── counter.spec.js │ │ └── setup.js │ └── webpack.config.js ├── real-world │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── components │ │ ├── Explore.js │ │ ├── List.js │ │ ├── Repo.js │ │ └── User.js │ ├── containers │ │ ├── App.js │ │ ├── DevTools.js │ │ ├── RepoPage.js │ │ ├── Root.dev.js │ │ ├── Root.js │ │ ├── Root.prod.js │ │ └── UserPage.js │ ├── index.html │ ├── index.js │ ├── middleware │ │ └── api.js │ ├── package.json │ ├── reducers │ │ ├── index.js │ │ └── paginate.js │ ├── routes.js │ ├── server.js │ ├── store │ │ ├── configureStore.dev.js │ │ ├── configureStore.js │ │ └── configureStore.prod.js │ └── webpack.config.js ├── shopping-cart │ ├── actions │ │ └── index.js │ ├── api │ │ ├── products.json │ │ └── shop.js │ ├── components │ │ ├── Cart.js │ │ ├── Product.js │ │ ├── ProductItem.js │ │ └── ProductsList.js │ ├── constants │ │ └── ActionTypes.js │ ├── containers │ │ ├── App.js │ │ ├── CartContainer.js │ │ └── ProductsContainer.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ ├── cart.js │ │ ├── index.js │ │ └── products.js │ ├── server.js │ └── webpack.config.js ├── todomvc │ ├── .babelrc │ ├── actions │ │ └── todos.js │ ├── components │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── MainSection.js │ │ ├── TodoItem.js │ │ └── TodoTextInput.js │ ├── constants │ │ ├── ActionTypes.js │ │ └── TodoFilters.js │ ├── containers │ │ └── App.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ ├── index.js │ │ └── todos.js │ ├── server.js │ ├── store │ │ └── configureStore.js │ ├── test │ │ ├── .eslintrc │ │ ├── actions │ │ │ └── todos.spec.js │ │ ├── components │ │ │ ├── Footer.spec.js │ │ │ ├── Header.spec.js │ │ │ ├── MainSection.spec.js │ │ │ ├── TodoItem.spec.js │ │ │ └── TodoTextInput.spec.js │ │ ├── reducers │ │ │ └── todos.spec.js │ │ └── setup.js │ └── webpack.config.js ├── todos-with-undo │ ├── .babelrc │ ├── actions.js │ ├── components │ │ ├── AddTodo.js │ │ ├── Footer.js │ │ ├── Todo.js │ │ └── TodoList.js │ ├── containers │ │ └── App.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers.js │ ├── server.js │ └── webpack.config.js └── universal │ ├── client │ └── index.js │ ├── common │ ├── actions │ │ └── counter.js │ ├── api │ │ └── counter.js │ ├── components │ │ └── Counter.js │ ├── containers │ │ └── App.js │ ├── reducers │ │ ├── counter.js │ │ └── index.js │ └── store │ │ └── configureStore.js │ ├── index.js │ ├── package.json │ ├── server │ ├── index.js │ └── server.js │ └── webpack.config.js ├── redux-undo-boilerplate ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.html ├── package.json ├── server.js ├── src │ ├── actions │ │ └── counter.js │ ├── components │ │ ├── Counter.js │ │ └── Main.js │ ├── containers │ │ ├── App.js │ │ ├── CounterPage.js │ │ ├── DevTools.js │ │ ├── Root.js │ │ └── index.js │ ├── index.js │ ├── reducers │ │ ├── counter.js │ │ └── index.js │ ├── routes.js │ ├── store │ │ └── configureStore.js │ └── utils │ │ └── .gitkeep ├── test │ ├── actions │ │ └── counter.spec.js │ ├── components │ │ └── Counter.spec.js │ ├── containers │ │ └── CounterPage.spec.js │ └── reducers │ │ └── counter.spec.js └── webpack.config.js ├── redux-wilddog-todos ├── README.MD ├── dist │ ├── app.js │ └── vendors.js ├── index.html ├── package.json ├── src │ ├── actions.js │ ├── components │ │ ├── AddTodo.js │ │ ├── Todo.js │ │ └── TodoList.js │ ├── containers │ │ └── App.js │ ├── index.js │ ├── reducers.js │ └── store.js └── webpack.config.js └── todo-reflux ├── README.MD ├── dist ├── app.js └── vendors.js ├── index.html ├── package.json ├── src ├── actions │ └── actions.js ├── components │ └── todo.js ├── index.js └── stores │ └── store.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 3 | 4 | *.iml 5 | 6 | ## Directory-based project format: 7 | .idea/ 8 | # if you remove the above rule, at least ignore the following: 9 | 10 | # User-specific stuff: 11 | # .idea/workspace.xml 12 | # .idea/tasks.xml 13 | # .idea/dictionaries 14 | 15 | # Sensitive or high-churn files: 16 | # .idea/dataSources.ids 17 | # .idea/dataSources.xml 18 | # .idea/sqlDataSources.xml 19 | # .idea/dynamic.xml 20 | # .idea/uiDesigner.xml 21 | 22 | # Gradle: 23 | # .idea/gradle.xml 24 | # .idea/libraries 25 | 26 | # Mongo Explorer plugin: 27 | # .idea/mongoSettings.xml 28 | 29 | ## File-based project format: 30 | *.ipr 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | /out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Crashlytics plugin (for Android Studio and IntelliJ) 45 | com_crashlytics_export_strings.xml 46 | crashlytics.properties 47 | crashlytics-build.properties 48 | 49 | ### Node template 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | 55 | # Runtime data 56 | pids 57 | *.pid 58 | *.seed 59 | 60 | # Directory for instrumented libs generated by jscoverage/JSCover 61 | lib-cov 62 | 63 | # Coverage directory used by tools like istanbul 64 | coverage 65 | 66 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 67 | .grunt 68 | 69 | # node-waf configuration 70 | .lock-wscript 71 | 72 | # Compiled binary addons (http://nodejs.org/api/addons.html) 73 | build/Release 74 | 75 | # Dependency directory 76 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 77 | node_modules 78 | 79 | # Created by .ignore support plugin (hsz.mobi) 80 | 81 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React+Redux系列教程 2 | [react+redux教程(一)connect、applyMiddleware、thunk、webpackHotMiddleware](http://www.cnblogs.com/lewis617/p/5145073.html) 3 | 4 | [react+redux教程(二)redux的单一状态树完全替代了react的状态机?](http://www.cnblogs.com/lewis617/p/5147445.html) 5 | 6 | [react+redux教程(三)reduce()、filter()、map()、some()、every()、...展开属性](http://www.cnblogs.com/lewis617/p/5149006.html) 7 | 8 | [react+redux教程(四)undo、devtools、router](http://www.cnblogs.com/lewis617/p/5161003.html) 9 | 10 | [react+redux教程(五)异步、单一state树结构、componentWillReceiveProps](http://www.cnblogs.com/lewis617/p/5170835.html) 11 | 12 | [react+redux教程(六)redux服务端渲染流程](http://www.cnblogs.com/lewis617/p/5174861.html) 13 | 14 | [react+redux教程(七)自定义redux中间件](http://www.cnblogs.com/lewis617/p/5177852.html) 15 | 16 | [react+redux教程(八)连接数据库的redux程序](http://www.cnblogs.com/lewis617/p/5180097.html) 17 | 18 | ## React+Reflux教程 19 | [react+reflux入门教程](http://www.cnblogs.com/lewis617/p/5129609.html) 20 | 21 | ## Angular2教程 22 | [Angular2教程](https://github.com/lewis617/angular2-tutorial) 23 | 24 | *如果您觉得本程序或者博客帮到了您,就赏颗星吧!* 25 | -------------------------------------------------------------------------------- /input-redux/README.MD: -------------------------------------------------------------------------------- 1 | ###运行方法 2 | npm install 3 | npm run build 4 | 手动打开index.html 5 | 6 | *如果您觉得本程序或者博客帮到了您,就赏颗星吧!* -------------------------------------------------------------------------------- /input-redux/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | input-redux 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /input-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-redux", 3 | "version": "1.0.0", 4 | "description": "", 5 | "dependencies": {}, 6 | "devDependencies": { 7 | "babel-core": "^6.4.0", 8 | "babel-loader": "^6.2.1", 9 | "babel-preset-es2015": "^6.3.13", 10 | "babel-preset-react": "^6.3.13", 11 | "path": "^0.12.7", 12 | "react": "^0.14.6", 13 | "react-dom": "^0.14.6", 14 | "react-redux": "^4.0.6", 15 | "redux": "^3.0.5", 16 | "webpack": "^1.12.10" 17 | }, 18 | "scripts": { 19 | "build": "webpack --progress -colors --watch" 20 | }, 21 | "author": "lewis617", 22 | "license": "ISC" 23 | } 24 | -------------------------------------------------------------------------------- /input-redux/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { findDOMNode, Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { connect } from 'react-redux'; 4 | import * as action from './actions' 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 | {this.props.propsValue} 12 |
13 | ); 14 | } 15 | changeHandle(){ 16 | const node = ReactDOM.findDOMNode(this.refs.input); 17 | const value = node.value.trim(); 18 | this.props.change(value); 19 | } 20 | } 21 | 22 | function mapStateToProps(state) { 23 | return { 24 | propsValue: state.value 25 | } 26 | } 27 | 28 | //将state的指定值映射在props上,将action的所有方法映射在props上 29 | export default connect(mapStateToProps,action)(App); -------------------------------------------------------------------------------- /input-redux/src/actions.js: -------------------------------------------------------------------------------- 1 | //定义一个change方法,将来把它绑定到props上 2 | export function change(value){ 3 | return{ 4 | type:"change", 5 | value:value 6 | } 7 | } -------------------------------------------------------------------------------- /input-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import App from './App' 6 | import inputApp from './reducers' 7 | 8 | let store = createStore(inputApp); 9 | 10 | render( 11 | 12 | 13 | , 14 | document.querySelector("#app") 15 | ); -------------------------------------------------------------------------------- /input-redux/src/reducers.js: -------------------------------------------------------------------------------- 1 | //reducer就是个function,名字随便你起,功能就是在action触发后,返回一个新的state(就是个对象) 2 | export default function change(state,action){ 3 | if(action.type=="change")return{value:action.value}; 4 | return {value:'default'}; 5 | } -------------------------------------------------------------------------------- /input-redux/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: { 6 | app:path.join(__dirname, 'src'), 7 | vendors: ['react','redux'] 8 | }, 9 | output: { 10 | path: path.join(__dirname, 'dist'), 11 | filename: '[name].js' 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test:/\.js?$/, 17 | exclude:/node_modules/, 18 | loader:'babel', 19 | query:{ 20 | presets:['react','es2015'] 21 | } 22 | } 23 | ] 24 | }, 25 | plugins: [ 26 | new webpack.optimize.CommonsChunkPlugin('vendors', 'vendors.js') 27 | ] 28 | }; 29 | -------------------------------------------------------------------------------- /redux-examples/async/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 2, 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | "react-transform" 7 | ], 8 | "extra": { 9 | "react-transform": { 10 | "transforms": [{ 11 | "transform": "react-transform-hmr", 12 | "imports": ["react"], 13 | "locals": ["module"] 14 | }] 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /redux-examples/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 | //选择新闻类型action 8 | export function selectReddit(reddit) { 9 | return { 10 | type: SELECT_REDDIT, 11 | reddit 12 | } 13 | } 14 | //废弃新闻类型action 15 | export function invalidateReddit(reddit) { 16 | return { 17 | type: INVALIDATE_REDDIT, 18 | reddit 19 | } 20 | } 21 | //开始获取新闻action 22 | function requestPosts(reddit) { 23 | return { 24 | type: REQUEST_POSTS, 25 | reddit 26 | } 27 | } 28 | //获取新闻成功的action 29 | function receivePosts(reddit, json) { 30 | return { 31 | type: RECEIVE_POSTS, 32 | reddit: reddit, 33 | posts: json.data.children.map(child => child.data), 34 | receivedAt: Date.now() 35 | } 36 | } 37 | 38 | //获取文章,先触发requestPosts开始获取action,完成后触发receivePosts获取成功的action 39 | function fetchPosts(reddit) { 40 | return dispatch => { 41 | dispatch(requestPosts(reddit)) 42 | return fetch(`https://www.reddit.com/r/${reddit}.json`) 43 | .then(response => response.json()) 44 | .then(json => dispatch(receivePosts(reddit, json))) 45 | } 46 | } 47 | 48 | //是否需要获取文章 49 | function shouldFetchPosts(state, reddit) { 50 | const posts = state.postsByReddit[reddit] 51 | if (!posts) { 52 | return true 53 | } 54 | if (posts.isFetching) { 55 | return false 56 | } 57 | return posts.didInvalidate 58 | } 59 | 60 | //如果需要则开始获取文章 61 | export function fetchPostsIfNeeded(reddit) { 62 | return (dispatch, getState) => { 63 | if (shouldFetchPosts(getState(), reddit)) { 64 | return dispatch(fetchPosts(reddit)) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /redux-examples/async/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class Picker extends Component { 4 | render() { 5 | const { value, onChange, options } = this.props 6 | 7 | return ( 8 | 9 |

{value}

10 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | Picker.propTypes = { 24 | options: PropTypes.arrayOf( 25 | PropTypes.string.isRequired 26 | ).isRequired, 27 | value: PropTypes.string.isRequired, 28 | onChange: PropTypes.func.isRequired 29 | } 30 | -------------------------------------------------------------------------------- /redux-examples/async/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | 3 | export default class Posts extends Component { 4 | render() { 5 | return ( 6 | 11 | ) 12 | } 13 | } 14 | 15 | Posts.propTypes = { 16 | posts: PropTypes.array.isRequired 17 | } 18 | -------------------------------------------------------------------------------- /redux-examples/async/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { selectReddit, fetchPostsIfNeeded, invalidateReddit } from '../actions' 4 | import Picker from '../components/Picker' 5 | import Posts from '../components/Posts' 6 | 7 | class App extends Component { 8 | constructor(props) { 9 | super(props) 10 | this.handleChange = this.handleChange.bind(this) 11 | this.handleRefreshClick = this.handleRefreshClick.bind(this) 12 | } 13 | 14 | //初始化渲染后触发 15 | componentDidMount() { 16 | console.log('执行componentDidMount'); 17 | const { dispatch, selectedReddit } = this.props 18 | dispatch(fetchPostsIfNeeded(selectedReddit)) 19 | } 20 | 21 | //每次接受新的props触发 22 | componentWillReceiveProps(nextProps) { 23 | console.log('执行componentWillReceiveProps',nextProps); 24 | if (nextProps.selectedReddit !== this.props.selectedReddit) { 25 | const { dispatch, selectedReddit } = nextProps 26 | dispatch(fetchPostsIfNeeded(selectedReddit)) 27 | } 28 | } 29 | 30 | handleChange(nextReddit) { 31 | this.props.dispatch(selectReddit(nextReddit)) 32 | } 33 | 34 | handleRefreshClick(e) { 35 | e.preventDefault() 36 | 37 | const { dispatch, selectedReddit } = this.props 38 | dispatch(invalidateReddit(selectedReddit)) 39 | dispatch(fetchPostsIfNeeded(selectedReddit)) 40 | } 41 | 42 | render() { 43 | const { selectedReddit, posts, isFetching, lastUpdated } = this.props 44 | return ( 45 |
46 | 49 |

50 | {lastUpdated && 51 | 52 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}. 53 | {' '} 54 | 55 | } 56 | {!isFetching && 57 | 59 | Refresh 60 | 61 | } 62 |

63 | {isFetching && posts.length === 0 && 64 |

Loading...

65 | } 66 | {!isFetching && posts.length === 0 && 67 |

Empty.

68 | } 69 | {posts.length > 0 && 70 |
71 | 72 |
73 | } 74 |
75 | ) 76 | } 77 | } 78 | 79 | App.propTypes = { 80 | selectedReddit: PropTypes.string.isRequired, 81 | posts: PropTypes.array.isRequired, 82 | isFetching: PropTypes.bool.isRequired, 83 | lastUpdated: PropTypes.number, 84 | dispatch: PropTypes.func.isRequired 85 | } 86 | 87 | function mapStateToProps(state) { 88 | const { selectedReddit, postsByReddit } = state 89 | const { 90 | isFetching, 91 | lastUpdated, 92 | items: posts 93 | } = postsByReddit[selectedReddit] || { 94 | isFetching: true, 95 | items: [] 96 | } 97 | 98 | return { 99 | selectedReddit, 100 | posts, 101 | isFetching, 102 | lastUpdated 103 | } 104 | } 105 | 106 | export default connect(mapStateToProps)(App) 107 | -------------------------------------------------------------------------------- /redux-examples/async/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux async example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /redux-examples/async/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-core/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 | -------------------------------------------------------------------------------- /redux-examples/async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-async-example", 3 | "version": "0.0.0", 4 | "description": "Redux async example", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/rackt/redux.git" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "reactjs", 15 | "hot", 16 | "reload", 17 | "hmr", 18 | "live", 19 | "edit", 20 | "webpack", 21 | "flux" 22 | ], 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/rackt/redux/issues" 26 | }, 27 | "homepage": "http://rackt.github.io/redux", 28 | "dependencies": { 29 | "isomorphic-fetch": "^2.1.1", 30 | "react": "^0.14.0", 31 | "react-dom": "^0.14.0", 32 | "react-redux": "^4.0.0", 33 | "redux": "^3.0.0", 34 | "redux-logger": "^2.0.2", 35 | "redux-thunk": "^0.1.0" 36 | }, 37 | "devDependencies": { 38 | "babel-core": "^5.6.18", 39 | "babel-loader": "^5.1.4", 40 | "babel-plugin-react-transform": "^1.1.0", 41 | "expect": "^1.6.0", 42 | "express": "^4.13.3", 43 | "node-libs-browser": "^0.5.2", 44 | "react-transform-hmr": "^1.0.0", 45 | "webpack": "^1.9.11", 46 | "webpack-dev-middleware": "^1.2.0", 47 | "webpack-hot-middleware": "^2.2.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /redux-examples/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 | //选择新闻后,将state.selectedReddit设为所选选项 8 | function selectedReddit(state = 'reactjs', action) { 9 | switch (action.type) { 10 | case SELECT_REDDIT: 11 | return action.reddit 12 | default: 13 | return state 14 | } 15 | } 16 | 17 | function posts(state = { 18 | //是否正在获取最新 19 | isFetching: false, 20 | //是否废弃 21 | didInvalidate: false, 22 | //内容 23 | items: [] 24 | }, action) { 25 | switch (action.type) { 26 | case INVALIDATE_REDDIT: 27 | return Object.assign({}, state, { 28 | didInvalidate: true 29 | }) 30 | case REQUEST_POSTS: 31 | return Object.assign({}, state, { 32 | isFetching: true, 33 | didInvalidate: false 34 | }) 35 | case RECEIVE_POSTS: 36 | return Object.assign({}, state, { 37 | isFetching: false, 38 | didInvalidate: false, 39 | items: action.posts, 40 | lastUpdated: action.receivedAt 41 | }) 42 | default: 43 | return state 44 | } 45 | } 46 | //废弃、接收到、开始接受新闻后,将state.postsByReddit设为相关参数 47 | function postsByReddit(state = { }, action) { 48 | switch (action.type) { 49 | case INVALIDATE_REDDIT: 50 | case RECEIVE_POSTS: 51 | case REQUEST_POSTS: 52 | return Object.assign({}, state, { 53 | [action.reddit]: posts(state[action.reddit], action) 54 | }) 55 | default: 56 | return state 57 | } 58 | } 59 | //将两个reducer合并成一个reducer,也就将全局的state加上postsByReddit,selectedReddit两个属性,每个属性都有自己的state 60 | const rootReducer = combineReducers({ 61 | postsByReddit, 62 | selectedReddit 63 | }) 64 | 65 | export default rootReducer 66 | -------------------------------------------------------------------------------- /redux-examples/async/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /redux-examples/async/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunkMiddleware from 'redux-thunk' 3 | import createLogger from 'redux-logger' 4 | import rootReducer from '../reducers' 5 | 6 | const createStoreWithMiddleware = applyMiddleware( 7 | thunkMiddleware, 8 | createLogger() 9 | )(createStore) 10 | 11 | export default function configureStore(initialState) { 12 | const store = createStoreWithMiddleware(rootReducer, initialState) 13 | 14 | if (module.hot) { 15 | // Enable Webpack hot module replacement for reducers 16 | module.hot.accept('../reducers', () => { 17 | const nextRootReducer = require('../reducers') 18 | store.replaceReducer(nextRootReducer) 19 | }) 20 | } 21 | 22 | return store 23 | } 24 | -------------------------------------------------------------------------------- /redux-examples/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' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [{ 22 | test: /\.js$/, 23 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | }] 27 | } 28 | } 29 | 30 | 31 | // When inside Redux repo, prefer src to compiled version. 32 | // You can safely delete these lines in your project. 33 | var reduxSrc = path.join(__dirname, '..', '..', 'src') 34 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules') 35 | var fs = require('fs') 36 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) { 37 | // Resolve Redux to source 38 | module.exports.resolve = { alias: { 'redux': reduxSrc } } 39 | // Compile Redux from source 40 | module.exports.module.loaders.push({ 41 | test: /\.js$/, 42 | loaders: ['babel'], 43 | include: reduxSrc 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /redux-examples/counter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 2, 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | "react-transform" 7 | ], 8 | "extra": { 9 | "react-transform": { 10 | "transforms": [{ 11 | "transform": "react-transform-hmr", 12 | "imports": ["react"], 13 | "locals": ["module"] 14 | }] 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /redux-examples/counter/actions/counter.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 | //导出奇数加一的方法,该方法返回一个方法,包含dispatch和getState两个参数,dispatch用于执行action的方法,getState返回state 16 | export function incrementIfOdd() { 17 | return (dispatch, getState) => { 18 | //获取state对象中的counter属性值 19 | const { counter } = getState() 20 | 21 | //偶数则返回 22 | if (counter % 2 === 0) { 23 | return 24 | } 25 | //没有返回就执行加一 26 | dispatch(increment()) 27 | } 28 | } 29 | //导出一个方法,包含一个默认参数delay,返回一个方法,一秒后加一 30 | export function incrementAsync(delay = 1000) { 31 | return dispatch => { 32 | setTimeout(() => { 33 | dispatch(increment()) 34 | }, delay) 35 | } 36 | } 37 | 38 | //这些方法都导出,在其他文件导入时候,使用import * as actions 就可以生成一个actions对象包含所有的export -------------------------------------------------------------------------------- /redux-examples/counter/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | class Counter extends Component { 4 | render() { 5 | //从组件的props属性中导入四个方法和一个变量 6 | const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props; 7 | //渲染组件,包括一个数字,四个按钮 8 | return ( 9 |

10 | Clicked: {counter} times 11 | {' '} 12 | 13 | {' '} 14 | 15 | {' '} 16 | 17 | {' '} 18 | 19 |

20 | ) 21 | } 22 | } 23 | //限制组件的props安全 24 | Counter.propTypes = { 25 | //increment必须为fucntion,且必须存在 26 | increment: PropTypes.func.isRequired, 27 | incrementIfOdd: PropTypes.func.isRequired, 28 | incrementAsync: PropTypes.func.isRequired, 29 | decrement: PropTypes.func.isRequired, 30 | //counter必须为数字,且必须存在 31 | counter: PropTypes.number.isRequired 32 | }; 33 | 34 | export default Counter 35 | -------------------------------------------------------------------------------- /redux-examples/counter/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/counter' 5 | 6 | //将state.counter绑定到props的counter 7 | function mapStateToProps(state) { 8 | return { 9 | counter: state.counter 10 | } 11 | } 12 | //将action的所有方法绑定到props上 13 | function mapDispatchToProps(dispatch) { 14 | return bindActionCreators(CounterActions, dispatch) 15 | } 16 | 17 | //通过react-redux提供的connect方法将我们需要的state中的数据和actions中的方法绑定到props上 18 | export default connect(mapStateToProps, mapDispatchToProps)(Counter) 19 | -------------------------------------------------------------------------------- /redux-examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux counter example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /redux-examples/counter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } 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 | 9 | render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ) 15 | -------------------------------------------------------------------------------- /redux-examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-counter-example", 3 | "version": "0.0.0", 4 | "description": "Redux counter example", 5 | "scripts": { 6 | "start": "node server.js", 7 | "test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js", 8 | "test:watch": "npm test -- --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rackt/redux.git" 13 | }, 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/rackt/redux/issues" 17 | }, 18 | "homepage": "http://rackt.github.io/redux", 19 | "dependencies": { 20 | "react": "^0.14.0", 21 | "react-dom": "^0.14.0", 22 | "react-redux": "^4.0.0", 23 | "redux": "^3.0.0", 24 | "redux-thunk": "^0.1.0" 25 | }, 26 | "devDependencies": { 27 | "babel-core": "^5.6.18", 28 | "babel-loader": "^5.1.4", 29 | "babel-plugin-react-transform": "^1.1.0", 30 | "expect": "^1.6.0", 31 | "express": "^4.13.3", 32 | "jsdom": "^5.6.1", 33 | "mocha": "^2.2.5", 34 | "node-libs-browser": "^0.5.2", 35 | "react-addons-test-utils": "^0.14.0", 36 | "react-transform-hmr": "^1.0.0", 37 | "webpack": "^1.9.11", 38 | "webpack-dev-middleware": "^1.2.0", 39 | "webpack-hot-middleware": "^2.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /redux-examples/counter/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter' 2 | 3 | //reducer其实也是个方法而已,参数是state和action,返回值是新的state 4 | export default function counter(state = 0, action) { 5 | switch (action.type) { 6 | case INCREMENT_COUNTER: 7 | return state + 1 8 | case DECREMENT_COUNTER: 9 | return state - 1 10 | default: 11 | return state 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /redux-examples/counter/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import counter from './counter' 3 | 4 | //使用redux的combineReducers方法将所有reducer打包起来 5 | const rootReducer = combineReducers({ 6 | counter 7 | }) 8 | 9 | export default rootReducer 10 | -------------------------------------------------------------------------------- /redux-examples/counter/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /redux-examples/counter/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware,compose } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import reducer from '../reducers' 4 | 5 | //applyMiddleware来自redux可以包装 store 的 dispatch 6 | //thunk作用是使被 dispatch 的 function 会接收 dispatch 作为参数,并且可以异步调用它 7 | const createStoreWithMiddleware = compose( 8 | applyMiddleware( 9 | thunk 10 | ), 11 | window.devToolsExtension ? window.devToolsExtension() : f => f 12 | )(createStore) 13 | 14 | export default function configureStore(initialState) { 15 | const store = createStoreWithMiddleware(reducer, initialState) 16 | 17 | //热替换选项 18 | if (module.hot) { 19 | // Enable Webpack hot module replacement for reducers 20 | module.hot.accept('../reducers', () => { 21 | const nextReducer = require('../reducers') 22 | store.replaceReducer(nextReducer) 23 | }) 24 | } 25 | 26 | return store 27 | } 28 | -------------------------------------------------------------------------------- /redux-examples/counter/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /redux-examples/counter/test/actions/counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { applyMiddleware } from 'redux' 3 | import thunk from 'redux-thunk' 4 | import * as actions from '../../actions/counter' 5 | 6 | const middlewares = [ thunk ] 7 | 8 | /* 9 | * Creates a mock of Redux store with middleware. 10 | */ 11 | function mockStore(getState, expectedActions, onLastAction) { 12 | if (!Array.isArray(expectedActions)) { 13 | throw new Error('expectedActions should be an array of expected actions.') 14 | } 15 | if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') { 16 | throw new Error('onLastAction should either be undefined or function.') 17 | } 18 | 19 | function mockStoreWithoutMiddleware() { 20 | return { 21 | getState() { 22 | return typeof getState === 'function' ? 23 | getState() : 24 | getState 25 | }, 26 | 27 | dispatch(action) { 28 | const expectedAction = expectedActions.shift() 29 | expect(action).toEqual(expectedAction) 30 | if (onLastAction && !expectedActions.length) { 31 | onLastAction() 32 | } 33 | return action 34 | } 35 | } 36 | } 37 | 38 | const mockStoreWithMiddleware = applyMiddleware( 39 | ...middlewares 40 | )(mockStoreWithoutMiddleware) 41 | 42 | return mockStoreWithMiddleware() 43 | } 44 | 45 | describe('actions', () => { 46 | it('increment should create increment action', () => { 47 | expect(actions.increment()).toEqual({ type: actions.INCREMENT_COUNTER }) 48 | }) 49 | 50 | it('decrement should create decrement action', () => { 51 | expect(actions.decrement()).toEqual({ type: actions.DECREMENT_COUNTER }) 52 | }) 53 | 54 | it('incrementIfOdd should create increment action', (done) => { 55 | const expectedActions = [ 56 | { type: actions.INCREMENT_COUNTER } 57 | ] 58 | const store = mockStore({ counter: 1 }, expectedActions, done) 59 | store.dispatch(actions.incrementIfOdd()) 60 | }) 61 | 62 | it('incrementIfOdd shouldnt create increment action if counter is even', (done) => { 63 | const expectedActions = [] 64 | const store = mockStore({ counter: 2 }, expectedActions) 65 | store.dispatch(actions.incrementIfOdd()) 66 | done() 67 | }) 68 | 69 | it('incrementAsync should create increment action', (done) => { 70 | const expectedActions = [ 71 | { type: actions.INCREMENT_COUNTER } 72 | ] 73 | const store = mockStore({ counter: 0 }, expectedActions, done) 74 | store.dispatch(actions.incrementAsync(100)) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /redux-examples/counter/test/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import TestUtils from 'react-addons-test-utils' 4 | import Counter from '../../components/Counter' 5 | 6 | function setup() { 7 | const actions = { 8 | increment: expect.createSpy(), 9 | incrementIfOdd: expect.createSpy(), 10 | incrementAsync: expect.createSpy(), 11 | decrement: expect.createSpy() 12 | } 13 | const component = TestUtils.renderIntoDocument() 14 | return { 15 | component: component, 16 | actions: actions, 17 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'), 18 | p: TestUtils.findRenderedDOMComponentWithTag(component, 'p') 19 | } 20 | } 21 | 22 | describe('Counter component', () => { 23 | it('should display count', () => { 24 | const { p } = setup() 25 | expect(p.textContent).toMatch(/^Clicked: 1 times/) 26 | }) 27 | 28 | it('first button should call increment', () => { 29 | const { buttons, actions } = setup() 30 | TestUtils.Simulate.click(buttons[0]) 31 | expect(actions.increment).toHaveBeenCalled() 32 | }) 33 | 34 | it('second button should call decrement', () => { 35 | const { buttons, actions } = setup() 36 | TestUtils.Simulate.click(buttons[1]) 37 | expect(actions.decrement).toHaveBeenCalled() 38 | }) 39 | 40 | it('third button should call incrementIfOdd', () => { 41 | const { buttons, actions } = setup() 42 | TestUtils.Simulate.click(buttons[2]) 43 | expect(actions.incrementIfOdd).toHaveBeenCalled() 44 | }) 45 | 46 | it('fourth button should call incrementAsync', () => { 47 | const { buttons, actions } = setup() 48 | TestUtils.Simulate.click(buttons[3]) 49 | expect(actions.incrementAsync).toHaveBeenCalled() 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /redux-examples/counter/test/containers/App.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import TestUtils from 'react-addons-test-utils' 4 | import { Provider } from 'react-redux' 5 | import App from '../../containers/App' 6 | import configureStore from '../../store/configureStore' 7 | 8 | function setup(initialState) { 9 | const store = configureStore(initialState) 10 | const app = TestUtils.renderIntoDocument( 11 | 12 | 13 | 14 | ) 15 | return { 16 | app: app, 17 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button'), 18 | p: TestUtils.findRenderedDOMComponentWithTag(app, 'p') 19 | } 20 | } 21 | 22 | describe('containers', () => { 23 | describe('App', () => { 24 | it('should display initial count', () => { 25 | const { p } = setup() 26 | expect(p.textContent).toMatch(/^Clicked: 0 times/) 27 | }) 28 | 29 | it('should display updated count after increment button click', () => { 30 | const { buttons, p } = setup() 31 | TestUtils.Simulate.click(buttons[0]) 32 | expect(p.textContent).toMatch(/^Clicked: 1 times/) 33 | }) 34 | 35 | it('should display updated count after decrement button click', () => { 36 | const { buttons, p } = setup() 37 | TestUtils.Simulate.click(buttons[1]) 38 | expect(p.textContent).toMatch(/^Clicked: -1 times/) 39 | }) 40 | 41 | it('shouldnt change if even and if odd button clicked', () => { 42 | const { buttons, p } = setup() 43 | TestUtils.Simulate.click(buttons[2]) 44 | expect(p.textContent).toMatch(/^Clicked: 0 times/) 45 | }) 46 | 47 | it('should change if odd and if odd button clicked', () => { 48 | const { buttons, p } = setup({ counter: 1 }) 49 | TestUtils.Simulate.click(buttons[2]) 50 | expect(p.textContent).toMatch(/^Clicked: 2 times/) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /redux-examples/counter/test/reducers/counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import counter from '../../reducers/counter' 3 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../actions/counter' 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 | -------------------------------------------------------------------------------- /redux-examples/counter/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 | -------------------------------------------------------------------------------- /redux-examples/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' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | loaders: [ 'babel' ], 25 | exclude: /node_modules/, 26 | include: __dirname 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /redux-examples/real-world/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 2, 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | "react-transform" 7 | ], 8 | "extra": { 9 | "react-transform": { 10 | "transforms": [{ 11 | "transform": "react-transform-hmr", 12 | "imports": ["react"], 13 | "locals": ["module"] 14 | }] 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /redux-examples/real-world/actions/index.js: -------------------------------------------------------------------------------- 1 | import { CALL_API, Schemas } from '../middleware/api' 2 | 3 | export const USER_REQUEST = 'USER_REQUEST' 4 | export const USER_SUCCESS = 'USER_SUCCESS' 5 | export const USER_FAILURE = 'USER_FAILURE' 6 | 7 | // Fetches a single user from Github API. 8 | // Relies on the custom API middleware defined in ../middleware/api.js. 9 | function fetchUser(login) { 10 | return { 11 | [CALL_API]: { 12 | types: [ USER_REQUEST, USER_SUCCESS, USER_FAILURE ], 13 | endpoint: `users/${login}`, 14 | schema: Schemas.USER 15 | } 16 | } 17 | } 18 | 19 | // Fetches a single user from Github API unless it is cached. 20 | // Relies on Redux Thunk middleware. 21 | export function loadUser(login, requiredFields = []) { 22 | return (dispatch, getState) => { 23 | const user = getState().entities.users[login] 24 | if (user && requiredFields.every(key => user.hasOwnProperty(key))) { 25 | return null 26 | } 27 | 28 | return dispatch(fetchUser(login)) 29 | } 30 | } 31 | 32 | export const REPO_REQUEST = 'REPO_REQUEST' 33 | export const REPO_SUCCESS = 'REPO_SUCCESS' 34 | export const REPO_FAILURE = 'REPO_FAILURE' 35 | 36 | // Fetches a single repository from Github API. 37 | // Relies on the custom API middleware defined in ../middleware/api.js. 38 | function fetchRepo(fullName) { 39 | return { 40 | [CALL_API]: { 41 | types: [ REPO_REQUEST, REPO_SUCCESS, REPO_FAILURE ], 42 | endpoint: `repos/${fullName}`, 43 | schema: Schemas.REPO 44 | } 45 | } 46 | } 47 | 48 | // Fetches a single repository from Github API unless it is cached. 49 | // Relies on Redux Thunk middleware. 50 | export function loadRepo(fullName, requiredFields = []) { 51 | return (dispatch, getState) => { 52 | const repo = getState().entities.repos[fullName] 53 | if (repo && requiredFields.every(key => repo.hasOwnProperty(key))) { 54 | return null 55 | } 56 | 57 | return dispatch(fetchRepo(fullName)) 58 | } 59 | } 60 | 61 | export const STARRED_REQUEST = 'STARRED_REQUEST' 62 | export const STARRED_SUCCESS = 'STARRED_SUCCESS' 63 | export const STARRED_FAILURE = 'STARRED_FAILURE' 64 | 65 | // Fetches a page of starred repos by a particular user. 66 | // Relies on the custom API middleware defined in ../middleware/api.js. 67 | function fetchStarred(login, nextPageUrl) { 68 | return { 69 | login, 70 | [CALL_API]: { 71 | types: [ STARRED_REQUEST, STARRED_SUCCESS, STARRED_FAILURE ], 72 | endpoint: nextPageUrl, 73 | schema: Schemas.REPO_ARRAY 74 | } 75 | } 76 | } 77 | 78 | // Fetches a page of starred repos by a particular user. 79 | // Bails out if page is cached and user didn’t specifically request next page. 80 | // Relies on Redux Thunk middleware. 81 | export function loadStarred(login, nextPage) { 82 | return (dispatch, getState) => { 83 | const { 84 | nextPageUrl = `users/${login}/starred`, 85 | pageCount = 0 86 | } = getState().pagination.starredByUser[login] || {} 87 | 88 | if (pageCount > 0 && !nextPage) { 89 | return null 90 | } 91 | 92 | return dispatch(fetchStarred(login, nextPageUrl)) 93 | } 94 | } 95 | 96 | export const STARGAZERS_REQUEST = 'STARGAZERS_REQUEST' 97 | export const STARGAZERS_SUCCESS = 'STARGAZERS_SUCCESS' 98 | export const STARGAZERS_FAILURE = 'STARGAZERS_FAILURE' 99 | 100 | // Fetches a page of stargazers for a particular repo. 101 | // Relies on the custom API middleware defined in ../middleware/api.js. 102 | function fetchStargazers(fullName, nextPageUrl) { 103 | return { 104 | fullName, 105 | [CALL_API]: { 106 | types: [ STARGAZERS_REQUEST, STARGAZERS_SUCCESS, STARGAZERS_FAILURE ], 107 | endpoint: nextPageUrl, 108 | schema: Schemas.USER_ARRAY 109 | } 110 | } 111 | } 112 | 113 | // Fetches a page of stargazers for a particular repo. 114 | // Bails out if page is cached and user didn’t specifically request next page. 115 | // Relies on Redux Thunk middleware. 116 | export function loadStargazers(fullName, nextPage) { 117 | return (dispatch, getState) => { 118 | const { 119 | nextPageUrl = `repos/${fullName}/stargazers`, 120 | pageCount = 0 121 | } = getState().pagination.stargazersByRepo[fullName] || {} 122 | 123 | if (pageCount > 0 && !nextPage) { 124 | return null 125 | } 126 | 127 | return dispatch(fetchStargazers(fullName, nextPageUrl)) 128 | } 129 | } 130 | 131 | export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE' 132 | 133 | // Resets the currently visible error message. 134 | export function resetErrorMessage() { 135 | return { 136 | type: RESET_ERROR_MESSAGE 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /redux-examples/real-world/components/Explore.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | const GITHUB_REPO = 'https://github.com/rackt/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 | 47 | 50 |

51 | Code on Github. 52 |

53 |

54 | Move the DevTools with Ctrl+W or hide them with Ctrl+H. 55 |

56 |
57 | ) 58 | } 59 | } 60 | 61 | Explore.propTypes = { 62 | value: PropTypes.string.isRequired, 63 | onChange: PropTypes.func.isRequired 64 | } 65 | -------------------------------------------------------------------------------- /redux-examples/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 | 12 | ) 13 | } 14 | 15 | render() { 16 | const { 17 | isFetching, nextPageUrl, pageCount, 18 | items, renderItem, loadingLabel 19 | } = this.props 20 | 21 | const isEmpty = items.length === 0 22 | if (isEmpty && isFetching) { 23 | return

{loadingLabel}

24 | } 25 | 26 | const isLastPage = !nextPageUrl 27 | if (isEmpty && isLastPage) { 28 | return

Nothing here!

29 | } 30 | 31 | return ( 32 |
33 | {items.map(renderItem)} 34 | {pageCount > 0 && !isLastPage && this.renderLoadMore()} 35 |
36 | ) 37 | } 38 | } 39 | 40 | List.propTypes = { 41 | loadingLabel: PropTypes.string.isRequired, 42 | pageCount: PropTypes.number, 43 | renderItem: PropTypes.func.isRequired, 44 | items: PropTypes.array.isRequired, 45 | isFetching: PropTypes.bool.isRequired, 46 | onLoadMoreClick: PropTypes.func.isRequired, 47 | nextPageUrl: PropTypes.string 48 | } 49 | 50 | List.defaultProps = { 51 | isFetching: true, 52 | loadingLabel: 'Loading...' 53 | } 54 | -------------------------------------------------------------------------------- /redux-examples/real-world/components/Repo.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | export default class Repo extends Component { 5 | 6 | render() { 7 | const { repo, owner } = this.props 8 | const { login } = owner 9 | const { name, description } = repo 10 | 11 | return ( 12 |
13 |

14 | 15 | {name} 16 | 17 | {' by '} 18 | 19 | {login} 20 | 21 |

22 | {description && 23 |

{description}

24 | } 25 |
26 | ) 27 | } 28 | } 29 | 30 | Repo.propTypes = { 31 | repo: PropTypes.shape({ 32 | name: PropTypes.string.isRequired, 33 | description: PropTypes.string 34 | }).isRequired, 35 | owner: PropTypes.shape({ 36 | login: PropTypes.string.isRequired 37 | }).isRequired 38 | } 39 | -------------------------------------------------------------------------------- /redux-examples/real-world/components/User.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | export default class User extends Component { 5 | render() { 6 | const { login, avatarUrl, name } = this.props.user 7 | 8 | return ( 9 |
10 | 11 | 12 |

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

15 | 16 |
17 | ) 18 | } 19 | } 20 | 21 | User.propTypes = { 22 | user: PropTypes.shape({ 23 | login: PropTypes.string.isRequired, 24 | avatarUrl: PropTypes.string.isRequired, 25 | name: PropTypes.string 26 | }).isRequired 27 | } 28 | -------------------------------------------------------------------------------- /redux-examples/real-world/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { pushState } from 'redux-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 | this.props.pushState(null, `/${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 | ( 35 | Dismiss 36 | ) 37 |

38 | ) 39 | } 40 | 41 | render() { 42 | const { children, inputValue } = this.props 43 | return ( 44 |
45 | 47 |
48 | {this.renderErrorMessage()} 49 | {children} 50 |
51 | ) 52 | } 53 | } 54 | 55 | App.propTypes = { 56 | // Injected by React Redux 57 | errorMessage: PropTypes.string, 58 | resetErrorMessage: PropTypes.func.isRequired, 59 | pushState: PropTypes.func.isRequired, 60 | inputValue: PropTypes.string.isRequired, 61 | // Injected by React Router 62 | children: PropTypes.node 63 | } 64 | 65 | function mapStateToProps(state) { 66 | return { 67 | errorMessage: state.errorMessage, 68 | inputValue: state.router.location.pathname.substring(1) 69 | } 70 | } 71 | 72 | export default connect(mapStateToProps, { 73 | resetErrorMessage, 74 | pushState 75 | })(App) 76 | -------------------------------------------------------------------------------- /redux-examples/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 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /redux-examples/real-world/containers/RepoPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { loadRepo, loadStargazers } from '../actions' 4 | import Repo from '../components/Repo' 5 | import User from '../components/User' 6 | import List from '../components/List' 7 | 8 | function loadData(props) { 9 | const { fullName } = props 10 | props.loadRepo(fullName, [ 'description' ]) 11 | props.loadStargazers(fullName) 12 | } 13 | 14 | class RepoPage extends Component { 15 | constructor(props) { 16 | super(props) 17 | this.renderUser = this.renderUser.bind(this) 18 | this.handleLoadMoreClick = this.handleLoadMoreClick.bind(this) 19 | } 20 | 21 | componentWillMount() { 22 | loadData(this.props) 23 | } 24 | 25 | componentWillReceiveProps(nextProps) { 26 | if (nextProps.fullName !== this.props.fullName) { 27 | loadData(nextProps) 28 | } 29 | } 30 | 31 | handleLoadMoreClick() { 32 | this.props.loadStargazers(this.props.fullName, true) 33 | } 34 | 35 | renderUser(user) { 36 | return ( 37 | 39 | ) 40 | } 41 | 42 | render() { 43 | const { repo, owner, name } = this.props 44 | if (!repo || !owner) { 45 | return

Loading {name} details...

46 | } 47 | 48 | const { stargazers, stargazersPagination } = this.props 49 | return ( 50 |
51 | 53 |
54 | 59 |
60 | ) 61 | } 62 | } 63 | 64 | RepoPage.propTypes = { 65 | repo: PropTypes.object, 66 | fullName: PropTypes.string.isRequired, 67 | name: PropTypes.string.isRequired, 68 | owner: PropTypes.object, 69 | stargazers: PropTypes.array.isRequired, 70 | stargazersPagination: PropTypes.object, 71 | loadRepo: PropTypes.func.isRequired, 72 | loadStargazers: PropTypes.func.isRequired 73 | } 74 | 75 | function mapStateToProps(state) { 76 | const { login, name } = state.router.params 77 | const { 78 | pagination: { stargazersByRepo }, 79 | entities: { users, repos } 80 | } = state 81 | 82 | const fullName = `${login}/${name}` 83 | const stargazersPagination = stargazersByRepo[fullName] || { ids: [] } 84 | const stargazers = stargazersPagination.ids.map(id => users[id]) 85 | 86 | return { 87 | fullName, 88 | name, 89 | stargazers, 90 | stargazersPagination, 91 | repo: repos[fullName], 92 | owner: users[login] 93 | } 94 | } 95 | 96 | export default connect(mapStateToProps, { 97 | loadRepo, 98 | loadStargazers 99 | })(RepoPage) 100 | -------------------------------------------------------------------------------- /redux-examples/real-world/containers/Root.dev.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Provider } from 'react-redux' 3 | import { ReduxRouter } from 'redux-router' 4 | import DevTools from './DevTools' 5 | 6 | export default class Root extends Component { 7 | render() { 8 | const { store } = this.props 9 | return ( 10 | 11 |
12 | 13 | 14 |
15 |
16 | ) 17 | } 18 | } 19 | 20 | Root.propTypes = { 21 | store: PropTypes.object.isRequired 22 | } 23 | -------------------------------------------------------------------------------- /redux-examples/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 | -------------------------------------------------------------------------------- /redux-examples/real-world/containers/Root.prod.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Provider } from 'react-redux' 3 | import { ReduxRouter } from 'redux-router' 4 | 5 | export default class Root extends Component { 6 | render() { 7 | const { store } = this.props 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | } 15 | 16 | Root.propTypes = { 17 | store: PropTypes.object.isRequired 18 | } 19 | -------------------------------------------------------------------------------- /redux-examples/real-world/containers/UserPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { loadUser, loadStarred } from '../actions' 4 | import User from '../components/User' 5 | import Repo from '../components/Repo' 6 | import List from '../components/List' 7 | import zip from 'lodash/array/zip' 8 | 9 | function loadData(props) { 10 | const { login } = props 11 | props.loadUser(login, [ 'name' ]) 12 | props.loadStarred(login) 13 | } 14 | 15 | class UserPage extends Component { 16 | constructor(props) { 17 | super(props) 18 | this.renderRepo = this.renderRepo.bind(this) 19 | this.handleLoadMoreClick = this.handleLoadMoreClick.bind(this) 20 | } 21 | 22 | componentWillMount() { 23 | loadData(this.props) 24 | } 25 | 26 | componentWillReceiveProps(nextProps) { 27 | if (nextProps.login !== this.props.login) { 28 | loadData(nextProps) 29 | } 30 | } 31 | 32 | handleLoadMoreClick() { 33 | this.props.loadStarred(this.props.login, true) 34 | } 35 | 36 | renderRepo([ repo, owner ]) { 37 | return ( 38 | 41 | ) 42 | } 43 | 44 | render() { 45 | const { user, login } = this.props 46 | if (!user) { 47 | return

Loading {login}’s profile...

48 | } 49 | 50 | const { starredRepos, starredRepoOwners, starredPagination } = this.props 51 | return ( 52 |
53 | 54 |
55 | 60 |
61 | ) 62 | } 63 | } 64 | 65 | UserPage.propTypes = { 66 | login: PropTypes.string.isRequired, 67 | user: PropTypes.object, 68 | starredPagination: PropTypes.object, 69 | starredRepos: PropTypes.array.isRequired, 70 | starredRepoOwners: PropTypes.array.isRequired, 71 | loadUser: PropTypes.func.isRequired, 72 | loadStarred: PropTypes.func.isRequired 73 | } 74 | 75 | function mapStateToProps(state) { 76 | const { login } = state.router.params 77 | const { 78 | pagination: { starredByUser }, 79 | entities: { users, repos } 80 | } = state 81 | 82 | const starredPagination = starredByUser[login] || { ids: [] } 83 | const starredRepos = starredPagination.ids.map(id => repos[id]) 84 | const starredRepoOwners = starredRepos.map(repo => users[repo.owner]) 85 | 86 | return { 87 | login, 88 | starredRepos, 89 | starredRepoOwners, 90 | starredPagination, 91 | user: users[login] 92 | } 93 | } 94 | 95 | export default connect(mapStateToProps, { 96 | loadUser, 97 | loadStarred 98 | })(UserPage) 99 | -------------------------------------------------------------------------------- /redux-examples/real-world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux real-world example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /redux-examples/real-world/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-core/polyfill' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import Root from './containers/Root' 5 | import configureStore from './store/configureStore' 6 | 7 | const store = configureStore() 8 | 9 | render( 10 | , 11 | document.getElementById('root') 12 | ) 13 | -------------------------------------------------------------------------------- /redux-examples/real-world/middleware/api.js: -------------------------------------------------------------------------------- 1 | import { Schema, arrayOf, normalize } from 'normalizr' 2 | import { camelizeKeys } from 'humps' 3 | import 'isomorphic-fetch' 4 | 5 | // Extracts the next page URL from Github API response. 6 | function getNextPageUrl(response) { 7 | const link = response.headers.get('link') 8 | if (!link) { 9 | return null 10 | } 11 | 12 | const nextLink = link.split(',').find(s => s.indexOf('rel="next"') > -1) 13 | if (!nextLink) { 14 | return null 15 | } 16 | 17 | return nextLink.split(';')[0].slice(1, -1) 18 | } 19 | 20 | const API_ROOT = 'https://api.github.com/' 21 | 22 | // Fetches an API response and normalizes the result JSON according to schema. 23 | // This makes every API response have the same shape, regardless of how nested it was. 24 | function callApi(endpoint, schema) { 25 | const fullUrl = (endpoint.indexOf(API_ROOT) === -1) ? API_ROOT + endpoint : endpoint 26 | 27 | return fetch(fullUrl) 28 | .then(response => 29 | response.json().then(json => ({ json, response })) 30 | ).then(({ json, response }) => { 31 | if (!response.ok) { 32 | return Promise.reject(json) 33 | } 34 | 35 | const camelizedJson = camelizeKeys(json) 36 | const nextPageUrl = getNextPageUrl(response) 37 | 38 | return Object.assign({}, 39 | normalize(camelizedJson, schema), 40 | { nextPageUrl } 41 | ) 42 | }) 43 | } 44 | 45 | // We use this Normalizr schemas to transform API responses from a nested form 46 | // to a flat form where repos and users are placed in `entities`, and nested 47 | // JSON objects are replaced with their IDs. This is very convenient for 48 | // consumption by reducers, because we can easily build a normalized tree 49 | // and keep it updated as we fetch more data. 50 | 51 | // Read more about Normalizr: https://github.com/gaearon/normalizr 52 | 53 | const userSchema = new Schema('users', { 54 | idAttribute: 'login' 55 | }) 56 | 57 | const repoSchema = new Schema('repos', { 58 | idAttribute: 'fullName' 59 | }) 60 | 61 | repoSchema.define({ 62 | owner: userSchema 63 | }) 64 | 65 | // Schemas for Github API responses. 66 | export const Schemas = { 67 | USER: userSchema, 68 | USER_ARRAY: arrayOf(userSchema), 69 | REPO: repoSchema, 70 | REPO_ARRAY: arrayOf(repoSchema) 71 | } 72 | 73 | // Action key that carries API call info interpreted by this Redux middleware. 74 | export const CALL_API = Symbol('Call API') 75 | 76 | // A Redux middleware that interprets actions with CALL_API info specified. 77 | // Performs the call and promises when such actions are dispatched. 78 | export default store => next => action => { 79 | const callAPI = action[CALL_API] 80 | if (typeof callAPI === 'undefined') { 81 | return next(action) 82 | } 83 | 84 | let { endpoint } = callAPI 85 | const { schema, types } = callAPI 86 | 87 | if (typeof endpoint === 'function') { 88 | endpoint = endpoint(store.getState()) 89 | } 90 | 91 | if (typeof endpoint !== 'string') { 92 | throw new Error('Specify a string endpoint URL.') 93 | } 94 | if (!schema) { 95 | throw new Error('Specify one of the exported Schemas.') 96 | } 97 | if (!Array.isArray(types) || types.length !== 3) { 98 | throw new Error('Expected an array of three action types.') 99 | } 100 | if (!types.every(type => typeof type === 'string')) { 101 | throw new Error('Expected action types to be strings.') 102 | } 103 | 104 | function actionWith(data) { 105 | const finalAction = Object.assign({}, action, data) 106 | delete finalAction[CALL_API] 107 | return finalAction 108 | } 109 | 110 | const [ requestType, successType, failureType ] = types 111 | next(actionWith({ type: requestType })) 112 | 113 | return callApi(endpoint, schema).then( 114 | response => next(actionWith({ 115 | response, 116 | type: successType 117 | })), 118 | error => next(actionWith({ 119 | type: failureType, 120 | error: error.message || 'Something bad happened' 121 | })) 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /redux-examples/real-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-real-world-example", 3 | "version": "0.0.0", 4 | "description": "Redux real-world example", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/rackt/redux.git" 11 | }, 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/rackt/redux/issues" 15 | }, 16 | "homepage": "http://rackt.github.io/redux", 17 | "dependencies": { 18 | "history": "^1.17.0", 19 | "humps": "^0.6.0", 20 | "isomorphic-fetch": "^2.1.1", 21 | "lodash": "^3.10.1", 22 | "normalizr": "^1.0.0", 23 | "react": "^0.14.0", 24 | "react-dom": "^0.14.0", 25 | "react-redux": "^2.1.2", 26 | "react-router": "^1.0.3", 27 | "redux": "^3.0.0", 28 | "redux-logger": "^2.0.2", 29 | "redux-router": "^1.0.0-beta3", 30 | "redux-thunk": "^0.1.0" 31 | }, 32 | "devDependencies": { 33 | "babel-core": "^5.6.18", 34 | "babel-loader": "^5.1.4", 35 | "babel-plugin-react-transform": "^1.1.0", 36 | "concurrently": "^0.1.1", 37 | "express": "^4.13.3", 38 | "react-transform-hmr": "^1.0.0", 39 | "redux-devtools": "^3.0.0", 40 | "redux-devtools-dock-monitor": "^1.0.1", 41 | "redux-devtools-log-monitor": "^1.0.1", 42 | "webpack": "^1.9.11", 43 | "webpack-dev-middleware": "^1.2.0", 44 | "webpack-hot-middleware": "^2.2.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /redux-examples/real-world/reducers/index.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions' 2 | import merge from 'lodash/object/merge' 3 | import paginate from './paginate' 4 | import { routerStateReducer as router } from 'redux-router' 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 | router 54 | }) 55 | 56 | export default rootReducer 57 | -------------------------------------------------------------------------------- /redux-examples/real-world/reducers/paginate.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/object/merge' 2 | import union from 'lodash/array/union' 3 | 4 | // Creates a reducer managing pagination, given the action types to handle, 5 | // and a function telling how to extract the key from an action. 6 | export default function paginate({ types, mapActionToKey }) { 7 | if (!Array.isArray(types) || types.length !== 3) { 8 | throw new Error('Expected types to be an array of three elements.') 9 | } 10 | if (!types.every(t => typeof t === 'string')) { 11 | throw new Error('Expected types to be strings.') 12 | } 13 | if (typeof mapActionToKey !== 'function') { 14 | throw new Error('Expected mapActionToKey to be a function.') 15 | } 16 | 17 | const [ requestType, successType, failureType ] = types 18 | 19 | function updatePagination(state = { 20 | isFetching: false, 21 | nextPageUrl: undefined, 22 | pageCount: 0, 23 | ids: [] 24 | }, action) { 25 | switch (action.type) { 26 | case requestType: 27 | return merge({}, state, { 28 | isFetching: true 29 | }) 30 | case successType: 31 | return merge({}, state, { 32 | isFetching: false, 33 | ids: union(state.ids, action.response.result), 34 | nextPageUrl: action.response.nextPageUrl, 35 | pageCount: state.pageCount + 1 36 | }) 37 | case failureType: 38 | return merge({}, state, { 39 | isFetching: false 40 | }) 41 | default: 42 | return state 43 | } 44 | } 45 | 46 | return function updatePaginationByKey(state = {}, action) { 47 | switch (action.type) { 48 | case requestType: 49 | case successType: 50 | case failureType: 51 | const key = mapActionToKey(action) 52 | if (typeof key !== 'string') { 53 | throw new Error('Expected key to be a string.') 54 | } 55 | return merge({}, state, { 56 | [key]: updatePagination(state[key], action) 57 | }) 58 | default: 59 | return state 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /redux-examples/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 | 11 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /redux-examples/real-world/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.use(function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /redux-examples/real-world/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import { reduxReactRouter } from 'redux-router' 3 | import DevTools from '../containers/DevTools' 4 | import createHistory from 'history/lib/createBrowserHistory' 5 | import routes from '../routes' 6 | import thunk from 'redux-thunk' 7 | import api from '../middleware/api' 8 | import createLogger from 'redux-logger' 9 | import rootReducer from '../reducers' 10 | 11 | const finalCreateStore = compose( 12 | applyMiddleware(thunk, api), 13 | reduxReactRouter({ routes, createHistory }), 14 | applyMiddleware(createLogger()), 15 | DevTools.instrument() 16 | )(createStore) 17 | 18 | export default function configureStore(initialState) { 19 | const store = finalCreateStore(rootReducer, initialState) 20 | 21 | if (module.hot) { 22 | // Enable Webpack hot module replacement for reducers 23 | module.hot.accept('../reducers', () => { 24 | const nextRootReducer = require('../reducers') 25 | store.replaceReducer(nextRootReducer) 26 | }) 27 | } 28 | 29 | return store 30 | } 31 | -------------------------------------------------------------------------------- /redux-examples/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 | -------------------------------------------------------------------------------- /redux-examples/real-world/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import { reduxReactRouter } from 'redux-router' 3 | import createHistory from 'history/lib/createBrowserHistory' 4 | import routes from '../routes' 5 | import thunk from 'redux-thunk' 6 | import api from '../middleware/api' 7 | import rootReducer from '../reducers' 8 | 9 | const finalCreateStore = compose( 10 | applyMiddleware(thunk, api), 11 | reduxReactRouter({ routes, createHistory }) 12 | )(createStore) 13 | 14 | export default function configureStore(initialState) { 15 | return finalCreateStore(rootReducer, initialState) 16 | } 17 | -------------------------------------------------------------------------------- /redux-examples/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.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [{ 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | }] 27 | } 28 | } 29 | 30 | 31 | // When inside Redux repo, prefer src to compiled version. 32 | // You can safely delete these lines in your project. 33 | var reduxSrc = path.join(__dirname, '..', '..', 'src') 34 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules') 35 | var fs = require('fs') 36 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) { 37 | // Resolve Redux to source 38 | module.exports.resolve = { alias: { 'redux': reduxSrc } } 39 | // Compile Redux from source 40 | module.exports.module.loaders.push({ 41 | test: /\.js$/, 42 | loaders: [ 'babel' ], 43 | include: reduxSrc 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/actions/index.js: -------------------------------------------------------------------------------- 1 | import shop from '../api/shop' 2 | import * as types from '../constants/ActionTypes' 3 | 4 | function receiveProducts(products) { 5 | return { 6 | type: types.RECEIVE_PRODUCTS, 7 | products: products 8 | } 9 | } 10 | 11 | export function getAllProducts() { 12 | return dispatch => { 13 | shop.getProducts(products => { 14 | dispatch(receiveProducts(products)) 15 | }) 16 | } 17 | } 18 | 19 | function addToCartUnsafe(productId) { 20 | return { 21 | type: types.ADD_TO_CART, 22 | productId 23 | } 24 | } 25 | 26 | export function addToCart(productId) { 27 | return (dispatch, getState) => { 28 | if (getState().products.byId[productId].inventory > 0) { 29 | dispatch(addToCartUnsafe(productId)) 30 | } 31 | } 32 | } 33 | 34 | export function checkout(products) { 35 | return (dispatch, getState) => { 36 | const cart = getState().cart 37 | 38 | dispatch({ 39 | type: types.CHECKOUT_REQUEST 40 | }) 41 | shop.buyProducts(products, () => { 42 | dispatch({ 43 | type: types.CHECKOUT_SUCCESS, 44 | cart 45 | }) 46 | // Replace the line above with line below to rollback on failure: 47 | // dispatch({ type: types.CHECKOUT_FAILURE, cart }) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/api/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2}, 3 | {"id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10}, 4 | {"id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5} 5 | ] 6 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/api/shop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocking client-server processing 3 | */ 4 | import _products from './products.json' 5 | 6 | const TIMEOUT = 100 7 | 8 | export default { 9 | getProducts(cb, timeout) { 10 | setTimeout(() => cb(_products), timeout || TIMEOUT) 11 | }, 12 | 13 | buyProducts(payload, cb, timeout) { 14 | setTimeout(() => cb(), timeout || TIMEOUT) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/components/Cart.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import Product from './Product' 3 | 4 | export default class Cart extends Component { 5 | render() { 6 | const { products, total, onCheckoutClicked } = this.props 7 | 8 | const hasProducts = products.length > 0 9 | const nodes = !hasProducts ? 10 | Please add some products to cart. : 11 | products.map(product => 12 | 17 | ) 18 | 19 | return ( 20 |
21 |

Your Cart

22 |
{nodes}
23 |

Total: ${total}

24 | 28 |
29 | ) 30 | } 31 | } 32 | 33 | Cart.propTypes = { 34 | products: PropTypes.array, 35 | total: PropTypes.string, 36 | onCheckoutClicked: PropTypes.func 37 | } 38 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/components/Product.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class Product extends Component { 4 | render() { 5 | const { price, quantity, title } = this.props 6 | return
{title} - ${price} {quantity ? `x ${quantity}` : null}
7 | } 8 | } 9 | 10 | Product.propTypes = { 11 | price: PropTypes.number, 12 | quantity: PropTypes.number, 13 | title: PropTypes.string 14 | } 15 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/components/ProductItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import Product from './Product' 3 | 4 | export default class ProductItem extends Component { 5 | render() { 6 | const { product } = this.props 7 | 8 | return ( 9 |
11 | 14 | 19 |
20 | ) 21 | } 22 | } 23 | 24 | ProductItem.propTypes = { 25 | product: PropTypes.shape({ 26 | title: PropTypes.string.isRequired, 27 | price: PropTypes.number.isRequired, 28 | inventory: PropTypes.number.isRequired 29 | }).isRequired, 30 | onAddToCartClicked: PropTypes.func.isRequired 31 | } 32 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/components/ProductsList.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class ProductsList extends Component { 4 | render() { 5 | return ( 6 |
7 |

{this.props.title}

8 |
{this.props.children}
9 |
10 | ) 11 | } 12 | } 13 | 14 | ProductsList.propTypes = { 15 | children: PropTypes.node, 16 | title: PropTypes.string.isRequired 17 | } 18 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TO_CART = 'ADD_TO_CART' 2 | export const CHECKOUT_REQUEST = 'CHECKOUT_REQUEST' 3 | export const CHECKOUT_SUCCESS = 'CHECKOUT_SUCCESS' 4 | export const CHECKOUT_FAILURE = 'CHECKOUT_FAILURE' 5 | export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS' 6 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ProductsContainer from './ProductsContainer' 3 | import CartContainer from './CartContainer' 4 | 5 | export default class App extends Component { 6 | render() { 7 | return ( 8 |
9 |

Shopping Cart Example

10 |
11 | 12 |
13 | 14 |
15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/containers/CartContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { checkout } from '../actions' 4 | import { getTotal, getCartProducts } from '../reducers' 5 | import Cart from '../components/Cart' 6 | 7 | class CartContainer extends Component { 8 | render() { 9 | const { products, total } = this.props 10 | 11 | return ( 12 | this.props.checkout()} /> 16 | ) 17 | } 18 | } 19 | 20 | CartContainer.propTypes = { 21 | products: PropTypes.arrayOf(PropTypes.shape({ 22 | id: PropTypes.number.isRequired, 23 | title: PropTypes.string.isRequired, 24 | price: PropTypes.number.isRequired, 25 | quantity: PropTypes.number.isRequired 26 | })).isRequired, 27 | total: PropTypes.string, 28 | checkout: PropTypes.func.isRequired 29 | } 30 | 31 | const mapStateToProps = (state) => { 32 | return { 33 | products: getCartProducts(state), 34 | total: getTotal(state) 35 | } 36 | } 37 | 38 | export default connect( 39 | mapStateToProps, 40 | { checkout } 41 | )(CartContainer) 42 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/containers/ProductsContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { addToCart } from '../actions' 4 | import { getVisibleProducts } from '../reducers/products' 5 | import ProductItem from '../components/ProductItem' 6 | import ProductsList from '../components/ProductsList' 7 | 8 | class ProductsContainer extends Component { 9 | render() { 10 | const { products } = this.props 11 | return ( 12 | 13 | {products.map(product => 14 | this.props.addToCart(product.id)} /> 18 | )} 19 | 20 | ) 21 | } 22 | } 23 | 24 | ProductsContainer.propTypes = { 25 | products: PropTypes.arrayOf(PropTypes.shape({ 26 | id: PropTypes.number.isRequired, 27 | title: PropTypes.string.isRequired, 28 | price: PropTypes.number.isRequired, 29 | inventory: PropTypes.number.isRequired 30 | })).isRequired, 31 | addToCart: PropTypes.func.isRequired 32 | } 33 | 34 | function mapStateToProps(state) { 35 | return { 36 | products: getVisibleProducts(state.products) 37 | } 38 | } 39 | 40 | export default connect( 41 | mapStateToProps, 42 | { addToCart } 43 | )(ProductsContainer) 44 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux shopping cart example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware,compose } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import logger from 'redux-logger' 6 | import thunk from 'redux-thunk' 7 | import reducer from './reducers' 8 | import { getAllProducts } from './actions' 9 | import App from './containers/App' 10 | 11 | const middleware = process.env.NODE_ENV === 'production' ? 12 | [ thunk ] : 13 | [ thunk, logger() ] 14 | 15 | const createStoreWithMiddleware = compose(applyMiddleware(...middleware), 16 | window.devToolsExtension ? window.devToolsExtension() : f => f)(createStore) 17 | const store = createStoreWithMiddleware(reducer) 18 | 19 | store.dispatch(getAllProducts()) 20 | 21 | render( 22 | 23 | 24 | , 25 | document.getElementById('root') 26 | ) 27 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-shopping-cart-example", 3 | "version": "0.0.0", 4 | "description": "Redux shopping-cart example", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/rackt/redux.git" 11 | }, 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/rackt/redux/issues" 15 | }, 16 | "homepage": "http://rackt.github.io/redux", 17 | "dependencies": { 18 | "react": "^0.14.0", 19 | "react-dom": "^0.14.0", 20 | "react-redux": "^4.0.0", 21 | "redux": "^3.0.4", 22 | "redux-thunk": "^1.0.0" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^5.6.18", 26 | "babel-loader": "^5.1.4", 27 | "babel-plugin-react-transform": "^1.0.3", 28 | "express": "^4.13.3", 29 | "json-loader": "^0.5.3", 30 | "redux-logger": "^2.0.1", 31 | "webpack": "^1.9.11", 32 | "webpack-dev-middleware": "^1.2.0", 33 | "webpack-hot-middleware": "^2.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/reducers/cart.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TO_CART, 3 | CHECKOUT_REQUEST, 4 | CHECKOUT_FAILURE 5 | } from '../constants/ActionTypes' 6 | 7 | const initialState = { 8 | addedIds: [], 9 | quantityById: {} 10 | } 11 | 12 | function addedIds(state = initialState.addedIds, action) { 13 | switch (action.type) { 14 | case ADD_TO_CART: 15 | if (state.indexOf(action.productId) !== -1) { 16 | return state 17 | } 18 | return [ ...state, action.productId ] 19 | default: 20 | return state 21 | } 22 | } 23 | 24 | function quantityById(state = initialState.quantityById, action) { 25 | switch (action.type) { 26 | case ADD_TO_CART: 27 | const { productId } = action 28 | return { 29 | ...state, 30 | [productId]: (state[productId] || 0) + 1 31 | } 32 | default: 33 | return state 34 | } 35 | } 36 | 37 | export default function cart(state = initialState, action) { 38 | switch (action.type) { 39 | case CHECKOUT_REQUEST: 40 | return initialState 41 | case CHECKOUT_FAILURE: 42 | return action.cart 43 | default: 44 | return { 45 | addedIds: addedIds(state.addedIds, action), 46 | quantityById: quantityById(state.quantityById, action) 47 | } 48 | } 49 | } 50 | 51 | export function getQuantity(state, productId) { 52 | return state.quantityById[productId] || 0 53 | } 54 | 55 | export function getAddedIds(state) { 56 | return state.addedIds 57 | } 58 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { default as cart, getQuantity, getAddedIds } from './cart' 3 | import { default as products, getProduct } from './products' 4 | 5 | export function getTotal(state) { 6 | return getAddedIds(state.cart).reduce((total, id) => 7 | total + getProduct(state.products, id).price * getQuantity(state.cart, id), 8 | 0 9 | ).toFixed(2) 10 | } 11 | 12 | export function getCartProducts(state) { 13 | return getAddedIds(state.cart).map(id => ({ 14 | ...getProduct(state.products, id), 15 | quantity: getQuantity(state.cart, id) 16 | })) 17 | } 18 | 19 | export default combineReducers({ 20 | cart, 21 | products 22 | }) 23 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/reducers/products.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { RECEIVE_PRODUCTS, ADD_TO_CART } from '../constants/ActionTypes' 3 | 4 | function products(state, action) { 5 | switch (action.type) { 6 | case ADD_TO_CART: 7 | return { 8 | ...state, 9 | inventory: state.inventory - 1 10 | } 11 | default: 12 | return state 13 | } 14 | } 15 | 16 | function byId(state = {}, action) { 17 | switch (action.type) { 18 | case RECEIVE_PRODUCTS: 19 | return { 20 | ...state, 21 | ...action.products.reduce((obj, product) => { 22 | obj[product.id] = product 23 | return obj 24 | }, {}) 25 | } 26 | default: 27 | const { productId } = action 28 | if (productId) { 29 | return { 30 | ...state, 31 | [productId]: products(state[productId], action) 32 | } 33 | } 34 | return state 35 | } 36 | } 37 | 38 | function visibleIds(state = [], action) { 39 | switch (action.type) { 40 | case RECEIVE_PRODUCTS: 41 | return action.products.map(product => product.id) 42 | default: 43 | return state 44 | } 45 | } 46 | 47 | export default combineReducers({ 48 | byId, 49 | visibleIds 50 | }) 51 | 52 | export function getProduct(state, id) { 53 | return state.byId[id] 54 | } 55 | 56 | export function getVisibleProducts(state) { 57 | return state.visibleIds.map(id => getProduct(state, id)) 58 | } 59 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /redux-examples/shopping-cart/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.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [{ 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | }, 27 | { 28 | test: /\.json$/, 29 | loaders: [ 'json' ], 30 | exclude: /node_modules/, 31 | include: __dirname 32 | }] 33 | } 34 | } 35 | 36 | 37 | // When inside Redux repo, prefer src to compiled version. 38 | // You can safely delete these lines in your project. 39 | var reduxSrc = path.join(__dirname, '..', '..', 'src') 40 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules') 41 | var fs = require('fs') 42 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) { 43 | // Resolve Redux to source 44 | module.exports.resolve = { alias: { 'redux': reduxSrc } } 45 | // Compile Redux from source 46 | module.exports.module.loaders.push({ 47 | test: /\.js$/, 48 | loaders: [ 'babel' ], 49 | include: reduxSrc 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /redux-examples/todomvc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 2, 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | "react-transform" 7 | ], 8 | "extra": { 9 | "react-transform": { 10 | "transforms": [{ 11 | "transform": "react-transform-hmr", 12 | "imports": ["react"], 13 | "locals": ["module"] 14 | }] 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /redux-examples/todomvc/actions/todos.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 | -------------------------------------------------------------------------------- /redux-examples/todomvc/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import classnames from 'classnames' 3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 4 | 5 | const FILTER_TITLES = { 6 | [SHOW_ALL]: 'All', 7 | [SHOW_ACTIVE]: 'Active', 8 | [SHOW_COMPLETED]: 'Completed' 9 | } 10 | 11 | class Footer extends Component { 12 | renderTodoCount() { 13 | const { activeCount } = this.props 14 | const itemWord = activeCount === 1 ? 'item' : 'items' 15 | 16 | return ( 17 | 18 | {activeCount || 'No'} {itemWord} left 19 | 20 | ) 21 | } 22 | 23 | renderFilterLink(filter) { 24 | const title = FILTER_TITLES[filter] 25 | const { filter: selectedFilter, onShow } = this.props 26 | 27 | return ( 28 | onShow(filter)}> 31 | {title} 32 | 33 | ) 34 | } 35 | 36 | renderClearButton() { 37 | const { completedCount, onClearCompleted } = this.props 38 | if (completedCount > 0) { 39 | return ( 40 | 44 | ) 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 |
51 | {this.renderTodoCount()} 52 |
    53 | {[ SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED ].map(filter => 54 |
  • 55 | {this.renderFilterLink(filter)} 56 |
  • 57 | )} 58 |
59 | {this.renderClearButton()} 60 |
61 | ) 62 | } 63 | } 64 | 65 | Footer.propTypes = { 66 | completedCount: PropTypes.number.isRequired, 67 | activeCount: PropTypes.number.isRequired, 68 | filter: PropTypes.string.isRequired, 69 | onClearCompleted: PropTypes.func.isRequired, 70 | onShow: PropTypes.func.isRequired 71 | } 72 | 73 | export default Footer 74 | -------------------------------------------------------------------------------- /redux-examples/todomvc/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import TodoTextInput from './TodoTextInput' 3 | 4 | class Header extends Component { 5 | handleSave(text) { 6 | if (text.length !== 0) { 7 | this.props.addTodo(text) 8 | } 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |

todos

15 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | Header.propTypes = { 24 | addTodo: PropTypes.func.isRequired 25 | } 26 | 27 | export default Header 28 | -------------------------------------------------------------------------------- /redux-examples/todomvc/components/MainSection.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import TodoItem from './TodoItem' 3 | import Footer from './Footer' 4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 5 | 6 | const TODO_FILTERS = { 7 | [SHOW_ALL]: () => true, 8 | [SHOW_ACTIVE]: todo => !todo.completed, 9 | [SHOW_COMPLETED]: todo => todo.completed 10 | } 11 | 12 | class MainSection extends Component { 13 | constructor(props, context) { 14 | super(props, context) 15 | this.state = { filter: SHOW_ALL } 16 | } 17 | 18 | handleClearCompleted() { 19 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed) 20 | if (atLeastOneCompleted) { 21 | this.props.actions.clearCompleted() 22 | } 23 | } 24 | 25 | handleShow(filter) { 26 | this.setState({ filter }) 27 | } 28 | 29 | renderToggleAll(completedCount) { 30 | const { todos, actions } = this.props 31 | if (todos.length > 0) { 32 | return ( 33 | 37 | ) 38 | } 39 | } 40 | 41 | renderFooter(completedCount) { 42 | const { todos } = this.props 43 | const { filter } = this.state 44 | const activeCount = todos.length - completedCount 45 | 46 | if (todos.length) { 47 | return ( 48 |