├── .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 |
7 | {this.props.posts.map((post, i) =>
8 | - {post.title}
9 | )}
10 |
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 |
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 |
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 |
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 |
53 | )
54 | }
55 | }
56 |
57 | render() {
58 | const { todos, actions } = this.props
59 | const { filter } = this.state
60 |
61 | const filteredTodos = todos.filter(TODO_FILTERS[filter])
62 | const completedCount = todos.reduce((count, todo) =>
63 | todo.completed ? count + 1 : count,
64 | 0
65 | )
66 |
67 | return (
68 |
69 | {this.renderToggleAll(completedCount)}
70 |
71 | {filteredTodos.map(todo =>
72 |
73 | )}
74 |
75 | {this.renderFooter(completedCount)}
76 |
77 | )
78 | }
79 | }
80 |
81 | MainSection.propTypes = {
82 | todos: PropTypes.array.isRequired,
83 | actions: PropTypes.object.isRequired
84 | }
85 |
86 | export default MainSection
87 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/components/TodoItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 | import TodoTextInput from './TodoTextInput'
4 |
5 | class TodoItem extends Component {
6 | constructor(props, context) {
7 | super(props, context)
8 | this.state = {
9 | editing: false
10 | }
11 | }
12 |
13 | handleDoubleClick() {
14 | this.setState({ editing: true })
15 | }
16 |
17 | handleSave(id, text) {
18 | if (text.length === 0) {
19 | this.props.deleteTodo(id)
20 | } else {
21 | this.props.editTodo(id, text)
22 | }
23 | this.setState({ editing: false })
24 | }
25 |
26 | render() {
27 | const { todo, completeTodo, deleteTodo } = this.props
28 |
29 | let element
30 | if (this.state.editing) {
31 | element = (
32 | this.handleSave(todo.id, text)} />
35 | )
36 | } else {
37 | element = (
38 |
39 | completeTodo(todo.id)} />
43 |
46 |
49 | )
50 | }
51 |
52 | return (
53 |
57 | {element}
58 |
59 | )
60 | }
61 | }
62 |
63 | TodoItem.propTypes = {
64 | todo: PropTypes.object.isRequired,
65 | editTodo: PropTypes.func.isRequired,
66 | deleteTodo: PropTypes.func.isRequired,
67 | completeTodo: PropTypes.func.isRequired
68 | }
69 |
70 | export default TodoItem
71 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/components/TodoTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 |
4 | class TodoTextInput extends Component {
5 | constructor(props, context) {
6 | super(props, context)
7 | this.state = {
8 | text: this.props.text || ''
9 | }
10 | }
11 |
12 | handleSubmit(e) {
13 | const text = e.target.value.trim()
14 | if (e.which === 13) {
15 | this.props.onSave(text)
16 | if (this.props.newTodo) {
17 | this.setState({ text: '' })
18 | }
19 | }
20 | }
21 |
22 | handleChange(e) {
23 | this.setState({ text: e.target.value })
24 | }
25 |
26 | handleBlur(e) {
27 | if (!this.props.newTodo) {
28 | this.props.onSave(e.target.value)
29 | }
30 | }
31 |
32 | render() {
33 | return (
34 |
46 | )
47 | }
48 | }
49 |
50 | TodoTextInput.propTypes = {
51 | onSave: PropTypes.func.isRequired,
52 | text: PropTypes.string,
53 | placeholder: PropTypes.string,
54 | editing: PropTypes.bool,
55 | newTodo: PropTypes.bool
56 | }
57 |
58 | export default TodoTextInput
59 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO'
2 | export const DELETE_TODO = 'DELETE_TODO'
3 | export const EDIT_TODO = 'EDIT_TODO'
4 | export const COMPLETE_TODO = 'COMPLETE_TODO'
5 | export const COMPLETE_ALL = 'COMPLETE_ALL'
6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'
7 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/constants/TodoFilters.js:
--------------------------------------------------------------------------------
1 | export const SHOW_ALL = 'show_all'
2 | export const SHOW_COMPLETED = 'show_completed'
3 | export const SHOW_ACTIVE = 'show_active'
4 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { bindActionCreators } from 'redux'
3 | import { connect } from 'react-redux'
4 | import Header from '../components/Header'
5 | import MainSection from '../components/MainSection'
6 | import * as TodoActions from '../actions/todos'
7 |
8 | class App extends Component {
9 | render() {
10 | const { todos, actions } = this.props
11 | return (
12 |
13 |
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | App.propTypes = {
21 | todos: PropTypes.array.isRequired,
22 | actions: PropTypes.object.isRequired
23 | }
24 |
25 | function mapStateToProps(state) {
26 | return {
27 | todos: state.todos
28 | }
29 | }
30 |
31 | function mapDispatchToProps(dispatch) {
32 | return {
33 | actions: bindActionCreators(TodoActions, dispatch)
34 | }
35 | }
36 |
37 | export default connect(
38 | mapStateToProps,
39 | mapDispatchToProps
40 | )(App)
41 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux TodoMVC example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/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 | import 'todomvc-app-css/index.css'
8 |
9 | const store = configureStore()
10 |
11 | render(
12 |
13 |
14 | ,
15 | document.getElementById('root')
16 | )
17 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-todomvc-example",
3 | "version": "0.0.0",
4 | "description": "Redux TodoMVC 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 | "classnames": "^2.1.2",
21 | "react": "^0.14.0",
22 | "react-dom": "^0.14.0",
23 | "react-redux": "^4.0.0",
24 | "redux": "^3.0.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.8.0",
31 | "express": "^4.13.3",
32 | "jsdom": "^5.6.1",
33 | "mocha": "^2.2.5",
34 | "node-libs-browser": "^0.5.2",
35 | "raw-loader": "^0.5.1",
36 | "react-addons-test-utils": "^0.14.0",
37 | "react-transform-hmr": "^1.0.0",
38 | "style-loader": "^0.12.3",
39 | "todomvc-app-css": "^2.0.1",
40 | "webpack": "^1.9.11",
41 | "webpack-dev-middleware": "^1.2.0",
42 | "webpack-hot-middleware": "^2.2.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 |
4 | const rootReducer = combineReducers({
5 | todos
6 | })
7 |
8 | export default rootReducer
9 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'
2 |
3 | const initialState = [
4 | {
5 | text: 'Use Redux',
6 | completed: false,
7 | id: 0
8 | }
9 | ]
10 |
11 | export default function todos(state = initialState, action) {
12 | switch (action.type) {
13 | case ADD_TODO:
14 | return [
15 | {
16 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
17 | completed: false,
18 | text: action.text
19 | },
20 | ...state
21 | ]
22 |
23 | case DELETE_TODO:
24 | return state.filter(todo =>
25 | todo.id !== action.id
26 | )
27 |
28 | case EDIT_TODO:
29 | return state.map(todo =>
30 | todo.id === action.id ?
31 | Object.assign({}, todo, { text: action.text }) :
32 | todo
33 | )
34 |
35 | case COMPLETE_TODO:
36 | return state.map(todo =>
37 | todo.id === action.id ?
38 | Object.assign({}, todo, { completed: !todo.completed }) :
39 | todo
40 | )
41 |
42 | case COMPLETE_ALL:
43 | const areAllMarked = state.every(todo => todo.completed)
44 | return state.map(todo => Object.assign({}, todo, {
45 | completed: !areAllMarked
46 | }))
47 |
48 | case CLEAR_COMPLETED:
49 | return state.filter(todo => todo.completed === false)
50 |
51 | default:
52 | return state
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/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/todomvc/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore ,compose} from 'redux'
2 | import rootReducer from '../reducers'
3 |
4 | const finalCreateStore = compose(
5 | window.devToolsExtension ? window.devToolsExtension() : f => f
6 | )(createStore);
7 |
8 | export default function configureStore(initialState) {
9 | const store = finalCreateStore(rootReducer, initialState);
10 |
11 | if (module.hot) {
12 | // Enable Webpack hot module replacement for reducers
13 | module.hot.accept('../reducers', () => {
14 | const nextReducer = require('../reducers')
15 | store.replaceReducer(nextReducer)
16 | })
17 | }
18 |
19 | return store
20 | }
21 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/test/actions/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import * as types from '../../constants/ActionTypes'
3 | import * as actions from '../../actions/todos'
4 |
5 | describe('todo actions', () => {
6 | it('addTodo should create ADD_TODO action', () => {
7 | expect(actions.addTodo('Use Redux')).toEqual({
8 | type: types.ADD_TODO,
9 | text: 'Use Redux'
10 | })
11 | })
12 |
13 | it('deleteTodo should create DELETE_TODO action', () => {
14 | expect(actions.deleteTodo(1)).toEqual({
15 | type: types.DELETE_TODO,
16 | id: 1
17 | })
18 | })
19 |
20 | it('editTodo should create EDIT_TODO action', () => {
21 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
22 | type: types.EDIT_TODO,
23 | id: 1,
24 | text: 'Use Redux everywhere'
25 | })
26 | })
27 |
28 | it('completeTodo should create COMPLETE_TODO action', () => {
29 | expect(actions.completeTodo(1)).toEqual({
30 | type: types.COMPLETE_TODO,
31 | id: 1
32 | })
33 | })
34 |
35 | it('completeAll should create COMPLETE_ALL action', () => {
36 | expect(actions.completeAll()).toEqual({
37 | type: types.COMPLETE_ALL
38 | })
39 | })
40 |
41 | it('clearCompleted should create CLEAR_COMPLETED action', () => {
42 | expect(actions.clearCompleted('Use Redux')).toEqual({
43 | type: types.CLEAR_COMPLETED
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/test/components/Footer.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import Footer from '../../components/Footer'
5 | import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters'
6 |
7 | function setup(propOverrides) {
8 | const props = Object.assign({
9 | completedCount: 0,
10 | activeCount: 0,
11 | filter: SHOW_ALL,
12 | onClearCompleted: expect.createSpy(),
13 | onShow: expect.createSpy()
14 | }, propOverrides)
15 |
16 | const renderer = TestUtils.createRenderer()
17 | renderer.render()
18 | const output = renderer.getRenderOutput()
19 |
20 | return {
21 | props: props,
22 | output: output
23 | }
24 | }
25 |
26 | function getTextContent(elem) {
27 | const children = Array.isArray(elem.props.children) ?
28 | elem.props.children : [ elem.props.children ]
29 |
30 | return children.reduce(function concatText(out, child) {
31 | // Children are either elements or text strings
32 | return out + (child.props ? getTextContent(child) : child)
33 | }, '')
34 | }
35 |
36 | describe('components', () => {
37 | describe('Footer', () => {
38 | it('should render container', () => {
39 | const { output } = setup()
40 | expect(output.type).toBe('footer')
41 | expect(output.props.className).toBe('footer')
42 | })
43 |
44 | it('should display active count when 0', () => {
45 | const { output } = setup({ activeCount: 0 })
46 | const [ count ] = output.props.children
47 | expect(getTextContent(count)).toBe('No items left')
48 | })
49 |
50 | it('should display active count when above 0', () => {
51 | const { output } = setup({ activeCount: 1 })
52 | const [ count ] = output.props.children
53 | expect(getTextContent(count)).toBe('1 item left')
54 | })
55 |
56 | it('should render filters', () => {
57 | const { output } = setup()
58 | const [ , filters ] = output.props.children
59 | expect(filters.type).toBe('ul')
60 | expect(filters.props.className).toBe('filters')
61 | expect(filters.props.children.length).toBe(3)
62 | filters.props.children.forEach(function checkFilter(filter, i) {
63 | expect(filter.type).toBe('li')
64 | const a = filter.props.children
65 | expect(a.props.className).toBe(i === 0 ? 'selected' : '')
66 | expect(a.props.children).toBe({
67 | 0: 'All',
68 | 1: 'Active',
69 | 2: 'Completed'
70 | }[i])
71 | })
72 | })
73 |
74 | it('should call onShow when a filter is clicked', () => {
75 | const { output, props } = setup()
76 | const [ , filters ] = output.props.children
77 | const filterLink = filters.props.children[1].props.children
78 | filterLink.props.onClick({})
79 | expect(props.onShow).toHaveBeenCalledWith(SHOW_ACTIVE)
80 | })
81 |
82 | it('shouldnt show clear button when no completed todos', () => {
83 | const { output } = setup({ completedCount: 0 })
84 | const [ , , clear ] = output.props.children
85 | expect(clear).toBe(undefined)
86 | })
87 |
88 | it('should render clear button when completed todos', () => {
89 | const { output } = setup({ completedCount: 1 })
90 | const [ , , clear ] = output.props.children
91 | expect(clear.type).toBe('button')
92 | expect(clear.props.children).toBe('Clear completed')
93 | })
94 |
95 | it('should call onClearCompleted on clear button click', () => {
96 | const { output, props } = setup({ completedCount: 1 })
97 | const [ , , clear ] = output.props.children
98 | clear.props.onClick({})
99 | expect(props.onClearCompleted).toHaveBeenCalled()
100 | })
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/test/components/Header.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import Header from '../../components/Header'
5 | import TodoTextInput from '../../components/TodoTextInput'
6 |
7 | function setup() {
8 | const props = {
9 | addTodo: expect.createSpy()
10 | }
11 |
12 | const renderer = TestUtils.createRenderer()
13 | renderer.render()
14 | const output = renderer.getRenderOutput()
15 |
16 | return {
17 | props: props,
18 | output: output,
19 | renderer: renderer
20 | }
21 | }
22 |
23 | describe('components', () => {
24 | describe('Header', () => {
25 | it('should render correctly', () => {
26 | const { output } = setup()
27 |
28 | expect(output.type).toBe('header')
29 | expect(output.props.className).toBe('header')
30 |
31 | const [ h1, input ] = output.props.children
32 |
33 | expect(h1.type).toBe('h1')
34 | expect(h1.props.children).toBe('todos')
35 |
36 | expect(input.type).toBe(TodoTextInput)
37 | expect(input.props.newTodo).toBe(true)
38 | expect(input.props.placeholder).toBe('What needs to be done?')
39 | })
40 |
41 | it('should call addTodo if length of text is greater than 0', () => {
42 | const { output, props } = setup()
43 | const input = output.props.children[1]
44 | input.props.onSave('')
45 | expect(props.addTodo.calls.length).toBe(0)
46 | input.props.onSave('Use Redux')
47 | expect(props.addTodo.calls.length).toBe(1)
48 | })
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/test/components/MainSection.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import MainSection from '../../components/MainSection'
5 | import TodoItem from '../../components/TodoItem'
6 | import Footer from '../../components/Footer'
7 | import { SHOW_ALL, SHOW_COMPLETED } from '../../constants/TodoFilters'
8 |
9 | function setup(propOverrides) {
10 | const props = Object.assign({
11 | todos: [
12 | {
13 | text: 'Use Redux',
14 | completed: false,
15 | id: 0
16 | }, {
17 | text: 'Run the tests',
18 | completed: true,
19 | id: 1
20 | }
21 | ],
22 | actions: {
23 | editTodo: expect.createSpy(),
24 | deleteTodo: expect.createSpy(),
25 | completeTodo: expect.createSpy(),
26 | completeAll: expect.createSpy(),
27 | clearCompleted: expect.createSpy()
28 | }
29 | }, propOverrides)
30 |
31 | const renderer = TestUtils.createRenderer()
32 | renderer.render()
33 | const output = renderer.getRenderOutput()
34 |
35 | return {
36 | props: props,
37 | output: output,
38 | renderer: renderer
39 | }
40 | }
41 |
42 | describe('components', () => {
43 | describe('MainSection', () => {
44 | it('should render container', () => {
45 | const { output } = setup()
46 | expect(output.type).toBe('section')
47 | expect(output.props.className).toBe('main')
48 | })
49 |
50 | describe('toggle all input', () => {
51 | it('should render', () => {
52 | const { output } = setup()
53 | const [ toggle ] = output.props.children
54 | expect(toggle.type).toBe('input')
55 | expect(toggle.props.type).toBe('checkbox')
56 | expect(toggle.props.checked).toBe(false)
57 | })
58 |
59 | it('should be checked if all todos completed', () => {
60 | const { output } = setup({ todos: [
61 | {
62 | text: 'Use Redux',
63 | completed: true,
64 | id: 0
65 | }
66 | ]
67 | })
68 | const [ toggle ] = output.props.children
69 | expect(toggle.props.checked).toBe(true)
70 | })
71 |
72 | it('should call completeAll on change', () => {
73 | const { output, props } = setup()
74 | const [ toggle ] = output.props.children
75 | toggle.props.onChange({})
76 | expect(props.actions.completeAll).toHaveBeenCalled()
77 | })
78 | })
79 |
80 | describe('footer', () => {
81 | it('should render', () => {
82 | const { output } = setup()
83 | const [ , , footer ] = output.props.children
84 | expect(footer.type).toBe(Footer)
85 | expect(footer.props.completedCount).toBe(1)
86 | expect(footer.props.activeCount).toBe(1)
87 | expect(footer.props.filter).toBe(SHOW_ALL)
88 | })
89 |
90 | it('onShow should set the filter', () => {
91 | const { output, renderer } = setup()
92 | const [ , , footer ] = output.props.children
93 | footer.props.onShow(SHOW_COMPLETED)
94 | const updated = renderer.getRenderOutput()
95 | const [ , , updatedFooter ] = updated.props.children
96 | expect(updatedFooter.props.filter).toBe(SHOW_COMPLETED)
97 | })
98 |
99 | it('onClearCompleted should call clearCompleted', () => {
100 | const { output, props } = setup()
101 | const [ , , footer ] = output.props.children
102 | footer.props.onClearCompleted()
103 | expect(props.actions.clearCompleted).toHaveBeenCalled()
104 | })
105 |
106 | it('onClearCompleted shouldnt call clearCompleted if no todos completed', () => {
107 | const { output, props } = setup({
108 | todos: [
109 | {
110 | text: 'Use Redux',
111 | completed: false,
112 | id: 0
113 | }
114 | ]
115 | })
116 | const [ , , footer ] = output.props.children
117 | footer.props.onClearCompleted()
118 | expect(props.actions.clearCompleted.calls.length).toBe(0)
119 | })
120 | })
121 |
122 | describe('todo list', () => {
123 | it('should render', () => {
124 | const { output, props } = setup()
125 | const [ , list ] = output.props.children
126 | expect(list.type).toBe('ul')
127 | expect(list.props.children.length).toBe(2)
128 | list.props.children.forEach((item, i) => {
129 | expect(item.type).toBe(TodoItem)
130 | expect(item.props.todo).toBe(props.todos[i])
131 | })
132 | })
133 |
134 | it('should filter items', () => {
135 | const { output, renderer, props } = setup()
136 | const [ , , footer ] = output.props.children
137 | footer.props.onShow(SHOW_COMPLETED)
138 | const updated = renderer.getRenderOutput()
139 | const [ , updatedList ] = updated.props.children
140 | expect(updatedList.props.children.length).toBe(1)
141 | expect(updatedList.props.children[0].props.todo).toBe(props.todos[1])
142 | })
143 | })
144 | })
145 | })
146 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/test/components/TodoItem.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import TodoItem from '../../components/TodoItem'
5 | import TodoTextInput from '../../components/TodoTextInput'
6 |
7 | function setup( editing = false ) {
8 | const props = {
9 | todo: {
10 | id: 0,
11 | text: 'Use Redux',
12 | completed: false
13 | },
14 | editTodo: expect.createSpy(),
15 | deleteTodo: expect.createSpy(),
16 | completeTodo: expect.createSpy()
17 | }
18 |
19 | const renderer = TestUtils.createRenderer()
20 |
21 | renderer.render(
22 |
23 | )
24 |
25 | let output = renderer.getRenderOutput()
26 |
27 | if (editing) {
28 | const label = output.props.children.props.children[1]
29 | label.props.onDoubleClick({})
30 | output = renderer.getRenderOutput()
31 | }
32 |
33 | return {
34 | props: props,
35 | output: output,
36 | renderer: renderer
37 | }
38 | }
39 |
40 | describe('components', () => {
41 | describe('TodoItem', () => {
42 | it('initial render', () => {
43 | const { output } = setup()
44 |
45 | expect(output.type).toBe('li')
46 | expect(output.props.className).toBe('')
47 |
48 | const div = output.props.children
49 |
50 | expect(div.type).toBe('div')
51 | expect(div.props.className).toBe('view')
52 |
53 | const [ input, label, button ] = div.props.children
54 |
55 | expect(input.type).toBe('input')
56 | expect(input.props.checked).toBe(false)
57 |
58 | expect(label.type).toBe('label')
59 | expect(label.props.children).toBe('Use Redux')
60 |
61 | expect(button.type).toBe('button')
62 | expect(button.props.className).toBe('destroy')
63 | })
64 |
65 | it('input onChange should call completeTodo', () => {
66 | const { output, props } = setup()
67 | const input = output.props.children.props.children[0]
68 | input.props.onChange({})
69 | expect(props.completeTodo).toHaveBeenCalledWith(0)
70 | })
71 |
72 | it('button onClick should call deleteTodo', () => {
73 | const { output, props } = setup()
74 | const button = output.props.children.props.children[2]
75 | button.props.onClick({})
76 | expect(props.deleteTodo).toHaveBeenCalledWith(0)
77 | })
78 |
79 | it('label onDoubleClick should put component in edit state', () => {
80 | const { output, renderer } = setup()
81 | const label = output.props.children.props.children[1]
82 | label.props.onDoubleClick({})
83 | const updated = renderer.getRenderOutput()
84 | expect(updated.type).toBe('li')
85 | expect(updated.props.className).toBe('editing')
86 | })
87 |
88 | it('edit state render', () => {
89 | const { output } = setup(true)
90 |
91 | expect(output.type).toBe('li')
92 | expect(output.props.className).toBe('editing')
93 |
94 | const input = output.props.children
95 | expect(input.type).toBe(TodoTextInput)
96 | expect(input.props.text).toBe('Use Redux')
97 | expect(input.props.editing).toBe(true)
98 | })
99 |
100 | it('TodoTextInput onSave should call editTodo', () => {
101 | const { output, props } = setup(true)
102 | output.props.children.props.onSave('Use Redux')
103 | expect(props.editTodo).toHaveBeenCalledWith(0, 'Use Redux')
104 | })
105 |
106 | it('TodoTextInput onSave should call deleteTodo if text is empty', () => {
107 | const { output, props } = setup(true)
108 | output.props.children.props.onSave('')
109 | expect(props.deleteTodo).toHaveBeenCalledWith(0)
110 | })
111 |
112 | it('TodoTextInput onSave should exit component from edit state', () => {
113 | const { output, renderer } = setup(true)
114 | output.props.children.props.onSave('Use Redux')
115 | const updated = renderer.getRenderOutput()
116 | expect(updated.type).toBe('li')
117 | expect(updated.props.className).toBe('')
118 | })
119 | })
120 | })
121 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/test/components/TodoTextInput.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import TodoTextInput from '../../components/TodoTextInput'
5 |
6 | function setup(propOverrides) {
7 | const props = Object.assign({
8 | onSave: expect.createSpy(),
9 | text: 'Use Redux',
10 | placeholder: 'What needs to be done?',
11 | editing: false,
12 | newTodo: false
13 | }, propOverrides)
14 |
15 | const renderer = TestUtils.createRenderer()
16 |
17 | renderer.render(
18 |
19 | )
20 |
21 | let output = renderer.getRenderOutput()
22 |
23 | output = renderer.getRenderOutput()
24 |
25 | return {
26 | props: props,
27 | output: output,
28 | renderer: renderer
29 | }
30 | }
31 |
32 | describe('components', () => {
33 | describe('TodoTextInput', () => {
34 | it('should render correctly', () => {
35 | const { output } = setup()
36 | expect(output.props.placeholder).toEqual('What needs to be done?')
37 | expect(output.props.value).toEqual('Use Redux')
38 | expect(output.props.className).toEqual('')
39 | })
40 |
41 | it('should render correctly when editing=true', () => {
42 | const { output } = setup({ editing: true })
43 | expect(output.props.className).toEqual('edit')
44 | })
45 |
46 | it('should render correctly when newTodo=true', () => {
47 | const { output } = setup({ newTodo: true })
48 | expect(output.props.className).toEqual('new-todo')
49 | })
50 |
51 | it('should update value on change', () => {
52 | const { output, renderer } = setup()
53 | output.props.onChange({ target: { value: 'Use Radox' } })
54 | const updated = renderer.getRenderOutput()
55 | expect(updated.props.value).toEqual('Use Radox')
56 | })
57 |
58 | it('should call onSave on return key press', () => {
59 | const { output, props } = setup()
60 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } })
61 | expect(props.onSave).toHaveBeenCalledWith('Use Redux')
62 | })
63 |
64 | it('should reset state on return key press if newTodo', () => {
65 | const { output, renderer } = setup({ newTodo: true })
66 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } })
67 | const updated = renderer.getRenderOutput()
68 | expect(updated.props.value).toEqual('')
69 | })
70 |
71 | it('should call onSave on blur', () => {
72 | const { output, props } = setup()
73 | output.props.onBlur({ target: { value: 'Use Redux' } })
74 | expect(props.onSave).toHaveBeenCalledWith('Use Redux')
75 | })
76 |
77 | it('shouldnt call onSave on blur if newTodo', () => {
78 | const { output, props } = setup({ newTodo: true })
79 | output.props.onBlur({ target: { value: 'Use Redux' } })
80 | expect(props.onSave.calls.length).toBe(0)
81 | })
82 | })
83 | })
84 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/test/reducers/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import todos from '../../reducers/todos'
3 | import * as types from '../../constants/ActionTypes'
4 |
5 | describe('todos reducer', () => {
6 | it('should handle initial state', () => {
7 | expect(
8 | todos(undefined, {})
9 | ).toEqual([
10 | {
11 | text: 'Use Redux',
12 | completed: false,
13 | id: 0
14 | }
15 | ])
16 | })
17 |
18 | it('should handle ADD_TODO', () => {
19 | expect(
20 | todos([], {
21 | type: types.ADD_TODO,
22 | text: 'Run the tests'
23 | })
24 | ).toEqual([
25 | {
26 | text: 'Run the tests',
27 | completed: false,
28 | id: 0
29 | }
30 | ])
31 |
32 | expect(
33 | todos([
34 | {
35 | text: 'Use Redux',
36 | completed: false,
37 | id: 0
38 | }
39 | ], {
40 | type: types.ADD_TODO,
41 | text: 'Run the tests'
42 | })
43 | ).toEqual([
44 | {
45 | text: 'Run the tests',
46 | completed: false,
47 | id: 1
48 | }, {
49 | text: 'Use Redux',
50 | completed: false,
51 | id: 0
52 | }
53 | ])
54 |
55 | expect(
56 | todos([
57 | {
58 | text: 'Run the tests',
59 | completed: false,
60 | id: 1
61 | }, {
62 | text: 'Use Redux',
63 | completed: false,
64 | id: 0
65 | }
66 | ], {
67 | type: types.ADD_TODO,
68 | text: 'Fix the tests'
69 | })
70 | ).toEqual([
71 | {
72 | text: 'Fix the tests',
73 | completed: false,
74 | id: 2
75 | }, {
76 | text: 'Run the tests',
77 | completed: false,
78 | id: 1
79 | }, {
80 | text: 'Use Redux',
81 | completed: false,
82 | id: 0
83 | }
84 | ])
85 | })
86 |
87 | it('should handle DELETE_TODO', () => {
88 | expect(
89 | todos([
90 | {
91 | text: 'Run the tests',
92 | completed: false,
93 | id: 1
94 | }, {
95 | text: 'Use Redux',
96 | completed: false,
97 | id: 0
98 | }
99 | ], {
100 | type: types.DELETE_TODO,
101 | id: 1
102 | })
103 | ).toEqual([
104 | {
105 | text: 'Use Redux',
106 | completed: false,
107 | id: 0
108 | }
109 | ])
110 | })
111 |
112 | it('should handle EDIT_TODO', () => {
113 | expect(
114 | todos([
115 | {
116 | text: 'Run the tests',
117 | completed: false,
118 | id: 1
119 | }, {
120 | text: 'Use Redux',
121 | completed: false,
122 | id: 0
123 | }
124 | ], {
125 | type: types.EDIT_TODO,
126 | text: 'Fix the tests',
127 | id: 1
128 | })
129 | ).toEqual([
130 | {
131 | text: 'Fix the tests',
132 | completed: false,
133 | id: 1
134 | }, {
135 | text: 'Use Redux',
136 | completed: false,
137 | id: 0
138 | }
139 | ])
140 | })
141 |
142 | it('should handle COMPLETE_TODO', () => {
143 | expect(
144 | todos([
145 | {
146 | text: 'Run the tests',
147 | completed: false,
148 | id: 1
149 | }, {
150 | text: 'Use Redux',
151 | completed: false,
152 | id: 0
153 | }
154 | ], {
155 | type: types.COMPLETE_TODO,
156 | id: 1
157 | })
158 | ).toEqual([
159 | {
160 | text: 'Run the tests',
161 | completed: true,
162 | id: 1
163 | }, {
164 | text: 'Use Redux',
165 | completed: false,
166 | id: 0
167 | }
168 | ])
169 | })
170 |
171 | it('should handle COMPLETE_ALL', () => {
172 | expect(
173 | todos([
174 | {
175 | text: 'Run the tests',
176 | completed: true,
177 | id: 1
178 | }, {
179 | text: 'Use Redux',
180 | completed: false,
181 | id: 0
182 | }
183 | ], {
184 | type: types.COMPLETE_ALL
185 | })
186 | ).toEqual([
187 | {
188 | text: 'Run the tests',
189 | completed: true,
190 | id: 1
191 | }, {
192 | text: 'Use Redux',
193 | completed: true,
194 | id: 0
195 | }
196 | ])
197 |
198 | // Unmark if all todos are currently completed
199 | expect(
200 | todos([
201 | {
202 | text: 'Run the tests',
203 | completed: true,
204 | id: 1
205 | }, {
206 | text: 'Use Redux',
207 | completed: true,
208 | id: 0
209 | }
210 | ], {
211 | type: types.COMPLETE_ALL
212 | })
213 | ).toEqual([
214 | {
215 | text: 'Run the tests',
216 | completed: false,
217 | id: 1
218 | }, {
219 | text: 'Use Redux',
220 | completed: false,
221 | id: 0
222 | }
223 | ])
224 | })
225 |
226 | it('should handle CLEAR_COMPLETED', () => {
227 | expect(
228 | todos([
229 | {
230 | text: 'Run the tests',
231 | completed: true,
232 | id: 1
233 | }, {
234 | text: 'Use Redux',
235 | completed: false,
236 | id: 0
237 | }
238 | ], {
239 | type: types.CLEAR_COMPLETED
240 | })
241 | ).toEqual([
242 | {
243 | text: 'Use Redux',
244 | completed: false,
245 | id: 0
246 | }
247 | ])
248 | })
249 |
250 | it('should not generate duplicate ids after CLEAR_COMPLETED', () => {
251 | expect(
252 | [
253 | {
254 | type: types.COMPLETE_TODO,
255 | id: 0
256 | }, {
257 | type: types.CLEAR_COMPLETED
258 | }, {
259 | type: types.ADD_TODO,
260 | text: 'Write more tests'
261 | }
262 | ].reduce(todos, [
263 | {
264 | id: 0,
265 | completed: false,
266 | text: 'Use Redux'
267 | }, {
268 | id: 1,
269 | completed: false,
270 | text: 'Write tests'
271 | }
272 | ])
273 | ).toEqual([
274 | {
275 | text: 'Write more tests',
276 | completed: false,
277 | id: 2
278 | }, {
279 | text: 'Write tests',
280 | completed: false,
281 | id: 1
282 | }
283 | ])
284 | })
285 | })
286 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/test/setup.js:
--------------------------------------------------------------------------------
1 | import { jsdom } from 'jsdom'
2 |
3 | global.document = jsdom('')
4 | global.window = document.defaultView
5 | global.navigator = global.window.navigator
6 |
--------------------------------------------------------------------------------
/redux-examples/todomvc/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.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 | test: /\.css?$/,
28 | loaders: [ 'style', 'raw' ],
29 | include: __dirname
30 | }]
31 | }
32 | }
33 |
34 |
35 | // When inside Redux repo, prefer src to compiled version.
36 | // You can safely delete these lines in your project.
37 | var reduxSrc = path.join(__dirname, '..', '..', 'src')
38 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules')
39 | var fs = require('fs')
40 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) {
41 | // Resolve Redux to source
42 | module.exports.resolve = { alias: { 'redux': reduxSrc } }
43 | // Compile Redux from source
44 | module.exports.module.loaders.push({
45 | test: /\.js$/,
46 | loaders: [ 'babel' ],
47 | include: reduxSrc
48 | })
49 | }
50 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/.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/todos-with-undo/actions.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO'
2 | export const COMPLETE_TODO = 'COMPLETE_TODO'
3 | export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
4 |
5 | export const VisibilityFilters = {
6 | SHOW_ALL: 'SHOW_ALL',
7 | SHOW_COMPLETED: 'SHOW_COMPLETED',
8 | SHOW_ACTIVE: 'SHOW_ACTIVE'
9 | }
10 |
11 | let nextTodoId = 0
12 |
13 | export function addTodo(text) {
14 | return {
15 | id: nextTodoId++,
16 | type: ADD_TODO,
17 | text
18 | }
19 | }
20 |
21 | export function completeTodo(id) {
22 | return { type: COMPLETE_TODO, id }
23 | }
24 |
25 | export function setVisibilityFilter(filter) {
26 | return { type: SET_VISIBILITY_FILTER, filter }
27 | }
28 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/components/AddTodo.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 |
3 | export default class AddTodo extends Component {
4 | handleSubmit(e) {
5 | e.preventDefault()
6 | const node = this.refs.input
7 | const text = node.value.trim()
8 | if (text) {
9 | this.props.onAddSubmit(text)
10 | node.value = ''
11 | }
12 | }
13 |
14 | render() {
15 | return (
16 |
17 |
23 |
24 | )
25 | }
26 | }
27 |
28 | AddTodo.propTypes = {
29 | onAddSubmit: PropTypes.func.isRequired
30 | }
31 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 |
3 | export default class Footer extends Component {
4 | renderFilter(filter, name) {
5 | if (filter === this.props.filter) {
6 | return name
7 | }
8 |
9 | return (
10 | {
11 | e.preventDefault()
12 | this.props.onFilterChange(filter)
13 | }}>
14 | {name}
15 |
16 | )
17 | }
18 |
19 | renderFilters() {
20 | return (
21 |
22 | Show:
23 | {' '}
24 | {this.renderFilter('SHOW_ALL', 'All')}
25 | {', '}
26 | {this.renderFilter('SHOW_COMPLETED', 'Completed')}
27 | {', '}
28 | {this.renderFilter('SHOW_ACTIVE', 'Active')}
29 | .
30 |
31 | )
32 | }
33 |
34 | renderUndo() {
35 | return (
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
43 | render() {
44 | return (
45 |
46 | {this.renderFilters()}
47 | {this.renderUndo()}
48 |
49 | )
50 | }
51 | }
52 |
53 | Footer.propTypes = {
54 | onFilterChange: PropTypes.func.isRequired,
55 | onUndo: PropTypes.func.isRequired,
56 | onRedo: PropTypes.func.isRequired,
57 | undoDisabled: PropTypes.bool.isRequired,
58 | redoDisabled: PropTypes.bool.isRequired,
59 | filter: PropTypes.oneOf([
60 | 'SHOW_ALL',
61 | 'SHOW_COMPLETED',
62 | 'SHOW_ACTIVE'
63 | ]).isRequired
64 | }
65 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 |
3 | export default class Todo extends Component {
4 | render() {
5 | return (
6 |
12 | {this.props.text}
13 |
14 | )
15 | }
16 | }
17 |
18 | Todo.propTypes = {
19 | onClick: PropTypes.func.isRequired,
20 | text: PropTypes.string.isRequired,
21 | completed: PropTypes.bool.isRequired
22 | }
23 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/components/TodoList.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import Todo from './Todo'
3 |
4 | export default class TodoList extends Component {
5 | render() {
6 | return (
7 |
8 | {this.props.todos.map(todo =>
9 | this.props.onTodoClick(todo.id)} />
12 | )}
13 |
14 | )
15 | }
16 | }
17 |
18 | TodoList.propTypes = {
19 | onTodoClick: PropTypes.func.isRequired,
20 | todos: PropTypes.arrayOf(PropTypes.shape({
21 | text: PropTypes.string.isRequired,
22 | completed: PropTypes.bool.isRequired
23 | }).isRequired).isRequired
24 | }
25 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { connect } from 'react-redux'
3 | import { ActionCreators } from 'redux-undo'
4 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'
5 | import AddTodo from '../components/AddTodo'
6 | import TodoList from '../components/TodoList'
7 | import Footer from '../components/Footer'
8 |
9 | class App extends Component {
10 | render() {
11 | const { dispatch, visibleTodos, visibilityFilter } = this.props
12 | return (
13 |
14 |
dispatch(addTodo(text))} />
16 | dispatch(completeTodo(id))} />
19 |
27 | )
28 | }
29 | }
30 |
31 | App.propTypes = {
32 | dispatch: PropTypes.func.isRequired,
33 | visibleTodos: PropTypes.arrayOf(PropTypes.shape({
34 | text: PropTypes.string.isRequired,
35 | completed: PropTypes.bool.isRequired
36 | }).isRequired).isRequired,
37 | visibilityFilter: PropTypes.oneOf([
38 | 'SHOW_ALL',
39 | 'SHOW_COMPLETED',
40 | 'SHOW_ACTIVE'
41 | ]).isRequired,
42 | undoDisabled: PropTypes.bool.isRequired,
43 | redoDisabled: PropTypes.bool.isRequired
44 | }
45 |
46 | function selectTodos(todos, filter) {
47 | switch (filter) {
48 | default:
49 | case VisibilityFilters.SHOW_ALL:
50 | return todos
51 | case VisibilityFilters.SHOW_COMPLETED:
52 | return todos.filter(todo => todo.completed)
53 | case VisibilityFilters.SHOW_ACTIVE:
54 | return todos.filter(todo => !todo.completed)
55 | }
56 | }
57 |
58 | function select(state) {
59 | return {
60 | undoDisabled: state.todos.past.length === 0,
61 | redoDisabled: state.todos.future.length === 0,
62 | visibleTodos: selectTodos(state.todos.present, state.visibilityFilter),
63 | visibilityFilter: state.visibilityFilter
64 | }
65 | }
66 |
67 | export default connect(select)(App)
68 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux todos with undo example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/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 './containers/App'
6 | import todoApp from './reducers'
7 |
8 | const store = createStore(todoApp)
9 |
10 | const rootElement = document.getElementById('root')
11 | render(
12 |
13 |
14 | ,
15 | rootElement
16 | )
17 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-todos-with-undo-example",
3 | "version": "0.0.0",
4 | "description": "Redux todos with undo 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.0",
22 | "redux-thunk": "^0.1.0",
23 | "redux-undo": "^0.5.0"
24 | },
25 | "devDependencies": {
26 | "babel-core": "^5.6.18",
27 | "babel-loader": "^5.1.4",
28 | "babel-plugin-react-transform": "^1.1.0",
29 | "expect": "^1.6.0",
30 | "express": "^4.13.3",
31 | "node-libs-browser": "^0.5.2",
32 | "react-transform-hmr": "^1.0.0",
33 | "webpack": "^1.9.11",
34 | "webpack-dev-middleware": "^1.2.0",
35 | "webpack-hot-middleware": "^2.2.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import undoable, { distinctState } from 'redux-undo'
3 |
4 | import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'
5 | const { SHOW_ALL } = VisibilityFilters
6 |
7 | function visibilityFilter(state = SHOW_ALL, action) {
8 | switch (action.type) {
9 | case SET_VISIBILITY_FILTER:
10 | return action.filter
11 | default:
12 | return state
13 | }
14 | }
15 |
16 | function todo(state, action) {
17 | switch (action.type) {
18 | case ADD_TODO:
19 | return {
20 | id: action.id,
21 | text: action.text,
22 | completed: false
23 | }
24 | case COMPLETE_TODO:
25 | if (state.id !== action.id) {
26 | return state
27 | }
28 | return {
29 | ...state,
30 | completed: true
31 | }
32 | default:
33 | return state
34 | }
35 | }
36 |
37 | function todos(state = [], action) {
38 | switch (action.type) {
39 | case ADD_TODO:
40 | return [
41 | ...state,
42 | todo(undefined, action)
43 | ]
44 | case COMPLETE_TODO:
45 | return state.map(t =>
46 | todo(t, action)
47 | )
48 | default:
49 | return state
50 | }
51 | }
52 |
53 | const todoApp = combineReducers({
54 | visibilityFilter,
55 | todos: undoable(todos, { filter: distinctState() })
56 | })
57 |
58 | export default todoApp
59 |
--------------------------------------------------------------------------------
/redux-examples/todos-with-undo/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/todos-with-undo/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/universal/client/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 configureStore from '../common/store/configureStore'
6 | import App from '../common/containers/App'
7 |
8 | const initialState = window.__INITIAL_STATE__
9 | const store = configureStore(initialState)
10 | const rootElement = document.getElementById('app')
11 |
12 | render(
13 |
14 |
15 | ,
16 | rootElement
17 | )
18 |
--------------------------------------------------------------------------------
/redux-examples/universal/common/actions/counter.js:
--------------------------------------------------------------------------------
1 | export const SET_COUNTER = 'SET_COUNTER'
2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'
3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'
4 |
5 | export function set(value) {
6 | return {
7 | type: SET_COUNTER,
8 | payload: value
9 | }
10 | }
11 |
12 | export function increment() {
13 | return {
14 | type: INCREMENT_COUNTER
15 | }
16 | }
17 |
18 | export function decrement() {
19 | return {
20 | type: DECREMENT_COUNTER
21 | }
22 | }
23 |
24 | export function incrementIfOdd() {
25 | return (dispatch, getState) => {
26 | const { counter } = getState()
27 |
28 | if (counter % 2 === 0) {
29 | return
30 | }
31 |
32 | dispatch(increment())
33 | }
34 | }
35 |
36 | export function incrementAsync(delay = 1000) {
37 | return dispatch => {
38 | setTimeout(() => {
39 | dispatch(increment())
40 | }, delay)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/redux-examples/universal/common/api/counter.js:
--------------------------------------------------------------------------------
1 | function getRandomInt(min, max) {
2 | return Math.floor(Math.random() * (max - min)) + min
3 | }
4 |
5 | export function fetchCounter(callback) {
6 | // Rather than immediately returning, we delay our code with a timeout to simulate asynchronous behavior
7 | setTimeout(() => {
8 | callback(getRandomInt(1, 100))
9 | }, 500)
10 |
11 | // In the case of a real world API call, you'll normally run into a Promise like this:
12 | // API.getUser().then(user => callback(user))
13 | }
14 |
--------------------------------------------------------------------------------
/redux-examples/universal/common/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 |
3 | class Counter extends Component {
4 | render() {
5 | const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props
6 | return (
7 |
8 | Clicked: {counter} times
9 | {' '}
10 |
11 | {' '}
12 |
13 | {' '}
14 |
15 | {' '}
16 |
17 |
18 | )
19 | }
20 | }
21 |
22 | Counter.propTypes = {
23 | increment: PropTypes.func.isRequired,
24 | incrementIfOdd: PropTypes.func.isRequired,
25 | incrementAsync: PropTypes.func.isRequired,
26 | decrement: PropTypes.func.isRequired,
27 | counter: PropTypes.number.isRequired
28 | }
29 |
30 | export default Counter
31 |
--------------------------------------------------------------------------------
/redux-examples/universal/common/containers/App.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux'
2 | import { connect } from 'react-redux'
3 | import Counter from '../components/Counter'
4 | import * as CounterActions from '../actions/counter'
5 |
6 | function mapStateToProps(state) {
7 | return {
8 | counter: state.counter
9 | }
10 | }
11 |
12 | function mapDispatchToProps(dispatch) {
13 | return bindActionCreators(CounterActions, dispatch)
14 | }
15 |
16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter)
17 |
--------------------------------------------------------------------------------
/redux-examples/universal/common/reducers/counter.js:
--------------------------------------------------------------------------------
1 | import { SET_COUNTER, INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter'
2 |
3 | export default function counter(state = 0, action) {
4 | switch (action.type) {
5 | case SET_COUNTER:
6 | return action.payload
7 | case INCREMENT_COUNTER:
8 | return state + 1
9 | case DECREMENT_COUNTER:
10 | return state - 1
11 | default:
12 | return state
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/redux-examples/universal/common/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import counter from './counter'
3 |
4 | const rootReducer = combineReducers({
5 | counter
6 | })
7 |
8 | export default rootReducer
9 |
--------------------------------------------------------------------------------
/redux-examples/universal/common/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import rootReducer from '../reducers'
4 |
5 | const createStoreWithMiddleware = applyMiddleware(
6 | thunk
7 | )(createStore)
8 |
9 | export default function configureStore(initialState) {
10 | const store = createStoreWithMiddleware(rootReducer, initialState)
11 |
12 | if (module.hot) {
13 | // Enable Webpack hot module replacement for reducers
14 | module.hot.accept('../reducers', () => {
15 | const nextRootReducer = require('../reducers')
16 | store.replaceReducer(nextRootReducer)
17 | })
18 | }
19 |
20 | return store
21 | }
22 |
--------------------------------------------------------------------------------
/redux-examples/universal/index.js:
--------------------------------------------------------------------------------
1 | require('./client')
2 |
--------------------------------------------------------------------------------
/redux-examples/universal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-universal-example",
3 | "version": "0.0.0",
4 | "description": "An example of a universally-rendered Redux application",
5 | "scripts": {
6 | "start": "node server/index.js"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+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 | "babel": "^5.8.21",
19 | "express": "^4.13.3",
20 | "qs": "^4.0.0",
21 | "react": "^0.14.0",
22 | "react-dom": "^0.14.0",
23 | "react-redux": "^4.0.0",
24 | "redux": "^3.0.0",
25 | "redux-thunk": "^0.1.0",
26 | "serve-static": "^1.10.0"
27 | },
28 | "devDependencies": {
29 | "babel-core": "^5.8.22",
30 | "babel-loader": "^5.3.2",
31 | "babel-plugin-react-transform": "^1.1.0",
32 | "babel-runtime": "^5.8.20",
33 | "react-transform-hmr": "^1.0.0",
34 | "webpack": "^1.11.0",
35 | "webpack-dev-middleware": "^1.2.0",
36 | "webpack-hot-middleware": "^2.2.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/redux-examples/universal/server/index.js:
--------------------------------------------------------------------------------
1 | require('babel/register')
2 | require('./server')
3 |
--------------------------------------------------------------------------------
/redux-examples/universal/server/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, no-use-before-define */
2 |
3 | import path from 'path'
4 | import Express from 'express'
5 | import qs from 'qs'
6 |
7 | import webpack from 'webpack'
8 | import webpackDevMiddleware from 'webpack-dev-middleware'
9 | import webpackHotMiddleware from 'webpack-hot-middleware'
10 | import webpackConfig from '../webpack.config'
11 |
12 | import React from 'react'
13 | import { renderToString } from 'react-dom/server'
14 | import { Provider } from 'react-redux'
15 |
16 | import configureStore from '../common/store/configureStore'
17 | import App from '../common/containers/App'
18 | import { fetchCounter } from '../common/api/counter'
19 |
20 | const app = new Express()
21 | const port = 3000
22 |
23 | // Use this middleware to set up hot module reloading via webpack.
24 | const compiler = webpack(webpackConfig)
25 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath }))
26 | app.use(webpackHotMiddleware(compiler))
27 |
28 | // This is fired every time the server side receives a request
29 | app.use(handleRender)
30 |
31 | function handleRender(req, res) {
32 | // Query our mock API asynchronously
33 | fetchCounter(apiResult => {
34 | // Read the counter from the request, if provided
35 | const params = qs.parse(req.query)
36 | const counter = parseInt(params.counter, 10) || apiResult || 0
37 |
38 | // Compile an initial state
39 | const initialState = { counter }
40 |
41 | // Create a new Redux store instance
42 | const store = configureStore(initialState)
43 |
44 | // Render the component to a string
45 | const html = renderToString(
46 |
47 |
48 |
49 | )
50 |
51 | // Grab the initial state from our Redux store
52 | const finalState = store.getState()
53 |
54 | // Send the rendered page back to the client
55 | res.send(renderFullPage(html, finalState))
56 | })
57 | }
58 |
59 | function renderFullPage(html, initialState) {
60 | return `
61 |
62 |
63 |
64 | Redux Universal Example
65 |
66 |
67 | ${html}
68 |
71 |
72 |
73 |
74 | `
75 | }
76 |
77 | app.listen(port, (error) => {
78 | if (error) {
79 | console.error(error)
80 | } else {
81 | console.info(`==> 🌎 Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`)
82 | }
83 | })
84 |
--------------------------------------------------------------------------------
/redux-examples/universal/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'inline-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './client/index.js'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.NoErrorsPlugin()
19 | ],
20 | module: {
21 | loaders: [
22 | {
23 | test: /\.js$/,
24 | loader: 'babel',
25 | exclude: /node_modules/,
26 | include: __dirname,
27 | query: {
28 | optional: [ 'runtime' ],
29 | stage: 2,
30 | env: {
31 | development: {
32 | plugins: [
33 | 'react-transform'
34 | ],
35 | extra: {
36 | 'react-transform': {
37 | transforms: [
38 | {
39 | transform: 'react-transform-hmr',
40 | imports: [ 'react' ],
41 | locals: [ 'module' ]
42 | }
43 | ]
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | ]
51 | }
52 | }
53 |
54 | // When inside Redux repo, prefer src to compiled version.
55 | // You can safely delete these lines in your project.
56 | var reduxSrc = path.join(__dirname, '..', '..', 'src')
57 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules')
58 | var fs = require('fs')
59 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) {
60 | // Resolve Redux to source
61 | module.exports.resolve = { alias: { 'redux': reduxSrc } }
62 | // Compile Redux from source
63 | module.exports.module.loaders.push({
64 | test: /\.js$/,
65 | loaders: [ 'babel' ],
66 | include: reduxSrc
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0,
3 | "env": {
4 | // this plugin will be included only in development mode, e.g.
5 | // if NODE_ENV (or BABEL_ENV) environment variable is not set
6 | // or is equal to "development"
7 | "development/client": {
8 | "plugins": [
9 | // Include babel-plugin-react-display-name if you’re
10 | // using React.createClass() *before* react-transform:
11 | // "react-display-name",
12 | "react-transform"
13 | ],
14 | "extra": {
15 | // must be defined and be an array
16 | "react-transform": {
17 | "transforms": [{
18 | // can be an NPM module name or a local path
19 | "transform": "react-transform-hmr",
20 | // see specific transform's docs for "imports" and "locals" it needs
21 | "imports": ["react"],
22 | "locals": ["module"]
23 | }, {
24 | // you can have many transforms, not just one
25 | "transform": "react-transform-catch-errors",
26 | "imports": ["react", "redbox-react"]
27 | }]
28 | }
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.{json,js,yml}]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [.eslintrc]
15 | indent_style = space
16 | indent_size = 2
17 |
18 | [*.md]
19 | trim_trailing_whitespace = false
20 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "parser": "babel-eslint",
4 | "ecmaFeatures": {
5 | "jsx": true
6 | },
7 | "env": {
8 | "browser": true,
9 | "mocha": true,
10 | "node": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules
4 | coverage
5 | dist
6 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - "4.0.0"
5 | script:
6 | - "npm run lint"
7 | - "npm run test:cov"
8 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls"
9 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.1.0 (2015/x/x)
2 |
3 | #### Features
4 |
5 | - **A:**
6 | - **B:**
7 |
8 | #### Bugs Fixed
9 |
10 | - **C:**
11 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Daniel Bugl (https://github.com/omnidan)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/README.md:
--------------------------------------------------------------------------------
1 | # redux-undo-boilerplate
2 |
3 |  [](https://travis-ci.org/omnidan/redux-undo-boilerplate) [](https://david-dm.org/omnidan/redux-undo-boilerplate) [](https://david-dm.org/omnidan/redux-undo-boilerplate#info=devDependencies) [](http://standardjs.com/) [](https://gratipay.com/omnidan/)
4 |
5 | _a magical boilerplate with [hot reloading](#what-happens-if-i-change-some-code) and [awesome error handling™](#what-happens-if-i-make-a-typo--syntax-error) that uses [webpack](https://github.com/webpack/webpack), [redux](https://github.com/rackt/redux), [react](https://github.com/facebook/react) and [redux-undo](https://github.com/omnidan/redux-undo)_
6 |
7 |
8 | ## Installation
9 |
10 | You need to have `npm` installed (it comes with [node.js](https://nodejs.org/)).
11 |
12 | ```sh
13 | npm install
14 | ```
15 |
16 |
17 | ## Running
18 |
19 | During development, run:
20 |
21 | ```sh
22 | npm run dev
23 | ```
24 |
25 | Which enables some development tools.
26 |
27 | In production, run:
28 |
29 | ```sh
30 | npm start
31 | ```
32 |
33 | These commands (unless configured otherwise) start a web server at: [http://localhost:3000](http://localhost:3000)
34 |
35 |
36 | ## Demo
37 |
38 | [](https://i.imgur.com/M2KR4uo.gif)
39 |
40 | ### What happens if I change some code?
41 |
42 | Save the file in your editor and immediately see the changes reflected in your
43 | browser - coding has never been more efficient. What a beautiful world we live
44 | in nowadays.
45 |
46 | [](http://i.imgur.com/VCxUA2b.gif)
47 |
48 | ### What happens if I make a typo / syntax error?
49 |
50 | Many of us know this: You accidentally type in the wrong window once, add a
51 | random character to your code and when you run it again you're like "WTF this
52 | just worked?!" - let `webpack-hot-middleware` help you out with this:
53 |
54 | [](http://i.imgur.com/DTnGNFE.gif)
55 |
56 | ### What happens if I mutate the state directly?
57 |
58 | Mutating the state directly causes lots of bugs with Redux. There are no
59 | immutables in JavaScript, so we can't make sure this doesn't happen unless we
60 | use something like [Immutable.js](https://facebook.github.io/immutable-js/).
61 |
62 | If you run this boilerplate in dev mode (`npm run dev`), it will tell you when
63 | you [mutate something directly](https://github.com/omnidan/redux-undo-boilerplate/blob/master/src/reducers/counter.js#L9):
64 |
65 | [](https://i.imgur.com/y02EDxc.png)
66 |
67 |
68 | ## Testing
69 |
70 | ```sh
71 | npm test
72 | ```
73 |
74 |
75 | ## Thanks
76 |
77 | Special thanks to these awesome projects/people making this possible :heart:
78 |
79 | * [React](https://facebook.github.io/react/)
80 | * [Redux](https://rackt.github.io/redux/)
81 | * [Babel](https://babeljs.io/) - for ES6 support
82 | * [redux-boilerplate](https://github.com/chentsulin/redux-boilerplate) by [chentsulin](https://github.com/chentsulin) - this boilerplate is based off his project
83 | * [babel-plugin-react-transform](https://github.com/gaearon/babel-plugin-react-transform) by [gaearon](https://github.com/gaearon) - as a base for the hot reloading and error handling
84 | * [react-transform-catch-errors](https://github.com/gaearon/react-transform-catch-errors) by [gaearon](https://github.com/gaearon) - error handling
85 | * [react-transform-hmr](https://github.com/gaearon/react-transform-hmr) by [gaearon](https://github.com/gaearon) - hot reloading
86 | * [redux-devtools](https://github.com/gaearon/redux-devtools) by [gaearon](https://github.com/gaearon)
87 | * [redux-immutable-state-invariant](https://github.com/leoasis/redux-immutable-state-invariant) by [leoasis](https://github.com/leoasis) - detect state mutations
88 |
89 |
90 | ## License
91 |
92 | redux-boilerplate: MIT © [C.T. Lin](https://github.com/chentsulin)
93 |
94 | redux-undo-boilerplate: MIT © [Daniel Bugl](https://github.com/omnidan)
95 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Redux counter example
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-undo-boilerplate",
3 | "version": "0.5.0",
4 | "description": "a magical boilerplate with hot reloading and awesome error handling™ that uses webpack, redux, react and redux-undo",
5 | "main": "lib/index.js",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/omnidan/redux-undo-boilerplate.git"
10 | },
11 | "scripts": {
12 | "start": "node server.js",
13 | "dev": "DEBUG=true node server.js",
14 | "clean": "rimraf lib dist coverage",
15 | "lint": "eslint src test",
16 | "test": "mocha --compilers js:babel/register --recursive",
17 | "test:watch": "npm test -- --watch",
18 | "test:cov": "babel-node $(npm bin)/isparta cover $(npm bin)/_mocha -- --recursive",
19 | "check": "npm run lint && npm run test",
20 | "build": " ",
21 | "preversion": "npm run clean && npm run check",
22 | "version": "npm run build",
23 | "postversion": "git push && git push --tags && npm run clean",
24 | "prepublish": "npm run clean && npm run build"
25 | },
26 | "author": "Daniel Bugl (https://github.com/omnidan)",
27 | "engines": {
28 | "node": ">=0.10.0"
29 | },
30 | "keywords": [
31 | "redux",
32 | "react",
33 | "redux-undo",
34 | "boilerplate",
35 | "redux-boilerplate",
36 | "redux-undo-boilerplate",
37 | "predictable",
38 | "functional",
39 | "immutable",
40 | "hot",
41 | "live",
42 | "replay",
43 | "undo",
44 | "redo",
45 | "time travel",
46 | "flux"
47 | ],
48 | "devDependencies": {
49 | "babel": "^5.8.23",
50 | "babel-core": "^5.8.25",
51 | "babel-eslint": "^4.1.3",
52 | "babel-loader": "^5.3.2",
53 | "babel-plugin-react-transform": "^1.1.1",
54 | "chai": "^3.4.0",
55 | "eslint": "^1.7.3",
56 | "eslint-config-airbnb": "0.1.0",
57 | "eslint-plugin-react": "^3.6.3",
58 | "express": "^4.13.3",
59 | "history": "^1.12.5",
60 | "isparta": "^3.1.0",
61 | "jsdom": "^7.0.2",
62 | "mocha": "*",
63 | "mocha-jsdom": "^1.0.0",
64 | "morgan": "^1.6.1",
65 | "proxyquire": "^1.7.3",
66 | "react-addons-test-utils": "^0.14.0",
67 | "react-hot-loader": "^1.3.0",
68 | "react-transform-catch-errors": "^1.0.0",
69 | "react-transform-hmr": "^1.0.1",
70 | "redbox-react": "^1.1.1",
71 | "redux-devtools": "^3.0.0-beta-3",
72 | "redux-devtools-dock-monitor": "^1.0.0-beta-3",
73 | "redux-devtools-log-monitor": "^1.0.0-beta-3",
74 | "redux-immutable-state-invariant": "^1.1.1",
75 | "rimraf": "^2.4.3",
76 | "sinon": "^1.17.2",
77 | "webpack": "^1.12.2",
78 | "webpack-dev-middleware": "^1.2.0",
79 | "webpack-hot-middleware": "^2.4.1"
80 | },
81 | "dependencies": {
82 | "react": "^0.14.0",
83 | "react-dom": "^0.14.0",
84 | "react-redux": "^4.0.0",
85 | "react-router": "^1.0.0-rc3",
86 | "redux": "^3.0.4",
87 | "redux-logger": "^2.0.4",
88 | "redux-promise": "^0.5.0",
89 | "redux-thunk": "^1.0.0",
90 | "redux-undo": "^0.5.0"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/server.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 |
3 | var express = require('express');
4 |
5 | var app = express();
6 |
7 | app.use(require('morgan')('short'));
8 |
9 | (function initWebpack() {
10 | var webpack = require('webpack');
11 | var webpackConfig = require('./webpack.config');
12 | var compiler = webpack(webpackConfig);
13 |
14 | app.use(require('webpack-dev-middleware')(compiler, {
15 | noInfo: true, publicPath: webpackConfig.output.publicPath
16 | }));
17 |
18 | app.use(require('webpack-hot-middleware')(compiler, {
19 | log: console.log, path: '/__webpack_hmr', heartbeat: 10 * 1000
20 | }));
21 | })();
22 |
23 | app.get('/', function root(req, res) {
24 | res.sendFile(__dirname + '/index.html');
25 | });
26 |
27 | if (require.main === module) {
28 | var server = http.createServer(app);
29 | server.listen(process.env.PORT || 3000, function onListen() {
30 | var address = server.address();
31 | console.log('Listening on: %j', address);
32 | console.log(' -> that probably means: http://localhost:%d', address.port);
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/actions/counter.js:
--------------------------------------------------------------------------------
1 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'
2 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'
3 |
4 | export const UNDO_COUNTER = 'UNDO_COUNTER'
5 | export const REDO_COUNTER = 'REDO_COUNTER'
6 |
7 | export function increment () {
8 | return {
9 | type: INCREMENT_COUNTER
10 | }
11 | }
12 |
13 | export function decrement () {
14 | return {
15 | type: DECREMENT_COUNTER
16 | }
17 | }
18 |
19 | export function undo () {
20 | return {
21 | type: UNDO_COUNTER
22 | }
23 | }
24 |
25 | export function redo () {
26 | return {
27 | type: REDO_COUNTER
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/components/Counter.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable*/
2 | import React, { Component, PropTypes } from 'react'
3 | /*eslint-enable*/
4 |
5 | export default class Counter extends Component {
6 |
7 | static propTypes = {
8 | increment: PropTypes.func.isRequired,
9 | decrement: PropTypes.func.isRequired,
10 | undo: PropTypes.func.isRequired,
11 | redo: PropTypes.func.isRequired,
12 | count: PropTypes.number.isRequired
13 | }
14 |
15 | render () {
16 | const { increment, decrement, count, undo, redo } = this.props
17 | return (
18 |
19 | Clicked: {count} times
20 | {' '}
21 |
22 | {' '}
23 |
24 | {' '}
25 |
26 | {' '}
27 |
28 |
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/components/Main.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 |
3 | export default class Main extends Component {
4 |
5 | static propTypes = {
6 | children: PropTypes.any.isRequired
7 | }
8 |
9 | render () {
10 | return (
11 |
12 | {/* this will render the child routes */}
13 | {React.cloneElement(this.props.children, this.props)}
14 |
15 | )
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import Main from '../components/Main'
3 |
4 | function mapStateToProps (/* state */) {
5 | return {}
6 | }
7 |
8 | export default connect(mapStateToProps)(Main)
9 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/containers/CounterPage.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 | function mapStateToProps (state) {
7 | return {
8 | count: state.counter.present.count
9 | }
10 | }
11 |
12 | function mapDispatchToProps (dispatch) {
13 | return bindActionCreators(CounterActions, dispatch)
14 | }
15 |
16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter)
17 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/containers/DevTools.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable*/
2 | import React from 'react'
3 | import { createDevTools } from 'redux-devtools'
4 | import LogMonitor from 'redux-devtools-log-monitor'
5 | import DockMonitor from 'redux-devtools-dock-monitor'
6 | /*eslint-enable*/
7 |
8 | export default createDevTools(
9 |
11 |
12 |
13 | )
14 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/containers/Root.js:
--------------------------------------------------------------------------------
1 | /* global __DEVTOOLS__ */
2 | /*eslint-disable*/
3 | import React, { Component, PropTypes } from 'react'
4 | import { Provider } from 'react-redux'
5 | import { Router } from 'react-router'
6 | import configureStore from '../store/configureStore'
7 | import routes from '../routes'
8 | /*eslint-enable*/
9 |
10 | const store = configureStore()
11 |
12 | function createElements (history) {
13 | const elements = [
14 |
15 | ]
16 |
17 | if (typeof __DEVTOOLS__ !== 'undefined' && __DEVTOOLS__) {
18 | /*eslint-disable*/
19 | const DevTools = require('./DevTools')
20 | /*eslint-enable*/
21 | elements.push()
22 | }
23 |
24 | return elements
25 | }
26 |
27 | export default class Root extends Component {
28 |
29 | static propTypes = {
30 | history: PropTypes.object.isRequired
31 | }
32 |
33 | render () {
34 | return (
35 |
36 |
37 | {createElements(this.props.history)}
38 |
39 |
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/containers/index.js:
--------------------------------------------------------------------------------
1 | export { default as Root } from './Root'
2 | export { default as App } from './App'
3 | export { default as CounterPage } from './CounterPage'
4 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/index.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable*/
2 | import React from 'react'
3 | import ReactDOM from 'react-dom'
4 | import Root from './containers/Root'
5 | import createBrowserHistory from 'history/lib/createBrowserHistory'
6 | /*eslint-enable*/
7 |
8 | const history = createBrowserHistory()
9 |
10 | ReactDOM.render(
11 | ,
12 | document.getElementById('root')
13 | )
14 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/reducers/counter.js:
--------------------------------------------------------------------------------
1 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter'
2 |
3 | export default function counter (state = { count: 0 }, action) {
4 | switch (action.type) {
5 | case INCREMENT_COUNTER:
6 | // State mutations are bad, in dev mode, we detect them and throw an error.
7 | // Try it out by uncommenting the line below and running `npm run dev`!
8 | // state.mutation = true
9 | return { ...state, count: state.count + 1 }
10 | case DECREMENT_COUNTER:
11 | return { ...state, count: state.count - 1 }
12 | default:
13 | return state
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import counter from './counter'
3 | import {
4 | INCREMENT_COUNTER, DECREMENT_COUNTER,
5 | UNDO_COUNTER, REDO_COUNTER
6 | } from '../actions/counter'
7 | import undoable, { includeAction } from 'redux-undo'
8 |
9 | const rootReducer = combineReducers({
10 | counter: undoable(counter, {
11 | filter: includeAction([INCREMENT_COUNTER, DECREMENT_COUNTER]),
12 | limit: 10,
13 | debug: true,
14 | undoType: UNDO_COUNTER,
15 | redoType: REDO_COUNTER
16 | })
17 | })
18 |
19 | export default rootReducer
20 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/routes.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable*/
2 | import React from 'react'
3 | import { Route } from 'react-router'
4 | import App from './containers/App'
5 | import * as containers from './containers'
6 | /*eslint-enable*/
7 |
8 | const {
9 | CounterPage
10 | } = containers
11 |
12 | export default (
13 |
14 |
15 |
16 | )
17 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | /* global __DEVTOOLS__ */
2 | import { createStore, applyMiddleware, compose } from 'redux'
3 | // reducer
4 | import rootReducer from '../reducers'
5 | // middleware
6 | import thunkMiddleware from 'redux-thunk'
7 | import promiseMiddleware from 'redux-promise'
8 | import createLogger from 'redux-logger'
9 |
10 | const loggerMiddleware = createLogger({
11 | level: 'info',
12 | collapsed: true
13 | })
14 |
15 | const enforceImmutableMiddleware = require('redux-immutable-state-invariant')()
16 |
17 | let createStoreWithMiddleware
18 |
19 | if (typeof __DEVTOOLS__ !== 'undefined' && __DEVTOOLS__) {
20 | const { persistState } = require('redux-devtools')
21 | const DevTools = require('../containers/DevTools')
22 | createStoreWithMiddleware = compose(
23 | applyMiddleware(
24 | enforceImmutableMiddleware,
25 | thunkMiddleware,
26 | promiseMiddleware,
27 | loggerMiddleware
28 | ),
29 | DevTools.instrument(),
30 | persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
31 | )(createStore)
32 | } else {
33 | createStoreWithMiddleware = compose(
34 | applyMiddleware(thunkMiddleware, promiseMiddleware)
35 | )(createStore)
36 | }
37 |
38 | /**
39 | * Creates a preconfigured store.
40 | */
41 | export default function configureStore (initialState) {
42 | const store = createStoreWithMiddleware(rootReducer, initialState)
43 |
44 | if (module.hot) {
45 | // Enable Webpack hot module replacement for reducers
46 | module.hot.accept('../reducers', () => {
47 | const nextRootReducer = require('../reducers/index')
48 | store.replaceReducer(nextRootReducer)
49 | })
50 | }
51 |
52 | return store
53 | }
54 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/src/utils/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KingNigel/react-redux-tutorial/e283bec8a8ea26aad9a843fae862684c16168a5a/redux-undo-boilerplate/src/utils/.gitkeep
--------------------------------------------------------------------------------
/redux-undo-boilerplate/test/actions/counter.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-expressions: 0 */
2 | import { expect } from 'chai'
3 | import * as actions from '../../src/actions/counter'
4 |
5 | describe('actions', () => {
6 | it('increment should create increment action', () => {
7 | expect(actions.increment()).to.deep.equal({ type: actions.INCREMENT_COUNTER })
8 | })
9 |
10 | it('decrement should create decrement action', () => {
11 | expect(actions.decrement()).to.deep.equal({ type: actions.DECREMENT_COUNTER })
12 | })
13 |
14 | it('undo should create undo action', () => {
15 | expect(actions.undo()).to.deep.equal({ type: actions.UNDO_COUNTER })
16 | })
17 |
18 | it('redo should create redo action', () => {
19 | expect(actions.redo()).to.deep.equal({ type: actions.REDO_COUNTER })
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/test/components/Counter.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-expressions: 0 */
2 | import { expect } from 'chai'
3 | import { spy } from 'sinon'
4 | import jsdom from 'mocha-jsdom'
5 | /*eslint-disable*/
6 | import React from 'react'
7 | import TestUtils from 'react-addons-test-utils'
8 | import Counter from '../../src/components/Counter'
9 | /*eslint-enable*/
10 |
11 | function setup () {
12 | const actions = {
13 | increment: spy(),
14 | incrementIfOdd: spy(),
15 | incrementAsync: spy(),
16 | decrement: spy(),
17 | undo: spy(),
18 | redo: spy()
19 | }
20 | const component = TestUtils.renderIntoDocument()
21 | return {
22 | component: component,
23 | actions: actions,
24 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button').map(button => {
25 | return button
26 | }),
27 | p: TestUtils.findRenderedDOMComponentWithTag(component, 'p')
28 | }
29 | }
30 |
31 | describe('Counter component', () => {
32 | jsdom()
33 |
34 | it('should display count', () => {
35 | const { p } = setup()
36 | expect(p.textContent).to.match(/^Clicked: 1 times/)
37 | })
38 |
39 | it('first button should call increment', () => {
40 | const { buttons, actions } = setup()
41 | TestUtils.Simulate.click(buttons[0])
42 | expect(actions.increment.called).to.be.true
43 | })
44 |
45 | it('second button should call decrement', () => {
46 | const { buttons, actions } = setup()
47 | TestUtils.Simulate.click(buttons[1])
48 | expect(actions.decrement.called).to.be.true
49 | })
50 |
51 | it('third button should call undo', () => {
52 | const { buttons, actions } = setup()
53 | TestUtils.Simulate.click(buttons[2])
54 | expect(actions.undo.called).to.be.true
55 | })
56 |
57 | it('fourth button should call redo', () => {
58 | const { buttons, actions } = setup()
59 | TestUtils.Simulate.click(buttons[3])
60 | expect(actions.redo.called).to.be.true
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/test/containers/CounterPage.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import jsdom from 'mocha-jsdom'
3 | /*eslint-disable*/
4 | import React from 'react'
5 | import TestUtils from 'react-addons-test-utils'
6 | import { Provider } from 'react-redux'
7 | import CounterPage from '../../src/containers/CounterPage'
8 | import configureStore from '../../src/store/configureStore'
9 | /*eslint-enable*/
10 |
11 | function setup (initialState) {
12 | const store = configureStore(initialState)
13 | const app = TestUtils.renderIntoDocument(
14 |
15 |
16 |
17 | )
18 | return {
19 | app: app,
20 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(app, 'button').map(button => {
21 | return button
22 | }),
23 | p: TestUtils.findRenderedDOMComponentWithTag(app, 'p')
24 | }
25 | }
26 |
27 | describe('containers', () => {
28 | jsdom()
29 |
30 | describe('App', () => {
31 | it('should display initial count', () => {
32 | const { p } = setup()
33 | expect(p.textContent).to.match(/^Clicked: 0 times/)
34 | })
35 |
36 | it('should display updated count after increment button click', () => {
37 | const { buttons, p } = setup()
38 | TestUtils.Simulate.click(buttons[0])
39 | expect(p.textContent).to.match(/^Clicked: 1 times/)
40 | })
41 |
42 | it('should display updated count after descrement button click', () => {
43 | const { buttons, p } = setup()
44 | TestUtils.Simulate.click(buttons[1])
45 | expect(p.textContent).to.match(/^Clicked: -1 times/)
46 | })
47 |
48 | it('should undo increment action on undo button click', () => {
49 | const { buttons, p } = setup()
50 | TestUtils.Simulate.click(buttons[0])
51 | expect(p.textContent).to.match(/^Clicked: 1 times/)
52 | TestUtils.Simulate.click(buttons[2])
53 | expect(p.textContent).to.match(/^Clicked: 0 times/)
54 | })
55 |
56 | it('should redo after undo on redo button click', () => {
57 | const { buttons, p } = setup()
58 | TestUtils.Simulate.click(buttons[0])
59 | expect(p.textContent).to.match(/^Clicked: 1 times/)
60 | TestUtils.Simulate.click(buttons[2])
61 | expect(p.textContent).to.match(/^Clicked: 0 times/)
62 | TestUtils.Simulate.click(buttons[3])
63 | expect(p.textContent).to.match(/^Clicked: 1 times/)
64 | })
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/test/reducers/counter.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import counter from '../../src/reducers/counter'
3 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../src/actions/counter'
4 |
5 | describe('reducers', () => {
6 | describe('counter', () => {
7 | it('should handle initial state', () => {
8 | expect(counter(undefined, {}).count).to.equal(0)
9 | })
10 |
11 | const testState = { count: 1 }
12 |
13 | it('should handle INCREMENT_COUNTER', () => {
14 | expect(counter(testState, { type: INCREMENT_COUNTER }).count).to.equal(2)
15 | })
16 |
17 | it('should handle DECREMENT_COUNTER', () => {
18 | expect(counter(testState, { type: DECREMENT_COUNTER }).count).to.equal(0)
19 | })
20 |
21 | it('should handle unknown action type', () => {
22 | expect(counter(testState, { type: 'unknown' }).count).to.equal(1)
23 | })
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/redux-undo-boilerplate/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 | './src/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 | new webpack.DefinePlugin({
20 | __DEVTOOLS__: !!process.env.DEBUG
21 | })
22 | ],
23 | resolve: {
24 | extensions: ['', '.js']
25 | },
26 | module: {
27 | loaders: [{
28 | test: /\.js$/,
29 | loaders: ['react-hot', 'babel'],
30 | exclude: /node_modules/
31 | }]
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/redux-wilddog-todos/README.MD:
--------------------------------------------------------------------------------
1 | ###运行方法:
2 | npm install
3 | npm run build
4 | 手动打开index.html
5 |
6 | *如果您觉得本程序或者博客帮到了您,就赏颗星吧!*
--------------------------------------------------------------------------------
/redux-wilddog-todos/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | redux-wilddog-todos
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/redux-wilddog-todos/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 | "redux-logger": "^2.4.0",
17 | "redux-thunk": "^1.0.3",
18 | "webpack": "^1.12.10",
19 | "wilddog": "^0.5.0"
20 | },
21 | "scripts": {
22 | "build": "webpack --progress -colors --watch"
23 | },
24 | "author": "lewis617",
25 | "license": "ISC"
26 | }
27 |
--------------------------------------------------------------------------------
/redux-wilddog-todos/src/actions.js:
--------------------------------------------------------------------------------
1 | import Wilddog from 'wilddog/lib/wilddog-node'
2 | /*
3 | * action 类型
4 | */
5 | export const GET_TODO_ERROR = 'GET_TODO_ERROR';
6 | export const GET_TODO_OK = 'GET_TODO_OK';
7 | export const ADD_TODO_ERROR = 'ADD_TODO_ERROR';
8 | export const ADD_TODO_OK = 'ADD_TODO_OK';
9 | export const REMOVE_TODO_OK = 'REMOVE_TODO_OK';
10 | export const REMOVE_TODO_ERROR = 'REMOVE_TODO_ERROR';
11 |
12 | let wilddog=new Wilddog('https://redux-wilddog-todos.wilddogio.com')
13 |
14 | /*
15 | * action 创建函数
16 | */
17 | export function getTodo() {
18 | return (dispatch,getState)=>{
19 |
20 | wilddog.child('todos').once('value',(snapshot)=>{
21 | let obj=snapshot.val();
22 | let array=[];
23 | for(let key in obj){
24 | array.push({key:key,text:obj[key].text})
25 | }
26 | dispatch({
27 | type: GET_TODO_OK,
28 | payload: array
29 | })
30 | },(err)=>{
31 | dispatch({
32 | type: GET_TODO_ERROR,
33 | payload: err
34 | })
35 | });
36 |
37 |
38 | }
39 | }
40 |
41 | export function addTodo(text) {
42 | return (dispatch,getState)=>{
43 |
44 | wilddog.child('todos').push({
45 | text
46 | },(err)=>{
47 | if(err){dispatch({type:ADD_TODO_ERROR,payload:err})}
48 | });
49 |
50 |
51 |
52 | }
53 | }
54 |
55 | export function removeTodo(key) {
56 | return (dispatch,getState)=>{
57 |
58 | wilddog.child(`todos/${key}`).remove((err)=>{
59 | if(err)dispatch({type:REMOVE_TODO_ERROR,payload:err})
60 | });
61 |
62 |
63 | }
64 | }
65 |
66 |
67 | export function registerListeners() {
68 | return (dispatch, getState) => {
69 |
70 | wilddog.child('todos').on('child_removed', snapshot => {
71 | dispatch({
72 | type: REMOVE_TODO_OK,
73 | payload: snapshot.key()
74 | })
75 | });
76 |
77 | wilddog.child('todos').on('child_added', snapshot => dispatch({
78 | type: ADD_TODO_OK,
79 | payload: Object.assign({},snapshot.val(),{key:snapshot.key()})
80 | }));
81 |
82 |
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/redux-wilddog-todos/src/components/AddTodo.js:
--------------------------------------------------------------------------------
1 | import React, { findDOMNode, Component, PropTypes } from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | export default class AddTodo extends Component {
5 | render() {
6 | return (
7 |
8 |
9 |
12 |
13 | );
14 | }
15 |
16 | handleClick(e) {
17 | const node = ReactDOM.findDOMNode(this.refs.input);
18 | const text = node.value.trim();
19 | this.props.onAddClick(text);
20 | node.value = '';
21 | }
22 | }
23 |
24 | AddTodo.propTypes = {
25 | onAddClick: PropTypes.func.isRequired
26 | }
--------------------------------------------------------------------------------
/redux-wilddog-todos/src/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | export default class Todo extends Component {
4 | render() {
5 | return (
6 |
7 | {this.props.todo.text}
8 |
9 |
10 | );
11 | }
12 | }
13 |
14 | Todo.propTypes = {
15 | remove: PropTypes.func.isRequired,
16 | todo: PropTypes.object.isRequired
17 | };
--------------------------------------------------------------------------------
/redux-wilddog-todos/src/components/TodoList.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import Todo from './Todo';
3 |
4 | export default class TodoList extends Component {
5 | render() {
6 | return (
7 |
8 | {this.props.todos.map((todo, index) =>
9 |
12 | )}
13 |
14 | )
15 | }
16 | }
17 |
18 | TodoList.propTypes = {
19 | remove: PropTypes.func.isRequired,
20 | todos: PropTypes.arrayOf(PropTypes.shape({
21 | text: PropTypes.string.isRequired
22 | }).isRequired).isRequired
23 | }
--------------------------------------------------------------------------------
/redux-wilddog-todos/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { addTodo, removeTodo} from '../actions';
4 | import AddTodo from '../components/AddTodo';
5 | import TodoList from '../components/TodoList';
6 |
7 | class App extends Component {
8 | render() {
9 | const { dispatch ,todos} = this.props;
10 | return (
11 |
12 |
14 | dispatch(addTodo(text))
15 | } />
16 |
19 | dispatch(removeTodo(key))
20 | } />
21 |
22 | )
23 | }
24 | }
25 |
26 | App.propTypes = {
27 | todos: PropTypes.arrayOf(PropTypes.shape({
28 | text: PropTypes.string.isRequired
29 | }))
30 | }
31 |
32 | //将state.counter绑定到props的counter
33 | function mapStateToProps(state) {
34 | return {
35 | todos: state.todos
36 | }
37 | }
38 |
39 | export default connect(mapStateToProps)(App);
--------------------------------------------------------------------------------
/redux-wilddog-todos/src/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 createStore from './store'
6 | import { getTodo,registerListeners} from './actions'
7 |
8 | let store = createStore();
9 |
10 | store.dispatch(getTodo())
11 | store.dispatch(registerListeners())
12 |
13 | render(
14 |
15 |
16 | ,
17 | document.querySelector("#app")
18 | );
--------------------------------------------------------------------------------
/redux-wilddog-todos/src/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { ADD_TODO_OK, REMOVE_TODO_OK ,GET_TODO_OK} from './actions'
3 |
4 | function todos(state=[], action) {
5 | switch (action.type) {
6 | case GET_TODO_OK:
7 | return action.payload
8 | case ADD_TODO_OK:
9 | return [
10 | ...state,
11 | action.payload
12 | ]
13 | case REMOVE_TODO_OK:
14 | return state.filter((todo)=>todo.key!==action.payload
15 | )
16 | default:
17 | return state
18 | }
19 | }
20 |
21 | const todoApp = combineReducers({
22 | todos
23 | })
24 |
25 | export default todoApp
--------------------------------------------------------------------------------
/redux-wilddog-todos/src/store.js:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore ,compose} from 'redux';
2 | import thunk from 'redux-thunk';
3 | import createLogger from 'redux-logger'
4 | import reducers from './reducers';
5 |
6 |
7 | export default (initialState) => {
8 | const store = compose(
9 | applyMiddleware(
10 | thunk,
11 | createLogger()
12 | ),
13 | window.devToolsExtension ? window.devToolsExtension() : f => f
14 | )(createStore)(reducers, initialState);
15 |
16 | return store;
17 | };
--------------------------------------------------------------------------------
/redux-wilddog-todos/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 | // kills the compilation upon an error.
27 | // this keeps the outputed bundle **always** valid
28 | new webpack.NoErrorsPlugin(),
29 | //这个使用uglifyJs压缩你的js代码
30 | //new webpack.optimize.UglifyJsPlugin({minimize: true}),
31 | new webpack.optimize.CommonsChunkPlugin('vendors', 'vendors.js')
32 | ]
33 | };
34 |
--------------------------------------------------------------------------------
/todo-reflux/README.MD:
--------------------------------------------------------------------------------
1 | ###运行方法
2 | npm install
3 | npm run build
4 | 手动打开index.html
5 |
6 | *如果您觉得本程序或者博客帮到了您,就赏颗星吧!*
--------------------------------------------------------------------------------
/todo-reflux/dist/app.js:
--------------------------------------------------------------------------------
1 | webpackJsonp([0],{
2 |
3 | /***/ 0:
4 | /***/ function(module, exports, __webpack_require__) {
5 |
6 | 'use strict';
7 |
8 | var _react = __webpack_require__(1);
9 |
10 | var _react2 = _interopRequireDefault(_react);
11 |
12 | var _reactDom = __webpack_require__(158);
13 |
14 | var _reactDom2 = _interopRequireDefault(_reactDom);
15 |
16 | var _todo = __webpack_require__(159);
17 |
18 | var _todo2 = _interopRequireDefault(_todo);
19 |
20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21 |
22 | _reactDom2.default.render(_react2.default.createElement(_todo2.default, null), document.querySelector('#app'));
23 |
24 | /***/ },
25 |
26 | /***/ 158:
27 | /***/ function(module, exports, __webpack_require__) {
28 |
29 | 'use strict';
30 |
31 | module.exports = __webpack_require__(3);
32 |
33 |
34 | /***/ },
35 |
36 | /***/ 159:
37 | /***/ function(module, exports, __webpack_require__) {
38 |
39 | 'use strict';
40 |
41 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
42 |
43 | Object.defineProperty(exports, "__esModule", {
44 | value: true
45 | });
46 |
47 | var _react = __webpack_require__(1);
48 |
49 | var _react2 = _interopRequireDefault(_react);
50 |
51 | var _reflux = __webpack_require__(160);
52 |
53 | var _reflux2 = _interopRequireDefault(_reflux);
54 |
55 | var _reactMixin = __webpack_require__(179);
56 |
57 | var _reactMixin2 = _interopRequireDefault(_reactMixin);
58 |
59 | var _store = __webpack_require__(182);
60 |
61 | var _store2 = _interopRequireDefault(_store);
62 |
63 | var _actions = __webpack_require__(183);
64 |
65 | var _actions2 = _interopRequireDefault(_actions);
66 |
67 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
68 |
69 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
70 |
71 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
72 |
73 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
74 |
75 | var Todo = function (_React$Component) {
76 | _inherits(Todo, _React$Component);
77 |
78 | function Todo() {
79 | _classCallCheck(this, Todo);
80 |
81 | return _possibleConstructorReturn(this, Object.getPrototypeOf(Todo).apply(this, arguments));
82 | }
83 |
84 | _createClass(Todo, [{
85 | key: 'componentDidMount',
86 | value: function componentDidMount() {
87 | _actions2.default.getAll();
88 | }
89 | }, {
90 | key: 'add',
91 | value: function add() {
92 | var item = this.refs.item.value;
93 | this.refs.item.value = '';
94 | _actions2.default.add(item);
95 | }
96 | }, {
97 | key: 'remove',
98 | value: function remove(i) {
99 | _actions2.default.remove(i);
100 | }
101 | }, {
102 | key: 'render',
103 | value: function render() {
104 | var _this2 = this;
105 |
106 | var items = undefined;
107 | if (this.state.list) {
108 | items = this.state.list.map(function (item, i) {
109 | return _react2.default.createElement(
110 | 'li',
111 | { key: i },
112 | item.name,
113 | _react2.default.createElement(
114 | 'button',
115 | { onClick: _this2.remove.bind(_this2, i) },
116 | 'remove'
117 | )
118 | );
119 | });
120 | }
121 | return _react2.default.createElement(
122 | 'div',
123 | null,
124 | _react2.default.createElement('input', { type: 'text', ref: 'item' }),
125 | _react2.default.createElement(
126 | 'button',
127 | { onClick: this.add.bind(this) },
128 | 'add'
129 | ),
130 | _react2.default.createElement(
131 | 'ul',
132 | null,
133 | items
134 | )
135 | );
136 | }
137 | }]);
138 |
139 | return Todo;
140 | }(_react2.default.Component);
141 |
142 | // ES6 mixin写法
143 |
144 | exports.default = Todo;
145 | _reactMixin2.default.onClass(Todo, _reflux2.default.connect(_store2.default));
146 |
147 | /***/ },
148 |
149 | /***/ 182:
150 | /***/ function(module, exports, __webpack_require__) {
151 |
152 | 'use strict';
153 |
154 | Object.defineProperty(exports, "__esModule", {
155 | value: true
156 | });
157 |
158 | var _reflux = __webpack_require__(160);
159 |
160 | var _reflux2 = _interopRequireDefault(_reflux);
161 |
162 | var _actions = __webpack_require__(183);
163 |
164 | var _actions2 = _interopRequireDefault(_actions);
165 |
166 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
167 |
168 | Array.prototype.remove = function (dx) {
169 | if (isNaN(dx) || dx > this.length) {
170 | return false;
171 | }
172 | for (var i = 0, n = 0; i < this.length; i++) {
173 | if (this[i] != this[dx]) {
174 | this[n++] = this[i];
175 | }
176 | }
177 | this.length -= 1;
178 | };
179 |
180 | exports.default = _reflux2.default.createStore({
181 | items: [],
182 | listenables: [_actions2.default],
183 | onGetAll: function onGetAll() {
184 | this.trigger({ list: this.items });
185 | },
186 | onAdd: function onAdd(item) {
187 | this.items.push({ name: item });
188 | this.trigger({ list: this.items });
189 | },
190 | onRemove: function onRemove(i) {
191 | this.items.remove(i);
192 | this.trigger({ list: this.items });
193 | }
194 | });
195 |
196 | /***/ },
197 |
198 | /***/ 183:
199 | /***/ function(module, exports, __webpack_require__) {
200 |
201 | 'use strict';
202 |
203 | Object.defineProperty(exports, "__esModule", {
204 | value: true
205 | });
206 |
207 | var _reflux = __webpack_require__(160);
208 |
209 | var _reflux2 = _interopRequireDefault(_reflux);
210 |
211 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
212 |
213 | exports.default = _reflux2.default.createActions(['getAll', 'add', 'remove']);
214 |
215 | /***/ }
216 |
217 | });
--------------------------------------------------------------------------------
/todo-reflux/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | todo-reflux
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/todo-reflux/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo",
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-mixin": "^3.0.3",
15 | "reflux": "^0.3.0",
16 | "webpack": "^1.12.10"
17 | },
18 | "scripts": {
19 | "build": "webpack --progress -colors --watch"
20 | },
21 | "author": "lewis617",
22 | "license": "ISC"
23 | }
24 |
--------------------------------------------------------------------------------
/todo-reflux/src/actions/actions.js:
--------------------------------------------------------------------------------
1 | import Reflux from 'reflux'
2 |
3 | export default Reflux.createActions(['getAll','add','remove']);
--------------------------------------------------------------------------------
/todo-reflux/src/components/todo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Reflux from 'reflux'
3 | import ReactMixin from 'react-mixin'
4 | import store from '../stores/store'
5 | import actions from '../actions/actions'
6 |
7 | export default class Todo extends React.Component{
8 |
9 | //组件渲染完成后,通过action获取所有的数组,刷新绑定到this.state上
10 | componentDidMount() {
11 | actions.getAll();
12 | }
13 |
14 | add(){
15 | var item =this.refs.item.value;
16 | this.refs.item.value='';
17 | actions.add(item);
18 |
19 | }
20 |
21 | remove(i){
22 | actions.remove(i);
23 | }
24 |
25 | render() {
26 | //items用于乘放li的集合
27 | let items;
28 | if(this.state.list){
29 | items=this.state.list.map( (item,i)=> {
30 | //设置key是因为react的diff算法,是通过key来计算最小变化的
31 | return
32 | {item.name}
33 |
34 |
35 | })
36 | }
37 | return (
38 |
45 | )
46 | }
47 | }
48 |
49 | // ES6 mixin写法,通过mixin将store的与组件连接,功能是监听store带来的state变化并刷新到this.state
50 | ReactMixin.onClass(Todo, Reflux.connect(store));
51 |
--------------------------------------------------------------------------------
/todo-reflux/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Todo from './components/todo';
4 |
5 | ReactDOM.render(
6 |
7 | ,
8 | document.querySelector('#app')
9 | );
--------------------------------------------------------------------------------
/todo-reflux/src/stores/store.js:
--------------------------------------------------------------------------------
1 | import Reflux from 'reflux'
2 | import actions from '../actions/actions'
3 |
4 | //给数组添加remove方法,用于去除指定下标的元素
5 | Array.prototype.remove=function(dx)
6 | {
7 | if(isNaN(dx)||dx>this.length){return false;}
8 | for(var i=0,n=0;i