├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING-zh-tw.md ├── CONTRIBUTING.md ├── LICENSE-logo.md ├── LICENSE.md ├── PATRONS.md ├── README.md ├── book.json ├── build ├── fix-wrong-path.js └── use-lodash-es.js ├── docs ├── FAQ.md ├── Feedback.md ├── Glossary.md ├── README.md ├── Troubleshooting.md ├── advanced │ ├── AsyncActions.md │ ├── AsyncFlow.md │ ├── ExampleRedditAPI.md │ ├── Middleware.md │ ├── NextSteps.md │ ├── README.md │ └── UsageWithReactRouter.md ├── api │ ├── README.md │ ├── Store.md │ ├── applyMiddleware.md │ ├── bindActionCreators.md │ ├── combineReducers.md │ ├── compose.md │ └── createStore.md ├── basics │ ├── Actions.md │ ├── DataFlow.md │ ├── ExampleTodoList.md │ ├── README.md │ ├── Reducers.md │ ├── Store.md │ └── UsageWithReact.md ├── introduction │ ├── Ecosystem.md │ ├── Examples.md │ ├── Motivation.md │ ├── PriorArt.md │ ├── README.md │ └── ThreePrinciples.md └── recipes │ ├── ComputingDerivedData.md │ ├── ImplementingUndoHistory.md │ ├── IsolatingSubapps.md │ ├── MigratingToRedux.md │ ├── README.md │ ├── ReducingBoilerplate.md │ ├── ServerRendering.md │ ├── UsingObjectSpreadOperator.md │ └── WritingTests.md ├── examples ├── README.md ├── 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 ├── buildAll.js ├── counter-vanilla │ └── index.html ├── counter │ ├── .babelrc │ ├── components │ │ └── Counter.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ └── index.js │ ├── server.js │ ├── test │ │ ├── .eslintrc │ │ ├── components │ │ │ └── Counter.spec.js │ │ └── reducers │ │ │ └── counter.spec.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 │ ├── .babelrc │ ├── 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 │ ├── test │ │ ├── .eslintrc │ │ ├── components │ │ │ ├── Cart.spec.js │ │ │ ├── Product.spec.js │ │ │ ├── ProductItem.spec.js │ │ │ └── ProductsList.spec.js │ │ └── reducers │ │ │ ├── cart.spec.js │ │ │ ├── products.spec.js │ │ │ └── selectors.spec.js │ └── webpack.config.js ├── testAll.js ├── todomvc │ ├── .babelrc │ ├── actions │ │ └── index.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 │ │ └── index.js │ ├── components │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Link.js │ │ ├── Todo.js │ │ └── TodoList.js │ ├── containers │ │ ├── AddTodo.js │ │ ├── FilterLink.js │ │ ├── UndoRedo.js │ │ └── VisibleTodoList.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ ├── index.js │ │ ├── todos.js │ │ └── visibilityFilter.js │ ├── server.js │ └── webpack.config.js ├── todos │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── components │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Link.js │ │ ├── Todo.js │ │ └── TodoList.js │ ├── containers │ │ ├── AddTodo.js │ │ ├── FilterLink.js │ │ └── VisibleTodoList.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ ├── index.js │ │ ├── todos.js │ │ └── visibilityFilter.js │ ├── server.js │ ├── test │ │ ├── .eslintrc │ │ ├── actions │ │ │ └── todos.spec.js │ │ ├── reducers │ │ │ └── todos.spec.js │ │ └── setup.js │ └── webpack.config.js ├── tree-view │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── containers │ │ └── Node.js │ ├── generateTree.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ └── index.js │ ├── server.js │ ├── store │ │ └── configureStore.js │ ├── test │ │ ├── .eslintrc │ │ ├── node.spec.js │ │ └── reducer.spec.js │ └── webpack.config.js └── universal │ ├── .babelrc │ ├── client │ └── index.js │ ├── common │ ├── actions │ │ └── index.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 ├── index.d.ts ├── logo ├── README.md ├── apple-touch-icon.png ├── favicon.ico ├── logo-title-dark.png ├── logo-title-light.png ├── logo.png └── logo.svg ├── package.json ├── src ├── applyMiddleware.js ├── bindActionCreators.js ├── combineReducers.js ├── compose.js ├── createStore.js ├── index.js └── utils │ └── warning.js ├── test ├── .eslintrc ├── applyMiddleware.spec.js ├── bindActionCreators.spec.js ├── combineReducers.spec.js ├── compose.spec.js ├── createStore.spec.js ├── helpers │ ├── actionCreators.js │ ├── actionTypes.js │ ├── middleware.js │ └── reducers.js ├── typescript.spec.js ├── typescript │ ├── actionCreators.ts │ ├── actions.ts │ ├── compose.ts │ ├── dispatch.ts │ ├── middleware.ts │ ├── reducers.ts │ └── store.ts └── utils │ └── warning.spec.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["transform-es2015-template-literals", { "loose": true }], 4 | "transform-es2015-literals", 5 | "transform-es2015-function-name", 6 | "transform-es2015-arrow-functions", 7 | "transform-es2015-block-scoped-functions", 8 | ["transform-es2015-classes", { "loose": true }], 9 | "transform-es2015-object-super", 10 | "transform-es2015-shorthand-properties", 11 | ["transform-es2015-computed-properties", { "loose": true }], 12 | ["transform-es2015-for-of", { "loose": true }], 13 | "transform-es2015-sticky-regex", 14 | "transform-es2015-unicode-regex", 15 | "check-es2015-constants", 16 | ["transform-es2015-spread", { "loose": true }], 17 | "transform-es2015-parameters", 18 | ["transform-es2015-destructuring", { "loose": true }], 19 | "transform-es2015-block-scoping", 20 | "transform-object-rest-spread", 21 | "transform-es3-member-expression-literals", 22 | "transform-es3-property-literals" 23 | ], 24 | "env": { 25 | "commonjs": { 26 | "plugins": [ 27 | ["transform-es2015-modules-commonjs", { "loose": true }] 28 | ] 29 | }, 30 | "es": { 31 | "plugins": [ 32 | "./build/use-lodash-es" 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/** 2 | **/node_modules/** 3 | **/server.js 4 | **/webpack.config*.js 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "rackt", 3 | "rules": { 4 | "valid-jsdoc": 2, 5 | // Disable until Flow supports let and const 6 | "no-var": 0, 7 | "react/jsx-uses-react": 1, 8 | "react/jsx-no-undef": 2, 9 | "react/wrap-multilines": 2 10 | }, 11 | "plugins": [ 12 | "react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/lib 3 | .*/test 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Do you want to request a *feature* or report a *bug*?** 2 | 3 | (If this is a *usage question*, please **do not post it here**—post it on [Stack Overflow](http://stackoverflow.com/questions/tagged/redux) instead. If this is not a “feature” or a “bug”, or the phrase “How do I...?” applies, then it's probably a usage question.) 4 | 5 | 6 | 7 | **What is the current behavior?** 8 | 9 | 10 | 11 | **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar.** 12 | 13 | 14 | 15 | **What is the expected behavior?** 16 | 17 | 18 | 19 | **Which versions of Redux, and which browser and OS are affected by this issue? Did this work in previous versions of Redux?** 20 | 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | dist 5 | lib 6 | es 7 | coverage 8 | _book 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | script: 7 | - npm run check:src 8 | - npm run build 9 | - npm run check:examples 10 | branches: 11 | only: 12 | - master 13 | cache: 14 | directories: 15 | - $HOME/.npm 16 | - examples/async/node_modules 17 | - examples/counter/node_modules 18 | - examples/real-world/node_modules 19 | - examples/shopping-cart/node_modules 20 | - examples/todomvc/node_modules 21 | - examples/todos/node_modules 22 | - examples/todos-with-undo/node_modules 23 | - examples/tree-view/node_modules 24 | - examples/universal/node_modules 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 變更日誌 2 | 3 | 這個專案遵循 [Semantic Versioning](http://semver.org/)。 4 | 每一個釋出版本,以及它的遷移說明,都被記錄在 Github 的 [Releases](https://github.com/reactjs/redux/releases) 頁面上。 5 | -------------------------------------------------------------------------------- /CONTRIBUTING-zh-tw.md: -------------------------------------------------------------------------------- 1 | ## 前置流程 2 | 3 | 把 `reactjs/redux` 加到 clone 下來專案的 remote 4 | 5 | ```sh 6 | git remote add upstream https://github.com/reactjs/redux.git 7 | ``` 8 | 9 | ## 更新流程 (參考用) 10 | 11 | ```sh 12 | git checkout new-doc-zh-tw 13 | git checkout -b doc-update # 隨便 branch 名稱 14 | git fetch upstream 15 | git merge upstream/master 16 | 17 | # 解決衝突 ....然後 commit 18 | # 發 Pull Request 19 | ``` 20 | 21 | ## 發佈文件 22 | 23 | ```sh 24 | npm run docs:publish 25 | ``` 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Dan Abramov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PATRONS.md: -------------------------------------------------------------------------------- 1 | # 贊助者 2 | 3 | 在 Redux 的工作是[由社群出資](https://www.patreon.com/reactdx)。 4 | 遇到一些卓越的公司與個人使這可以成真: 5 | 6 | * [Webflow](https://github.com/webflow) 7 | * [Ximedes](https://www.ximedes.com/) 8 | * [HauteLook](http://hautelook.github.io/) 9 | * [Ken Wheeler](http://kenwheeler.github.io/) 10 | * [Chung Yen Li](https://www.facebook.com/prototocal.lee) 11 | * [Sunil Pai](https://twitter.com/threepointone) 12 | * [Charlie Cheever](https://twitter.com/ccheever) 13 | * [Eugene G](https://twitter.com/e1g) 14 | * [Matt Apperson](https://twitter.com/mattapperson) 15 | * [Jed Watson](https://twitter.com/jedwatson) 16 | * [Sasha Aickin](https://twitter.com/xander76) 17 | * [Stefan Tennigkeit](https://twitter.com/whobubble) 18 | * [Sam Vincent](https://twitter.com/samvincent) 19 | * Olegzandr Denman 20 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "2.4.3", 3 | "title": "Redux", 4 | "structure": { 5 | "summary": "docs/README.md" 6 | }, 7 | "plugins": ["edit-link", "prism", "-highlight", "github", "anker-enable"], 8 | "pluginsConfig": { 9 | "edit-link": { 10 | "base": "https://github.com/chentsulin/redux/tree/new-doc-zh-tw", 11 | "label": "Edit This Page" 12 | }, 13 | "github": { 14 | "url": "https://github.com/chentsulin/redux/" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build/fix-wrong-path.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob') 2 | var fs = require('fs') 3 | 4 | glob('./_book/docs/**/*.html', function (err, files) { 5 | if (err) { 6 | throw err 7 | } 8 | 9 | files.forEach(function (file) { 10 | fs.readFile(file, 'utf8', function (err, data) { 11 | if (err) { 12 | throw err 13 | } 14 | 15 | fs.writeFile(file, data.replace(/..\/..\/..\/_book\/docs/g, '../../docs'), function (err) { 16 | if (err) { 17 | throw err 18 | } 19 | 20 | console.log('path fixed ' + file) // eslint-disable-line no-console 21 | }) 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /build/use-lodash-es.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | visitor: { 4 | ImportDeclaration(path) { 5 | var source = path.node.source 6 | source.value = source.value.replace(/^lodash($|\/)/, 'lodash-es$1') 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/Feedback.md: -------------------------------------------------------------------------------- 1 | # Feedback 2 | 3 | 我們很感謝你從 community feedback 訊息。你可以在 [Product Pains](https://productpains.com/product/redux) 發表功能需求和 bug 回報。 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | * [Read Me](/README.md) 4 | * [介紹](/docs/introduction/README.md) 5 | * [動機](/docs/introduction/Motivation.md) 6 | * [三大原則](/docs/introduction/ThreePrinciples.md) 7 | * [先前技術](/docs/introduction/PriorArt.md) 8 | * [生態系](/docs/introduction/Ecosystem.md) 9 | * [範例](/docs/introduction/Examples.md) 10 | * [基礎](/docs/basics/README.md) 11 | * [Action](/docs/basics/Actions.md) 12 | * [Reducer](/docs/basics/Reducers.md) 13 | * [Store](/docs/basics/Store.md) 14 | * [資料流](/docs/basics/DataFlow.md) 15 | * [搭配 React 運用](/docs/basics/UsageWithReact.md) 16 | * [範例:Todo 清單](/docs/basics/ExampleTodoList.md) 17 | * [進階](/docs/advanced/README.md) 18 | * [Async Action](/docs/advanced/AsyncActions.md) 19 | * [非同步資料流](/docs/advanced/AsyncFlow.md) 20 | * [Middleware](/docs/advanced/Middleware.md) 21 | * Usage with React Router 22 | * [範例:Reddit API](/docs/advanced/ExampleRedditAPI.md) 23 | * Next Steps 24 | * [Recipes](/docs/recipes/README.md) 25 | * [遷移到 Redux](/docs/recipes/MigratingToRedux.md) 26 | * [使用 Object Spread 運算子](/docs/recipes/UsingObjectSpreadOperator.md) 27 | * [減少 Boilerplate](/docs/recipes/ReducingBoilerplate.md) 28 | * [伺服器 Render](/docs/recipes/ServerRendering.md) 29 | * [撰寫測試](/docs/recipes/WritingTests.md) 30 | * [計算衍生資料](/docs/recipes/ComputingDerivedData.md) 31 | * [實作 Undo 歷史](/docs/recipes/ImplementingUndoHistory.md) 32 | * [分隔 Redux 子應用程式](/docs/recipes/IsolatingSubapps.md) 33 | * [常見問答集](/docs/FAQ.md) 34 | * [疑難排解](/docs/Troubleshooting.md) 35 | * [術語表](/docs/Glossary.md) 36 | * [API 參考](/docs/api/README.md) 37 | * [createStore](/docs/api/createStore.md) 38 | * [Store](/docs/api/Store.md) 39 | * [combineReducers](/docs/api/combineReducers.md) 40 | * [applyMiddleware](/docs/api/applyMiddleware.md) 41 | * [bindActionCreators](/docs/api/bindActionCreators.md) 42 | * [compose](/docs/api/compose.md) 43 | * [變更日誌](/CHANGELOG.md) 44 | * [贊助者](/PATRONS.md) 45 | * [Feedback](/docs/Feedback.md) 46 | -------------------------------------------------------------------------------- /docs/advanced/AsyncFlow.md: -------------------------------------------------------------------------------- 1 | # 非同步資料流 2 | 3 | 不使用 [middleware](Middleware.md) 的話,Redux 的 store 只支援[同步資料流](../basics/DataFlow.md)。 這就是你使用 [`createStore()`](../api/createStore.md) 預設會拿到的。 4 | 5 | 你可以用 [`applyMiddleware()`](../api/applyMiddleware.md) 來加強 [`createStore()`](../api/createStore.md)。這不是必須的,不過這讓你[用比較方便的方式來表達非同步的 action](AsyncActions.md)。 6 | 7 | 非同步的 middleware 像是 [redux-thunk](https://github.com/gaearon/redux-thunk) 或是 [redux-promise](https://github.com/acdlite/redux-promise) 都包裝了 store 的 [`dispatch()`](../api/Store.md#dispatch) 方法,並允許你 dispatch 一些 action 以外的東西,例如:function 或 Promise。你使用的任何 middleware 可以接著解譯你 dispatch 的任何東西,並轉而傳遞 action 到下一個鏈中的 middleware。例如,Promise middleware 可以攔截 Promise,並針對每一個 Promise 非同步的 dispatch 一對的開始/結束 action。 8 | 9 | 當最後一個在鏈中的 middleware 要 dispatch action 時,action 必須是個一般物件。這就是[同步的 Redux 資料流](../basics/DataFlow.md)開始的地方。 10 | 11 | 請查看[非同步範例完整的原始碼](ExampleRedditAPI.md)。 12 | 13 | ## 下一步 14 | 15 | 現在你已經看過一個 middleware 在 Redux 中可以做到什麼的範例了,是時候來學習它實際上如何運作,還有你可以如何建立你自己的 middleware。前進到下一個章節有 [Middleware](Middleware.md) 相關的詳細內容。 16 | -------------------------------------------------------------------------------- /docs/advanced/NextSteps.md: -------------------------------------------------------------------------------- 1 | # 接下來 2 | 3 | 抱歉,我們還在撰寫這份文件。 4 | 敬請關注,它將會在幾天後出現。 5 | -------------------------------------------------------------------------------- /docs/advanced/README.md: -------------------------------------------------------------------------------- 1 | # 進階 2 | 3 | 在[基礎章節](../basics/README.md),我們已經探討了如何去架構一個簡單的 Redux 應用程式。在這個章節,我們將會探討如何處理 AJAX 和 routing 進到整個架構中。 4 | 5 | * [Async Action](AsyncActions.md) 6 | * [非同步資料流](AsyncFlow.md) 7 | * [Middleware](Middleware.md) 8 | * [搭配 React Router 運用](UsageWithReactRouter.md) 9 | * [範例:Reddit API](ExampleRedditAPI.md) 10 | * [接下來](NextSteps.md) 11 | -------------------------------------------------------------------------------- /docs/advanced/UsageWithReactRouter.md: -------------------------------------------------------------------------------- 1 | # 搭配 React Router 運用 2 | 3 | 抱歉,我們還在撰寫這份文件。 4 | 敬請關注,它將會在幾天後出現。 5 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # API 參考 2 | 3 | Redux 暴露出來的 API 非常少。Redux 定義一系列的介面給你去實作 (例如 [reducers](../Glossary.md#reducer)) 並提供少數的 helper function 來把這些介面綁在一起。 4 | 5 | 這個章節把 Redux API 完整的文件化。請謹記於心,Redux 只關注管理 state。在一個實際的應用程式中,你也會想要使用像是 [react-redux](https://github.com/gaearon/react-redux) 之類的 UI 綁定。 6 | 7 | ### 頂層 Exports 8 | 9 | * [createStore(reducer, [preloadedState])](createStore.md) 10 | * [combineReducers(reducers)](combineReducers.md) 11 | * [applyMiddleware(...middlewares)](applyMiddleware.md) 12 | * [bindActionCreators(actionCreators, dispatch)](bindActionCreators.md) 13 | * [compose(...functions)](compose.md) 14 | 15 | ### Store API 16 | 17 | * [Store](Store.md) 18 | * [getState()](Store.md#getState) 19 | * [dispatch(action)](Store.md#dispatch) 20 | * [subscribe(listener)](Store.md#subscribe) 21 | * [replaceReducer(nextReducer)](Store.md#replaceReducer) 22 | 23 | ### Importing 24 | 25 | 描述在上面的每個都是頂層 export 的 function。你可以像這樣 import 它們之中任何一個: 26 | 27 | #### ES6 28 | 29 | ```js 30 | import { createStore } from 'redux' 31 | ``` 32 | 33 | #### ES5 (CommonJS) 34 | 35 | ```js 36 | var createStore = require('redux').createStore 37 | ``` 38 | 39 | #### ES5 (UMD build) 40 | 41 | ```js 42 | var createStore = Redux.createStore 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/api/compose.md: -------------------------------------------------------------------------------- 1 | # `compose(...functions)` 2 | 3 | 把 function 從右到左組合起來。 4 | 5 | 這是一個 functional programming 的 utility,並為了方便而直接被放在 Redux 裡。 6 | 你可能會想要使用它來在一行中使用幾個 [store enhancer](../Glossary.md#store-enhancer)。 7 | 8 | #### 參數 9 | 10 | 1. (*arguments*):要組合的 function。每個 function 都預期會接收一個參數。它的回傳值將會作為在它左邊的 function 的變數,以此類推。有個例外是當為最後被組合的 function 提供 signature 時, 最右邊的變數可接受多個參數。 11 | 12 | #### 回傳 13 | 14 | (*Function*):藉由從右到左組合給定的 function 而獲得的最終 function。 15 | 16 | #### 範例 17 | 18 | 這個範例展示了要如何使用 `compose` 藉由 [`applyMiddleware`](applyMiddleware.md) 與幾個來自 [redux-devtools](https://github.com/gaearon/redux-devtools) 套件的開發工具來增強一個 [store](Store.md)。 19 | 20 | ```js 21 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux' 22 | import thunk from 'redux-thunk' 23 | import DevTools from './containers/DevTools' 24 | import reducer from '../reducers/index' 25 | 26 | const store = createStore( 27 | reducer, 28 | compose( 29 | applyMiddleware(thunk), 30 | DevTools.instrument() 31 | ) 32 | ) 33 | ``` 34 | 35 | #### 提示 36 | 37 | * `compose` 做的只是讓你不需要把程式碼往右縮進,就能撰寫深度巢狀的 function 轉換。不要把它想得太複雜! 38 | -------------------------------------------------------------------------------- /docs/api/createStore.md: -------------------------------------------------------------------------------- 1 | # `createStore(reducer, [preloadedState], [enhancer])` 2 | 3 | 建立一個 Redux [store](Store.md),它掌控應用程式的完整 state tree。 4 | 在你的應用程式中應該只有單一一個 store。 5 | 6 | #### 參數 7 | 8 | 1. `reducer` *(Function)*:一個回傳下個 [state tree](../Glossary.md#state) 的 [reducing function](../Glossary.md#reducer),會接收當下的 state tree 和一個要處理的 [action](../Glossary.md#action)。 9 | 10 | 2. [`preloadedState`] *(any)*:初始的 state。你可以選擇性的指定它來在 universal 應用程式 hydrate 從伺服器來的 state,或是用來恢復使用者先前被 serialize 的操作狀態。如果你使用 [`combineReducers`](combineReducers.md) 來產生 `reducer`,這必須是一個跟之前傳遞給它的物件有著相同形狀的 keys 的一般物件。反之,你可以自由地傳遞任何你的 `reducer` 可以了解的東西。 11 | 12 | 3. [`enhancer`] *(Function)*:store 的 enhancer。你可以選擇指定它來使用第三方功能以加強 store,像是 middleware、時間旅行、persistence 等等。Redux 唯一附帶的 store enhancer 是 [`applyMiddleware()`](./applyMiddleware.md)。 13 | 14 | #### 回傳 15 | 16 | ([*`Store`*](Store.md)):掌控應用程式的完整 state 的一個物件。改變它的 state 的唯一方式是藉由 [dispatch actions](Store.md#dispatch)。你也可以[訂閱](Store.md#subscribe)它的 state 變更以更新 UI。 17 | 18 | #### 範例 19 | 20 | ```js 21 | import { createStore } from 'redux' 22 | 23 | function todos(state = [], action) { 24 | switch (action.type) { 25 | case 'ADD_TODO': 26 | return state.concat([ action.text ]) 27 | default: 28 | return state 29 | } 30 | } 31 | 32 | let store = createStore(todos, [ 'Use Redux' ]) 33 | 34 | store.dispatch({ 35 | type: 'ADD_TODO', 36 | text: 'Read the docs' 37 | }) 38 | 39 | console.log(store.getState()) 40 | // [ 'Use Redux', 'Read the docs' ] 41 | ``` 42 | 43 | #### 提示 44 | 45 | * 不要在應用程式中建立超過一個 store!作為替代,使用 [`combineReducers`](combineReducers.md) 來從多個 reducer 建立一個 root reducer。 46 | 47 | * 要怎樣選擇 state 的格式取決於你。你可以使用一般物件或是一些像是 [Immutable](http://facebook.github.io/immutable-js/) 的東西。如果你不確定要用什麼,請從使用一般物件開始。 48 | 49 | * 如果你的 state 是一個一般物件,請確認你從來沒有變更它!例如,不要從你的 reducers 回傳一些像是 `Object.assign(state, newData)` 的東西,應該回傳 `Object.assign({}, state, newData)`。用這個方法你不會覆寫掉先前的 `state`。如果你啟用 [object spread 運算子提案](../recipes/UsingObjectSpreadOperator.md),你也可以寫成 `return { ...state, ...newData }`。 50 | 51 | * 對於運行在伺服器上的 universal 應用程式,在每個請求建立一個 store 實體,因此它們彼此是隔離的。在伺服器上 render 應用程式之前,dispatch 幾個資料抓取的 actions 到 store 實體並等待它們完成。 52 | 53 | * 當一個 store 被建立,Redux 會 dispatch 一個假的 action 到你的 reducer 來把初始的 state 填到 store。這不意味你可以直接的處理這個假 action。只要記住如果給你的 reducer 作為第一個參數的 state 是 `undefined`,那它應該回傳某種初始的 state,然後你所有的東西都會設置好。 54 | 55 | * 要套用多個 store enhancer,你可以使用 [`compose()`](./compose.md)。 56 | -------------------------------------------------------------------------------- /docs/basics/README.md: -------------------------------------------------------------------------------- 1 | # 基礎 2 | 3 | 不要被所有有關 reducer、middleware、store enhancer 的花俏談話給騙了—Redux 令人難以置信的簡單。如果你曾經建置過一個 Flux 應用程式,你會感覺像是回到家了。如果你剛開始接觸 Flux,它也很簡單! 4 | 5 | 在這份指南中,我們將帶過建立一個簡單 Todo 應用程式的過程。 6 | 7 | * [Action](Actions.md) 8 | * [Reducer](Reducers.md) 9 | * [Store](Store.md) 10 | * [資料流](DataFlow.md) 11 | * [搭配 React 運用](UsageWithReact.md) 12 | * [範例:Todo 清單](ExampleTodoList.md) 13 | -------------------------------------------------------------------------------- /docs/basics/Store.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | 在前面的章節,我們定義了代表實際上「發生了什麼」的 [action](Actions.md),和依據這些 action 更新 state 的 [reducer](Reducers.md)。 4 | 5 | **Store** 是把它們結合在一起的物件。store 有以下的責任: 6 | 7 | * 掌管應用程式狀態; 8 | * 允許藉由 [`getState()`](../api/Store.md#getState) 獲取 state; 9 | * 允許藉由 [`dispatch(action)`](../api/Store.md#dispatch) 來更新 state; 10 | * 藉由 [`subscribe(listener)`](../api/Store.md#subscribe) 註冊 listener; 11 | * 藉由 [`subscribe(listener)`](../api/Store.md#subscribe) 回傳的 function 處理撤銷 listener。 12 | 13 | 有一點很重要需要注意,你的 Redux 應用程式中將只會有一個 store。當你想要把你的資料處理邏輯拆分時,你會使用 [reducer 合成](Reducers.md#splitting-reducers) 而不是使用許多的 store。 14 | 15 | 如果你已經有一個 reducer,要建立 store 非常簡單。在[前面的章節](Reducers.md),我們使用 [`combineReducers()`](../api/combineReducers.md) 來把一些 reducer 合併成一個。我們現在要 import 它,並把它傳進 [`createStore()`](../api/createStore.md)。 16 | 17 | ```js 18 | import { createStore } from 'redux' 19 | import todoApp from './reducers' 20 | let store = createStore(todoApp) 21 | ``` 22 | 23 | 你可以選擇性的指定初始 state 作為第二個參數傳遞給 [`createStore()`](../api/createStore.md)。這對 hydrate 客戶端的 state 以符合運行在伺服器上的 Redux 應用程式的 state 非常有用。 24 | 25 | ```js 26 | let store = createStore(todoApp, window.STATE_FROM_SERVER) 27 | ``` 28 | 29 | ## Dispatch Action 30 | 31 | 現在我們已經建立了一個 store,讓我們來驗證程式可以運作!即使沒有任何的 UI,我們也已經可以測試更新邏輯。 32 | 33 | ```js 34 | import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions' 35 | 36 | // 記錄初始 state 37 | console.log(store.getState()) 38 | 39 | // 每次 state 變更,就記錄它 40 | // 記得 subscribe() 會回傳一個用來撤銷 listener 的 function 41 | let unsubscribe = store.subscribe(() => 42 | console.log(store.getState()) 43 | ) 44 | 45 | // Dispatch 一些 action 46 | store.dispatch(addTodo('Learn about actions')) 47 | store.dispatch(addTodo('Learn about reducers')) 48 | store.dispatch(addTodo('Learn about store')) 49 | store.dispatch(toggleTodo(0)) 50 | store.dispatch(toggleTodo(1)) 51 | store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) 52 | 53 | // 停止監聽 state 的更新 54 | unsubscribe() 55 | ``` 56 | 57 | 你可以看到這如何造成 store 掌管的 state 改變: 58 | 59 | 60 | 61 | 在我們開始撰寫 UI 之前,我們已經設定了應用程式的行為。在這個時間點,你已經可以為你的 reducer 和 action creator 撰寫測試,不過我們不會在這個教學中這樣做。你不需要 mock 任何東西,因為他們只是 function。呼叫它們,然後對它們回傳的東西做出 assertion。 62 | 63 | ## 原始碼 64 | 65 | #### `index.js` 66 | 67 | ```js 68 | import { createStore } from 'redux' 69 | import todoApp from './reducers' 70 | 71 | let store = createStore(todoApp) 72 | ``` 73 | 74 | ## 下一步 75 | 76 | 在為我們的 todo 應用程式建立 UI 之前,我們將會繞道看看[資料在 Redux 應用程式中如何流動](DataFlow.md)。 77 | -------------------------------------------------------------------------------- /docs/introduction/Motivation.md: -------------------------------------------------------------------------------- 1 | # 動機 2 | 3 | 隨著對 JavaScript single-page 應用程式的要求變得越來愈複雜,**我們的程式碼必須管理高於以往的 state**。這些 state 可以包括伺服器回應和快取的資料,以及本地端建立而尚未保存到伺服器的資料。UI state 也越來愈複雜,因為我們需要管理 active route、被選擇的 tab、是否要顯示 spinner、 pagination 控制應不應該被顯示,等等。 4 | 5 | 管理這個不斷變化的 state 是很困難的。如果一個 model 可以更新其他的 model,接著一個 view 可以更新一個 model,而它更新了另一個 model,而這個順帶的,可能造成另一個 view 被更新。到了某個時間點,你不再了解你的應用程式中發生了些什麼,因為你已經**失去對 state 何時、為什麼、如何運作的控制權。**當系統不透明且充滿不確定性,就很難去重現 bug 或添加新的功能。 6 | 7 | 彷彿這樣還不夠糟糕,試想一些**在前端產品開發越來越普遍的新需求**。作為開發者,我們被期望要去處理 optimistic update、伺服器端 render、在轉換 route 前抓取資料,等等。我們發現自己嘗試去管理一個從來沒有處理過的複雜度,我們不免會有這樣的疑問:[是時候該放棄了嗎?](http://www.quirksmode.org/blog/archives/2015/07/stop_pushing_th.html) 答案是_不_。 8 | 9 | 這樣的複雜度很難去處理,因為 **我們混合了兩個概念**,它們都是人類的頭腦非常難去思考的概念:**變更和非同步。**我稱它們為 [Mentos and Coke](https://en.wikipedia.org/wiki/Diet_Coke_and_Mentos_eruption)。分開都好好的,但混在一起就變成一團糟。[React](http://facebook.github.io/react) 之類的 Library 試圖藉由移除非同步和直接的 DOM 操作,來在 view layer 解決這個問題。但是,管理資料 state 的部分留下來讓你自己決定。這就是 Redux 的切入點。 10 | 11 | 跟隨著 [Flux](http://facebook.github.io/flux)、[CQRS](http://martinfowler.com/bliki/CQRS.html) 和 [Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html) 的腳步,Redux 藉由強加某些限制在更新發生的方式和時機上,**試圖讓 state 的變化更有可預測性**。這些限制都反應在 Redux 的[三大原則](ThreePrinciples.md)中。 12 | -------------------------------------------------------------------------------- /docs/introduction/README.md: -------------------------------------------------------------------------------- 1 | ## 介紹 2 | 3 | * [動機](Motivation.md) 4 | * [三大原則](ThreePrinciples.md) 5 | * [先前技術](PriorArt.md) 6 | * [生態系](Ecosystem.md) 7 | * [範例](Examples.md) 8 | -------------------------------------------------------------------------------- /docs/introduction/ThreePrinciples.md: -------------------------------------------------------------------------------- 1 | # 三大原則 2 | 3 | Redux 可以用三個基本的原則來描述: 4 | 5 | ### 唯一真相來源 6 | 7 | **你整個應用程式的 [state](../Glossary.md#state),被儲存在一個樹狀物件放在唯一的 [store](../Glossary.md#store) 裡面。** 8 | 9 | 這讓建立 universal 應用程式變得更簡單,因為從伺服器來的 state 可以被 serialize 並 hydrate 進去客戶端,而不需要撰寫其他額外的程式碼。一個單一的 state tree 也讓 debug 或是調試一個應用程式更容易;它也讓你在開發期間保存應用程式的 state,以求更快的開發週期。一些傳統很難實作的功能 - 例如,復原/重做 - 可以突然變得很容易實作,如果你所有的 state 都被儲存在單一一個 single tree。 10 | 11 | ```js 12 | console.log(store.getState()) 13 | 14 | /* Prints 15 | { 16 | visibilityFilter: 'SHOW_ALL', 17 | todos: [ 18 | { 19 | text: 'Consider using Redux', 20 | completed: true, 21 | }, 22 | { 23 | text: 'Keep all state in a single tree', 24 | completed: false 25 | } 26 | ] 27 | } 28 | */ 29 | ``` 30 | 31 | ### State 是唯讀的 32 | 33 | **改變 state 的唯一的方式是發出一個 [action](../Glossary.md#action),也就是一個描述發生什麼事的物件。** 34 | 35 | 這能確保 view 和網路 callback 都不會直接寫入 state。替代的,它們表達了一個改變 state 的意圖。因為所有的改變都是集中的,並依照嚴格的順序一個接一個的發生,沒有需要特別注意的微妙 race condition。因為 Action 只是普通物件,所以它們可以被記錄、serialize、儲存、並在之後為了 debug 或測試目的而重播。 36 | 37 | ```js 38 | store.dispatch({ 39 | type: 'COMPLETE_TODO', 40 | index: 1 41 | }) 42 | 43 | store.dispatch({ 44 | type: 'SET_VISIBILITY_FILTER', 45 | filter: 'SHOW_COMPLETED' 46 | }) 47 | ``` 48 | 49 | ### 變更被寫成 pure function 50 | 51 | **要指定 state tree 如何藉由 action 來轉變,你必須撰寫 pure [reducer](../Glossary.md#reducer)。** 52 | 53 | Reducer 只是 pure function,它取得先前的 state 和一個 action,並回傳下一個 state。請記得要回傳一個新的 state 物件,而不要去改變先前的 state。你可以從單一一個 reducer 開始,而隨著你的應用程式成長,把它們拆分成比較小的 reducer 來管理 state tree 中的特定部分。因為 reducer 只是 function,你可以控制它們被呼叫的順序、傳遞額外的資料、或甚至建立可重用的 reducer 來做一些常見的任務,例如 pagination。 54 | 55 | ```js 56 | 57 | function visibilityFilter(state = 'SHOW_ALL', action) { 58 | switch (action.type) { 59 | case 'SET_VISIBILITY_FILTER': 60 | return action.filter 61 | default: 62 | return state 63 | } 64 | } 65 | 66 | function todos(state = [], action) { 67 | switch (action.type) { 68 | case 'ADD_TODO': 69 | return [ 70 | ...state, 71 | { 72 | text: action.text, 73 | completed: false 74 | } 75 | ] 76 | case 'COMPLETE_TODO': 77 | return state.map((todo, index) => { 78 | if (index === action.index) { 79 | return Object.assign({}, todo, { 80 | completed: true 81 | }) 82 | } 83 | return todo 84 | }) 85 | default: 86 | return state 87 | } 88 | } 89 | 90 | import { combineReducers, createStore } from 'redux' 91 | let reducer = combineReducers({ visibilityFilter, todos }) 92 | let store = createStore(reducer) 93 | ``` 94 | 95 | 就這樣!現在你已經知道 Redux 都是些什麼了。 96 | -------------------------------------------------------------------------------- /docs/recipes/IsolatingSubapps.md: -------------------------------------------------------------------------------- 1 | # 分隔 Redux 子應用程式 2 | 3 | 試想一個嵌入較小的「子應用程式」(被裝在 `` component) 的「大型」應用程式 (被裝在一個 `` component) 案例: 4 | 5 | ```js 6 | import React, { Component } from 'react' 7 | import SubApp from './subapp' 8 | 9 | class BigApp extends Component { 10 | render() { 11 | return ( 12 |
13 | 14 | 15 | 16 |
17 | ) 18 | } 19 | } 20 | ``` 21 | 22 | 這些 `` 會是完全地獨立的。它們不會共享資料或是 action,並不會看見或與彼此溝通。 23 | 24 | 最好不要把這個方式跟標準的 Redux reducer composition 混在一起。 25 | 針對典型的 web 應用程式,請繼續使用 reducer composition。針對 26 | 「product hub」、「dashboard」或是把不同的工具包進一個統一包裝的企業軟體,則可以試試子應用程式方式。 27 | 28 | 子應用程式方式也對以產品或功能劃分的大型團隊很有用。這些團隊可以獨立的發布子應用程式或是與一個附帶的 「應用程式 shell」做結合。 29 | 30 | 以下是一個子應用程式的 root connected component。 31 | 跟一般一樣,它可以 render 更多的 component 作為 children,不管有沒有被 connect 的都可以。 32 | 通常我們會就這樣在 `` 中 render 它。 33 | 34 | ```js 35 | class App extends Component { ... } 36 | export default connect(mapStateToProps)(App) 37 | ``` 38 | 39 | 不過,如果我們對隱藏這個子應用程式 component 是一個 Redux 應用程式的事實有興趣,我們不需要呼叫 `ReactDOM.render()`。 40 | 41 | 或許我們想要能夠在同個「較大的」應用程式執行數個它的實體並讓它維持是一個完全的黑箱,把 Redux 變成實作細節。 42 | 43 | 要把 Redux 隱藏在 React API 的背後,我們可以把它包在一個特別的 component 中,並在 constructor 初始化 store: 44 | 45 | ```js 46 | import React, { Component } from 'react' 47 | import { Provider } from 'react-redux' 48 | import reducer from './reducers' 49 | import App from './App' 50 | 51 | class SubApp extends Component { 52 | constructor(props) { 53 | super(props) 54 | this.store = createStore(reducer) 55 | } 56 | 57 | render() { 58 | return ( 59 | 60 | 61 | 62 | ) 63 | } 64 | } 65 | ``` 66 | 67 | 用這個方法每個實體都會是獨立的。 68 | 69 | 這個模式*不*建議用在相同但是會共享資料的應用程式部件上。 70 | 不過,它在較大的應用程式完全不存取較小的應用程式內部時很有用, 71 | 而且我們想要把它們是用 Redux 實作的事實作為實作細節。 72 | 每個 component 實體會有它自己的 store,所以它們不會「了解」彼此。 73 | -------------------------------------------------------------------------------- /docs/recipes/MigratingToRedux.md: -------------------------------------------------------------------------------- 1 | # 遷移到 Redux 2 | 3 | Redux 不是一個整體的框架,而是一系列的介面和[一些可以讓它們一起合作的 functions](../api/README.md)。甚至你大多數的「Redux 程式碼」都不會用到 Redux API,因為你大部份的時間都會是在撰寫 function。 4 | 5 | 這讓遷入還是遷出 Redux 都很簡單。 6 | 我們不想要把你綁在上面! 7 | 8 | ## 從 Flux 9 | 10 | [Reducer](../Glossary.md#reducer) 擷取了 Flux Store 的「本質」,所以無論你是使用 [Flummox](http://github.com/acdlite/flummox)、[Alt](http://github.com/goatslacker/alt)、[傳統 Flux](https://github.com/facebook/flux),或任何其他的 Flux library,漸漸的遷移一個已存在的 Flux 專案到 Redux 都是可以的。 11 | 12 | 同樣的你也可以反向的依照一樣的步驟,來從 Redux 遷移到上述任何的這些 library。 13 | 14 | 你的流程會看起來像這樣: 15 | 16 | * 建立一個叫做 `createFluxStore(reducer)` 的 function,它用來從 reducer function 建立一個相容於你現存應用程式的 Flux store。從 Redux 內部來說,它可能看起來很像 [`createStore`](../api/createStore.md) ([原始碼](https://github.com/reactjs/redux/blob/master/src/createStore.js)) 的實作。它的 dispatch handler 應該只針對 action 去呼叫 `reducer`、儲存 state 變化、並發送 change 事件。 17 | 18 | * 這讓你可以漸漸的把應用程式中的每一個 Flux Store 改寫成 reducer,但仍然 export `createFluxStore(reducer)`,所以應用程式的剩餘部分並不會察覺到有什麼事發生,也只會看到 Flux store。 19 | 20 | * 當你改寫了你的 Store 後,你將會發現你需要避免某些 Flux 的反模式,例如:在 Store 裡面抓取 API、或在 Store 裡面觸發 action。一旦你把它們改寫成基於 reducer,你的 Flux 程式碼會更容易去了解! 21 | 22 | * 當你已經把所有的 Flux Store 都改成基於 reducer 之上去實作,你可以把 Flux library 置換成一個單一的 Redux store,並藉由 [`combineReducers(reducers)`](../api/combineReducers.md) 來把你已經有的這些 reducer 結合成一個。 23 | 24 | * 現在只剩下要移植 UI 來[使用 react-redux](../basics/UsageWithReact.md) 或其他類似的東西。 25 | 26 | * 最後,你可能會想要開始使用一些 Redux 特有的功能像是 middleware ,來進一步簡化你的非同步程式碼。 27 | 28 | ## 從 Backbone 29 | 30 | Backbone 的 model 層跟 Redux 有很大的差異,所以我們不建議把它們混在一起。如果可能的話,最好從頭開始重寫你的應用程式的 model 層而不是把 Backbone 連接到 Redux。然而,如果重寫是不可行的,那你可以使用 [backbone-redux](https://github.com/redbooth/backbone-redux) 來漸漸的遷移,並維持 Redux store 跟 Backbone model 和 collection 之間的同步。 31 | -------------------------------------------------------------------------------- /docs/recipes/README.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | 這裡有一些使用案例和程式碼片段讓你在實際的應用程式入門 Redux。這邊假設你已經了解[基礎](../basics/README.md)和[進階](../advanced/README.md)教學裡的主題。 4 | 5 | * [遷移到 Redux](MigratingToRedux.md) 6 | * [使用 Object Spread 運算子](UsingObjectSpreadOperator.md) 7 | * [減少 Boilerplate](ReducingBoilerplate.md) 8 | * [伺服器 Render](ServerRendering.md) 9 | * [撰寫測試](WritingTests.md) 10 | * [計算衍生資料](ComputingDerivedData.md) 11 | * [實作 Undo 歷史](ImplementingUndoHistory.md) 12 | * [分隔 Redux 子應用程式](IsolatingSubapps.md) 13 | -------------------------------------------------------------------------------- /docs/recipes/UsingObjectSpreadOperator.md: -------------------------------------------------------------------------------- 1 | # 使用 Object Spread 運算子 2 | 3 | 因為 Redux 最核心的原則之一就是永不更動 state,你將會常常發現自己使用 [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 來建立擁有新值或更新其中值的複製 object。例如,在 `todoApp` 內, `Object.assign()` 慣於用來回傳一個新的且有更新 `visibilityFilter` 屬性後的 `state` object: 4 | 5 | ```js 6 | function todoApp(state = initialState, action) { 7 | switch (action.type) { 8 | case SET_VISIBILITY_FILTER: 9 | return Object.assign({}, state, { 10 | visibilityFilter: action.filter 11 | }) 12 | default: 13 | return state 14 | } 15 | } 16 | ``` 17 | 18 | 然而大力使用 `Object.assign()` 會很快地導致簡單的 reducers 因為其相當冗長的語法而難以閱讀。 19 | 20 | 一個替代方法就是使用為了下一代 JavaScript 提出的 [object spread 語法](https://github.com/sebmarkbage/ecmascript-rest-spread),這將讓你使用 spread (`...`) 運算子來更有效地從一個 object 複製 enumerable 屬性到另一個。此 object spread 運算子概念上相似於 ES6 的 [array spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator)。我們可以經由使用 object spread 運算子來簡化 `todoApp` 範例: 21 | 22 | ```js 23 | function todoApp(state = initialState, action) { 24 | switch (action.type) { 25 | case SET_VISIBILITY_FILTER: 26 | return { ...state, visibilityFilter: action.filter } 27 | default: 28 | return state 29 | } 30 | } 31 | ``` 32 | 33 | 當你正在構成複雜的 objects時,使用 object spread 語法的好處會變的越來越明顯。下面 `getAddedIds` 用 `getProduct` 和 `getQuantity` 的回傳值 map 一個 `id` array 到另一個 object array。 34 | 35 | ```js 36 | return getAddedIds(state.cart).map(id => Object.assign( 37 | {}, 38 | getProduct(state.products, id), 39 | { 40 | quantity: getQuantity(state.cart, id) 41 | } 42 | )) 43 | ``` 44 | 45 | Object spread 讓我們簡化以上的 `map` 呼叫,成為: 46 | 47 | ```js 48 | return getAddedIds(state.cart).map(id => ({ 49 | ...getProduct(state.products, id), 50 | quantity: getQuantity(state.cart, id) 51 | })) 52 | ``` 53 | 54 | 因為 object spread 語法仍然是 ECMAScript 的 Stage 2 提案,你將需要使用像是 [Babel](http://babeljs.io/) 的 transpiler 使 production 環境下可以使用它。你可以使用已存在的 `es2015` preset,安裝 [`babel-plugin-transform-object-rest-spread`](http://babeljs.io/docs/plugins/transform-object-rest-spread/) 並逐一將它加入在你 `.babelrc` 中的 `plugins` array。 55 | 56 | ```js 57 | { 58 | "presets": ["es2015"], 59 | "plugins": ["transform-object-rest-spread"] 60 | } 61 | ``` 62 | 63 | 注意這仍然個實驗性質的功能提案語法,所以它可能在未來會改變。儘管如此,一些大專案像是 [React Native](https://github.com/facebook/react-native) 已經廣泛使用。所以可以安全的說,如果未來它真的改變了也將會有一個好的自動 migration 方式。 64 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # 範例 2 | 3 | 閱讀在 [Examples](../docs/introduction/Examples.md) 文件頁面上每個範例的敘述。 4 | -------------------------------------------------------------------------------- /examples/async/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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 | 8 | export function selectReddit(reddit) { 9 | return { 10 | type: SELECT_REDDIT, 11 | reddit 12 | } 13 | } 14 | 15 | export function invalidateReddit(reddit) { 16 | return { 17 | type: INVALIDATE_REDDIT, 18 | reddit 19 | } 20 | } 21 | 22 | function requestPosts(reddit) { 23 | return { 24 | type: REQUEST_POSTS, 25 | reddit 26 | } 27 | } 28 | 29 | function receivePosts(reddit, json) { 30 | return { 31 | type: RECEIVE_POSTS, 32 | reddit, 33 | posts: json.data.children.map(child => child.data), 34 | receivedAt: Date.now() 35 | } 36 | } 37 | 38 | function fetchPosts(reddit) { 39 | return dispatch => { 40 | dispatch(requestPosts(reddit)) 41 | return fetch(`https://www.reddit.com/r/${reddit}.json`) 42 | .then(response => response.json()) 43 | .then(json => dispatch(receivePosts(reddit, json))) 44 | } 45 | } 46 | 47 | function shouldFetchPosts(state, reddit) { 48 | const posts = state.postsByReddit[reddit] 49 | if (!posts) { 50 | return true 51 | } 52 | if (posts.isFetching) { 53 | return false 54 | } 55 | return posts.didInvalidate 56 | } 57 | 58 | export function fetchPostsIfNeeded(reddit) { 59 | return (dispatch, getState) => { 60 | if (shouldFetchPosts(getState(), reddit)) { 61 | return dispatch(fetchPosts(reddit)) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | componentDidMount() { 15 | const { dispatch, selectedReddit } = this.props 16 | dispatch(fetchPostsIfNeeded(selectedReddit)) 17 | } 18 | 19 | componentWillReceiveProps(nextProps) { 20 | if (nextProps.selectedReddit !== this.props.selectedReddit) { 21 | const { dispatch, selectedReddit } = nextProps 22 | dispatch(fetchPostsIfNeeded(selectedReddit)) 23 | } 24 | } 25 | 26 | handleChange(nextReddit) { 27 | this.props.dispatch(selectReddit(nextReddit)) 28 | } 29 | 30 | handleRefreshClick(e) { 31 | e.preventDefault() 32 | 33 | const { dispatch, selectedReddit } = this.props 34 | dispatch(invalidateReddit(selectedReddit)) 35 | dispatch(fetchPostsIfNeeded(selectedReddit)) 36 | } 37 | 38 | render() { 39 | const { selectedReddit, posts, isFetching, lastUpdated } = this.props 40 | const isEmpty = posts.length === 0 41 | return ( 42 |
43 | 46 |

47 | {lastUpdated && 48 | 49 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}. 50 | {' '} 51 | 52 | } 53 | {!isFetching && 54 | 56 | Refresh 57 | 58 | } 59 |

60 | {isEmpty 61 | ? (isFetching ?

Loading...

:

Empty.

) 62 | :
63 | 64 |
65 | } 66 |
67 | ) 68 | } 69 | } 70 | 71 | App.propTypes = { 72 | selectedReddit: PropTypes.string.isRequired, 73 | posts: PropTypes.array.isRequired, 74 | isFetching: PropTypes.bool.isRequired, 75 | lastUpdated: PropTypes.number, 76 | dispatch: PropTypes.func.isRequired 77 | } 78 | 79 | function mapStateToProps(state) { 80 | const { selectedReddit, postsByReddit } = state 81 | const { 82 | isFetching, 83 | lastUpdated, 84 | items: posts 85 | } = postsByReddit[selectedReddit] || { 86 | isFetching: true, 87 | items: [] 88 | } 89 | 90 | return { 91 | selectedReddit, 92 | posts, 93 | isFetching, 94 | lastUpdated 95 | } 96 | } 97 | 98 | export default connect(mapStateToProps)(App) 99 | -------------------------------------------------------------------------------- /examples/async/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux async example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/async/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { Provider } from 'react-redux' 5 | import App from './containers/App' 6 | import configureStore from './store/configureStore' 7 | 8 | const store = configureStore() 9 | 10 | render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ) 16 | -------------------------------------------------------------------------------- /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/reactjs/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/reactjs/redux/issues" 26 | }, 27 | "homepage": "http://redux.js.org", 28 | "dependencies": { 29 | "babel-polyfill": "^6.3.14", 30 | "isomorphic-fetch": "^2.1.1", 31 | "react": "^0.14.7", 32 | "react-dom": "^0.14.7", 33 | "react-redux": "^4.2.1", 34 | "redux": "^3.2.1", 35 | "redux-logger": "^2.4.0", 36 | "redux-thunk": "^1.0.3" 37 | }, 38 | "devDependencies": { 39 | "babel-core": "^6.3.15", 40 | "babel-loader": "^6.2.0", 41 | "babel-preset-es2015": "^6.3.13", 42 | "babel-preset-react": "^6.3.13", 43 | "babel-preset-react-hmre": "^1.1.1", 44 | "expect": "^1.6.0", 45 | "express": "^4.13.3", 46 | "node-libs-browser": "^0.5.2", 47 | "webpack": "^1.9.11", 48 | "webpack-dev-middleware": "^1.2.0", 49 | "webpack-hot-middleware": "^2.9.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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 | function selectedReddit(state = 'reactjs', action) { 8 | switch (action.type) { 9 | case SELECT_REDDIT: 10 | return action.reddit 11 | default: 12 | return state 13 | } 14 | } 15 | 16 | function posts(state = { 17 | isFetching: false, 18 | didInvalidate: false, 19 | items: [] 20 | }, action) { 21 | switch (action.type) { 22 | case INVALIDATE_REDDIT: 23 | return Object.assign({}, state, { 24 | didInvalidate: true 25 | }) 26 | case REQUEST_POSTS: 27 | return Object.assign({}, state, { 28 | isFetching: true, 29 | didInvalidate: false 30 | }) 31 | case RECEIVE_POSTS: 32 | return Object.assign({}, state, { 33 | isFetching: false, 34 | didInvalidate: false, 35 | items: action.posts, 36 | lastUpdated: action.receivedAt 37 | }) 38 | default: 39 | return state 40 | } 41 | } 42 | 43 | function postsByReddit(state = { }, action) { 44 | switch (action.type) { 45 | case INVALIDATE_REDDIT: 46 | case RECEIVE_POSTS: 47 | case REQUEST_POSTS: 48 | return Object.assign({}, state, { 49 | [action.reddit]: posts(state[action.reddit], action) 50 | }) 51 | default: 52 | return state 53 | } 54 | } 55 | 56 | const rootReducer = combineReducers({ 57 | postsByReddit, 58 | selectedReddit 59 | }) 60 | 61 | export default rootReducer 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | export default function configureStore(preloadedState) { 7 | const store = createStore( 8 | rootReducer, 9 | preloadedState, 10 | applyMiddleware(thunkMiddleware, createLogger()) 11 | ) 12 | 13 | if (module.hot) { 14 | // Enable Webpack hot module replacement for reducers 15 | module.hot.accept('../reducers', () => { 16 | const nextRootReducer = require('../reducers').default 17 | store.replaceReducer(nextRootReducer) 18 | }) 19 | } 20 | 21 | return store 22 | } 23 | -------------------------------------------------------------------------------- /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.OccurrenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | loaders: ['babel'], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/buildAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs an ordered set of commands within each of the build directories. 3 | */ 4 | 5 | import fs from 'fs' 6 | import path from 'path' 7 | import { spawnSync } from 'child_process' 8 | 9 | var exampleDirs = fs.readdirSync(__dirname).filter((file) => { 10 | return fs.statSync(path.join(__dirname, file)).isDirectory() 11 | }) 12 | 13 | // Ordering is important here. `npm install` must come first. 14 | var cmdArgs = [ 15 | { cmd: 'npm', args: [ 'install' ] }, 16 | { cmd: 'webpack', args: [ 'index.js' ] } 17 | ] 18 | 19 | for (const dir of exampleDirs) { 20 | for (const cmdArg of cmdArgs) { 21 | // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 22 | const opts = { 23 | cwd: path.join(__dirname, dir), 24 | stdio: 'inherit' 25 | } 26 | let result = {} 27 | if (process.platform === 'win32') { 28 | result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts) 29 | } else { 30 | result = spawnSync(cmdArg.cmd, cmdArg.args, opts) 31 | } 32 | if (result.status !== 0) { 33 | throw new Error('Building examples exited with non-zero') 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/counter-vanilla/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux basic example 5 | 6 | 7 | 8 |
9 |

10 | Clicked: 0 times 11 | 12 | 13 | 14 | 15 |

16 |
17 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/counter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/counter/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | class Counter extends Component { 4 | constructor(props) { 5 | super(props) 6 | this.incrementAsync = this.incrementAsync.bind(this) 7 | this.incrementIfOdd = this.incrementIfOdd.bind(this) 8 | } 9 | 10 | incrementIfOdd() { 11 | if (this.props.value % 2 !== 0) { 12 | this.props.onIncrement() 13 | } 14 | } 15 | 16 | incrementAsync() { 17 | setTimeout(this.props.onIncrement, 1000) 18 | } 19 | 20 | render() { 21 | const { value, onIncrement, onDecrement } = this.props 22 | return ( 23 |

24 | Clicked: {value} times 25 | {' '} 26 | 29 | {' '} 30 | 33 | {' '} 34 | 37 | {' '} 38 | 41 |

42 | ) 43 | } 44 | } 45 | 46 | Counter.propTypes = { 47 | value: PropTypes.number.isRequired, 48 | onIncrement: PropTypes.func.isRequired, 49 | onDecrement: PropTypes.func.isRequired 50 | } 51 | 52 | export default Counter 53 | -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux counter example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/counter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { createStore } from 'redux' 4 | import Counter from './components/Counter' 5 | import counter from './reducers' 6 | 7 | const store = createStore(counter) 8 | const rootEl = document.getElementById('root') 9 | 10 | function render() { 11 | ReactDOM.render( 12 | store.dispatch({ type: 'INCREMENT' })} 15 | onDecrement={() => store.dispatch({ type: 'DECREMENT' })} 16 | />, 17 | rootEl 18 | ) 19 | } 20 | 21 | render() 22 | store.subscribe(render) 23 | -------------------------------------------------------------------------------- /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": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register", 8 | "test:watch": "npm test -- --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/reactjs/redux.git" 13 | }, 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/reactjs/redux/issues" 17 | }, 18 | "homepage": "http://redux.js.org", 19 | "dependencies": { 20 | "react": "^0.14.7", 21 | "react-dom": "^0.14.7", 22 | "react-redux": "^4.2.1", 23 | "redux": "^3.2.1" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.3.15", 27 | "babel-loader": "^6.2.0", 28 | "babel-preset-es2015": "^6.3.13", 29 | "babel-preset-react": "^6.3.13", 30 | "babel-preset-react-hmre": "^1.1.1", 31 | "babel-register": "^6.3.13", 32 | "cross-env": "^1.0.7", 33 | "enzyme": "^2.0.0", 34 | "expect": "^1.6.0", 35 | "express": "^4.13.3", 36 | "mocha": "^2.2.5", 37 | "node-libs-browser": "^0.5.2", 38 | "react-addons-test-utils": "^0.14.7", 39 | "webpack": "^1.9.11", 40 | "webpack-dev-middleware": "^1.2.0", 41 | "webpack-hot-middleware": "^2.9.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/counter/reducers/index.js: -------------------------------------------------------------------------------- 1 | export default function counter(state = 0, action) { 2 | switch (action.type) { 3 | case 'INCREMENT': 4 | return state + 1 5 | case 'DECREMENT': 6 | return state - 1 7 | default: 8 | return state 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/counter/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/counter/test/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import Counter from '../../components/Counter' 5 | 6 | function setup(value = 0) { 7 | const actions = { 8 | onIncrement: expect.createSpy(), 9 | onDecrement: expect.createSpy() 10 | } 11 | const component = shallow( 12 | 13 | ) 14 | 15 | return { 16 | component: component, 17 | actions: actions, 18 | buttons: component.find('button'), 19 | p: component.find('p') 20 | } 21 | } 22 | 23 | describe('Counter component', () => { 24 | it('should display count', () => { 25 | const { p } = setup() 26 | expect(p.text()).toMatch(/^Clicked: 0 times/) 27 | }) 28 | 29 | it('first button should call onIncrement', () => { 30 | const { buttons, actions } = setup() 31 | buttons.at(0).simulate('click') 32 | expect(actions.onIncrement).toHaveBeenCalled() 33 | }) 34 | 35 | it('second button should call onDecrement', () => { 36 | const { buttons, actions } = setup() 37 | buttons.at(1).simulate('click') 38 | expect(actions.onDecrement).toHaveBeenCalled() 39 | }) 40 | 41 | it('third button should not call onIncrement if the counter is even', () => { 42 | const { buttons, actions } = setup(42) 43 | buttons.at(2).simulate('click') 44 | expect(actions.onIncrement).toNotHaveBeenCalled() 45 | }) 46 | 47 | it('third button should call onIncrement if the counter is odd', () => { 48 | const { buttons, actions } = setup(43) 49 | buttons.at(2).simulate('click') 50 | expect(actions.onIncrement).toHaveBeenCalled() 51 | }) 52 | 53 | it('third button should call onIncrement if the counter is odd and negative', () => { 54 | const { buttons, actions } = setup(-43) 55 | buttons.at(2).simulate('click') 56 | expect(actions.onIncrement).toHaveBeenCalled() 57 | }) 58 | 59 | it('fourth button should call onIncrement in a second', (done) => { 60 | const { buttons, actions } = setup() 61 | buttons.at(3).simulate('click') 62 | setTimeout(() => { 63 | expect(actions.onIncrement).toHaveBeenCalled() 64 | done() 65 | }, 1000) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /examples/counter/test/reducers/counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import counter from '../../reducers' 3 | 4 | describe('reducers', () => { 5 | describe('counter', () => { 6 | it('should provide the initial state', () => { 7 | expect(counter(undefined, {})).toBe(0) 8 | }) 9 | 10 | it('should handle INCREMENT action', () => { 11 | expect(counter(1, { type: 'INCREMENT' })).toBe(2) 12 | }) 13 | 14 | it('should handle DECREMENT action', () => { 15 | expect(counter(1, { type: 'DECREMENT' })).toBe(0) 16 | }) 17 | 18 | it('should ignore unknown actions', () => { 19 | expect(counter(1, { type: 'unknown' })).toBe(1) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /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.OccurrenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/real-world/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/real-world/components/Explore.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | const GITHUB_REPO = 'https://github.com/reactjs/redux' 4 | 5 | export default class Explore extends Component { 6 | constructor(props) { 7 | super(props) 8 | this.handleKeyUp = this.handleKeyUp.bind(this) 9 | this.handleGoClick = this.handleGoClick.bind(this) 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | if (nextProps.value !== this.props.value) { 14 | this.setInputValue(nextProps.value) 15 | } 16 | } 17 | 18 | getInputValue() { 19 | return this.refs.input.value 20 | } 21 | 22 | setInputValue(val) { 23 | // Generally mutating DOM is a bad idea in React components, 24 | // but doing this for a single uncontrolled field is less fuss 25 | // than making it controlled and maintaining a state for it. 26 | this.refs.input.value = val 27 | } 28 | 29 | handleKeyUp(e) { 30 | if (e.keyCode === 13) { 31 | this.handleGoClick() 32 | } 33 | } 34 | 35 | handleGoClick() { 36 | this.props.onChange(this.getInputValue()) 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |

Type a username or repo full name and hit 'Go':

43 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/real-world/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { browserHistory } from 'react-router' 4 | import Explore from '../components/Explore' 5 | import { resetErrorMessage } from '../actions' 6 | 7 | class App extends Component { 8 | constructor(props) { 9 | super(props) 10 | this.handleChange = this.handleChange.bind(this) 11 | this.handleDismissClick = this.handleDismissClick.bind(this) 12 | } 13 | 14 | handleDismissClick(e) { 15 | this.props.resetErrorMessage() 16 | e.preventDefault() 17 | } 18 | 19 | handleChange(nextValue) { 20 | browserHistory.push(`/${nextValue}`) 21 | } 22 | 23 | renderErrorMessage() { 24 | const { errorMessage } = this.props 25 | if (!errorMessage) { 26 | return null 27 | } 28 | 29 | return ( 30 |

31 | {errorMessage} 32 | {' '} 33 | ( 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 | inputValue: PropTypes.string.isRequired, 60 | // Injected by React Router 61 | children: PropTypes.node 62 | } 63 | 64 | function mapStateToProps(state, ownProps) { 65 | return { 66 | errorMessage: state.errorMessage, 67 | inputValue: ownProps.location.pathname.substring(1) 68 | } 69 | } 70 | 71 | export default connect(mapStateToProps, { 72 | resetErrorMessage 73 | })(App) 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/real-world/containers/Root.dev.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Provider } from 'react-redux' 3 | import routes from '../routes' 4 | import DevTools from './DevTools' 5 | import { Router } from 'react-router' 6 | 7 | export default class Root extends Component { 8 | render() { 9 | const { store, history } = this.props 10 | return ( 11 | 12 |
13 | 14 | 15 |
16 |
17 | ) 18 | } 19 | } 20 | 21 | Root.propTypes = { 22 | store: PropTypes.object.isRequired, 23 | history: PropTypes.object.isRequired 24 | } 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/real-world/containers/Root.prod.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Provider } from 'react-redux' 3 | import routes from '../routes' 4 | import { Router } from 'react-router' 5 | 6 | export default class Root extends Component { 7 | render() { 8 | const { store, history } = this.props 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | } 16 | 17 | Root.propTypes = { 18 | store: PropTypes.object.isRequired, 19 | history: PropTypes.object.isRequired 20 | } 21 | -------------------------------------------------------------------------------- /examples/real-world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux real-world example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/real-world/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { browserHistory } from 'react-router' 5 | import { syncHistoryWithStore } from 'react-router-redux' 6 | import Root from './containers/Root' 7 | import configureStore from './store/configureStore' 8 | 9 | const store = configureStore() 10 | const history = syncHistoryWithStore(browserHistory, store) 11 | 12 | render( 13 | , 14 | document.getElementById('root') 15 | ) 16 | -------------------------------------------------------------------------------- /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/reactjs/redux.git" 11 | }, 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/reactjs/redux/issues" 15 | }, 16 | "homepage": "http://redux.js.org", 17 | "dependencies": { 18 | "babel-polyfill": "^6.3.14", 19 | "humps": "^0.6.0", 20 | "isomorphic-fetch": "^2.1.1", 21 | "lodash": "^4.0.0", 22 | "normalizr": "^2.0.0", 23 | "react": "^0.14.7", 24 | "react-dom": "^0.14.7", 25 | "react-redux": "^4.2.1", 26 | "react-router": "2.0.0", 27 | "react-router-redux": "^4.0.0-rc.1", 28 | "redux": "^3.2.1", 29 | "redux-logger": "^2.4.0", 30 | "redux-thunk": "^1.0.3" 31 | }, 32 | "devDependencies": { 33 | "babel-core": "^6.3.15", 34 | "babel-loader": "^6.2.0", 35 | "babel-preset-es2015": "^6.3.13", 36 | "babel-preset-react": "^6.3.13", 37 | "babel-preset-react-hmre": "^1.1.1", 38 | "concurrently": "^0.1.1", 39 | "express": "^4.13.3", 40 | "redux-devtools": "^3.1.0", 41 | "redux-devtools-dock-monitor": "^1.0.1", 42 | "redux-devtools-log-monitor": "^1.0.3", 43 | "webpack": "^1.9.11", 44 | "webpack-dev-middleware": "^1.2.0", 45 | "webpack-hot-middleware": "^2.9.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/real-world/reducers/index.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions' 2 | import merge from 'lodash/merge' 3 | import paginate from './paginate' 4 | import { routerReducer as routing } from 'react-router-redux' 5 | import { combineReducers } from 'redux' 6 | 7 | // Updates an entity cache in response to any action with response.entities. 8 | function entities(state = { users: {}, repos: {} }, action) { 9 | if (action.response && action.response.entities) { 10 | return merge({}, state, action.response.entities) 11 | } 12 | 13 | return state 14 | } 15 | 16 | // Updates error message to notify about the failed fetches. 17 | function errorMessage(state = null, action) { 18 | const { type, error } = action 19 | 20 | if (type === ActionTypes.RESET_ERROR_MESSAGE) { 21 | return null 22 | } else if (error) { 23 | return action.error 24 | } 25 | 26 | return state 27 | } 28 | 29 | // Updates the pagination data for different actions. 30 | const pagination = combineReducers({ 31 | starredByUser: paginate({ 32 | mapActionToKey: action => action.login, 33 | types: [ 34 | ActionTypes.STARRED_REQUEST, 35 | ActionTypes.STARRED_SUCCESS, 36 | ActionTypes.STARRED_FAILURE 37 | ] 38 | }), 39 | stargazersByRepo: paginate({ 40 | mapActionToKey: action => action.fullName, 41 | types: [ 42 | ActionTypes.STARGAZERS_REQUEST, 43 | ActionTypes.STARGAZERS_SUCCESS, 44 | ActionTypes.STARGAZERS_FAILURE 45 | ] 46 | }) 47 | }) 48 | 49 | const rootReducer = combineReducers({ 50 | entities, 51 | pagination, 52 | errorMessage, 53 | routing 54 | }) 55 | 56 | export default rootReducer 57 | -------------------------------------------------------------------------------- /examples/real-world/reducers/paginate.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge' 2 | import union from 'lodash/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/real-world/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import createLogger from 'redux-logger' 4 | import api from '../middleware/api' 5 | import rootReducer from '../reducers' 6 | import DevTools from '../containers/DevTools' 7 | 8 | export default function configureStore(preloadedState) { 9 | const store = createStore( 10 | rootReducer, 11 | preloadedState, 12 | compose( 13 | applyMiddleware(thunk, api, createLogger()), 14 | DevTools.instrument() 15 | ) 16 | ) 17 | 18 | if (module.hot) { 19 | // Enable Webpack hot module replacement for reducers 20 | module.hot.accept('../reducers', () => { 21 | const nextRootReducer = require('../reducers').default 22 | store.replaceReducer(nextRootReducer) 23 | }) 24 | } 25 | 26 | return store 27 | } 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/real-world/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import api from '../middleware/api' 4 | import rootReducer from '../reducers' 5 | 6 | export default function configureStore(preloadedState) { 7 | return createStore( 8 | rootReducer, 9 | preloadedState, 10 | applyMiddleware(thunk, api) 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /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.OccurrenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/shopping-cart/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/shopping-cart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux shopping cart example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/shopping-cart/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { createStore, applyMiddleware } from 'redux' 5 | import { Provider } from 'react-redux' 6 | import logger from 'redux-logger' 7 | import thunk from 'redux-thunk' 8 | import reducer from './reducers' 9 | import { getAllProducts } from './actions' 10 | import App from './containers/App' 11 | 12 | const middleware = process.env.NODE_ENV === 'production' ? 13 | [ thunk ] : 14 | [ thunk, logger() ] 15 | 16 | const store = createStore( 17 | reducer, 18 | applyMiddleware(...middleware) 19 | ) 20 | 21 | store.dispatch(getAllProducts()) 22 | 23 | render( 24 | 25 | 26 | , 27 | document.getElementById('root') 28 | ) 29 | -------------------------------------------------------------------------------- /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 | "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register", 8 | "test:watch": "npm test -- --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/reactjs/redux.git" 13 | }, 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/reactjs/redux/issues" 17 | }, 18 | "homepage": "http://redux.js.org", 19 | "dependencies": { 20 | "babel-polyfill": "^6.3.14", 21 | "react": "^0.14.7", 22 | "react-dom": "^0.14.7", 23 | "react-redux": "^4.2.1", 24 | "redux": "^3.2.1", 25 | "redux-thunk": "^1.0.3" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^6.3.15", 29 | "babel-loader": "^6.2.0", 30 | "babel-preset-es2015": "^6.3.13", 31 | "babel-preset-react": "^6.3.13", 32 | "babel-preset-react-hmre": "^1.1.1", 33 | "cross-env": "^1.0.7", 34 | "enzyme": "^2.0.0", 35 | "expect": "^1.20.1", 36 | "express": "^4.13.3", 37 | "json-loader": "^0.5.3", 38 | "react-addons-test-utils": "^0.14.7", 39 | "redux-logger": "^2.0.1", 40 | "mocha": "^2.2.5", 41 | "node-libs-browser": "^0.5.2", 42 | "webpack": "^1.9.11", 43 | "webpack-dev-middleware": "^1.2.0", 44 | "webpack-hot-middleware": "^2.9.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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 Object.assign({}, state, { 29 | [productId]: (state[productId] || 0) + 1 30 | }) 31 | default: 32 | return state 33 | } 34 | } 35 | 36 | export default function cart(state = initialState, action) { 37 | switch (action.type) { 38 | case CHECKOUT_REQUEST: 39 | return initialState 40 | case CHECKOUT_FAILURE: 41 | return action.cart 42 | default: 43 | return { 44 | addedIds: addedIds(state.addedIds, action), 45 | quantityById: quantityById(state.quantityById, action) 46 | } 47 | } 48 | } 49 | 50 | export function getQuantity(state, productId) { 51 | return state.quantityById[productId] || 0 52 | } 53 | 54 | export function getAddedIds(state) { 55 | return state.addedIds 56 | } 57 | -------------------------------------------------------------------------------- /examples/shopping-cart/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import cart, * as fromCart from './cart' 3 | import products, * as fromProducts from './products' 4 | 5 | export default combineReducers({ 6 | cart, 7 | products 8 | }) 9 | 10 | function getAddedIds(state) { 11 | return fromCart.getAddedIds(state.cart) 12 | } 13 | 14 | function getQuantity(state, id) { 15 | return fromCart.getQuantity(state.cart, id) 16 | } 17 | 18 | function getProduct(state, id) { 19 | return fromProducts.getProduct(state.products, id) 20 | } 21 | 22 | export function getTotal(state) { 23 | return getAddedIds(state).reduce((total, id) => 24 | total + getProduct(state, id).price * getQuantity(state, id), 25 | 0 26 | ).toFixed(2) 27 | } 28 | 29 | export function getCartProducts(state) { 30 | return getAddedIds(state).map(id => Object.assign( 31 | {}, 32 | getProduct(state, id), 33 | { 34 | quantity: getQuantity(state, id) 35 | } 36 | )) 37 | } 38 | 39 | -------------------------------------------------------------------------------- /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 Object.assign({}, state, { 8 | inventory: state.inventory - 1 9 | }) 10 | default: 11 | return state 12 | } 13 | } 14 | 15 | function byId(state = {}, action) { 16 | switch (action.type) { 17 | case RECEIVE_PRODUCTS: 18 | return Object.assign({}, 19 | state, 20 | action.products.reduce((obj, product) => { 21 | obj[product.id] = product 22 | return obj 23 | }, {}) 24 | ) 25 | default: 26 | const { productId } = action 27 | if (productId) { 28 | return Object.assign({}, state, { 29 | [productId]: products(state[productId], action) 30 | }) 31 | } 32 | return state 33 | } 34 | } 35 | 36 | function visibleIds(state = [], action) { 37 | switch (action.type) { 38 | case RECEIVE_PRODUCTS: 39 | return action.products.map(product => product.id) 40 | default: 41 | return state 42 | } 43 | } 44 | 45 | export default combineReducers({ 46 | byId, 47 | visibleIds 48 | }) 49 | 50 | export function getProduct(state, id) { 51 | return state.byId[id] 52 | } 53 | 54 | export function getVisibleProducts(state) { 55 | return state.visibleIds.map(id => getProduct(state, id)) 56 | } 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/shopping-cart/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/shopping-cart/test/components/Cart.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import Cart from '../../components/Cart' 5 | import Product from '../../components/Product' 6 | 7 | function setup(total, products = []) { 8 | const actions = { 9 | onCheckoutClicked: expect.createSpy() 10 | } 11 | 12 | const component = shallow( 13 | 14 | ) 15 | 16 | return { 17 | component: component, 18 | actions: actions, 19 | button: component.find('button'), 20 | products: component.find(Product), 21 | em: component.find('em'), 22 | p: component.find('p') 23 | } 24 | } 25 | 26 | describe('Cart component', () => { 27 | it('should display total', () => { 28 | const { p } = setup('76') 29 | expect(p.text()).toMatch(/^Total: \$76/) 30 | }) 31 | 32 | it('should display add some products message', () => { 33 | const { em } = setup() 34 | expect(em.text()).toMatch(/^Please add some products to cart/) 35 | }) 36 | 37 | it('should disable button', () => { 38 | const { button } = setup() 39 | expect(button.prop('disabled')).toEqual('disabled') 40 | }) 41 | 42 | describe('when given product', () => { 43 | const product = [ 44 | { 45 | id: 1, 46 | title: 'Product 1', 47 | price: 9.99, 48 | quantity: 1 49 | } 50 | ] 51 | 52 | it('should render products', () => { 53 | const { products } = setup('9.99', product) 54 | const props = { 55 | title: product[0].title, 56 | price: product[0].price, 57 | quantity: product[0].quantity 58 | } 59 | 60 | expect(products.at(0).props()).toEqual(props) 61 | }) 62 | 63 | it('should not disable button', () => { 64 | const { button } = setup('9.99', product) 65 | expect(button.prop('disabled')).toEqual('') 66 | }) 67 | 68 | it('should call action on button click', () => { 69 | const { button, actions } = setup('9.99', product) 70 | button.simulate('click') 71 | expect(actions.onCheckoutClicked).toHaveBeenCalled() 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /examples/shopping-cart/test/components/Product.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import Product from '../../components/Product' 5 | 6 | function setup(props) { 7 | const component = shallow( 8 | 9 | ) 10 | 11 | return { 12 | component: component 13 | } 14 | } 15 | 16 | describe('Product component', () => { 17 | it('should render title and price', () => { 18 | const { component } = setup({ title: 'Test Product', price: 9.99 }) 19 | expect(component.text()).toMatch(/^ Test Product - \$9.99 {2}$/) 20 | }) 21 | 22 | describe('when given quantity', () => { 23 | it('should render title, price, and quantity', () => { 24 | const { component } = setup({ title: 'Test Product', price: 9.99, quantity: 6 }) 25 | expect(component.text()).toMatch(/^ Test Product - \$9.99 x 6 $/) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /examples/shopping-cart/test/components/ProductItem.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import Product from '../../components/Product' 5 | import ProductItem from '../../components/ProductItem' 6 | 7 | function setup(product) { 8 | const actions = { 9 | onAddToCartClicked: expect.createSpy() 10 | } 11 | 12 | const component = shallow( 13 | 14 | ) 15 | 16 | return { 17 | component: component, 18 | actions: actions, 19 | button: component.find('button'), 20 | product: component.find(Product) 21 | } 22 | } 23 | 24 | let productProps 25 | 26 | describe('ProductItem component', () => { 27 | beforeEach(() => { 28 | productProps = { 29 | title: 'Product 1', 30 | price: 9.99, 31 | inventory: 6 32 | } 33 | }) 34 | 35 | it('should render product', () => { 36 | const { product } = setup(productProps) 37 | expect(product.props()).toEqual({ title: 'Product 1', price: 9.99 }) 38 | }) 39 | 40 | it('should render Add To Cart message', () => { 41 | const { button } = setup(productProps) 42 | expect(button.text()).toMatch(/^Add to cart/) 43 | }) 44 | 45 | it('should not disable button', () => { 46 | const { button } = setup(productProps) 47 | expect(button.prop('disabled')).toEqual('') 48 | }) 49 | 50 | it('should call action on button click', () => { 51 | const { button, actions } = setup(productProps) 52 | button.simulate('click') 53 | expect(actions.onAddToCartClicked).toHaveBeenCalled() 54 | }) 55 | 56 | describe('when product inventory is 0', () => { 57 | beforeEach(() => { 58 | productProps.inventory = 0 59 | }) 60 | 61 | it('should render Sold Out message', () => { 62 | const { button } = setup(productProps) 63 | expect(button.text()).toMatch(/^Sold Out/) 64 | }) 65 | 66 | it('should disable button', () => { 67 | const { button } = setup(productProps) 68 | expect(button.prop('disabled')).toEqual('disabled') 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /examples/shopping-cart/test/components/ProductsList.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import ProductsList from '../../components/ProductsList' 5 | 6 | function setup(props) { 7 | const component = shallow( 8 | {props.children} 9 | ) 10 | 11 | return { 12 | component: component, 13 | children: component.children().at(1), 14 | h3: component.find('h3') 15 | } 16 | } 17 | 18 | describe('ProductsList component', () => { 19 | it('should render title', () => { 20 | const { h3 } = setup({ title: 'Test Products' }) 21 | expect(h3.text()).toMatch(/^Test Products$/) 22 | }) 23 | 24 | it('should render children', () => { 25 | const { children } = setup({ title: 'Test Products', children: 'Test Children' }) 26 | expect(children.text()).toMatch(/^Test Children$/) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /examples/shopping-cart/test/reducers/cart.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import cart from '../../reducers/cart' 3 | 4 | describe('reducers', () => { 5 | describe('cart', () => { 6 | const initialState = { 7 | addedIds: [], 8 | quantityById: {} 9 | } 10 | 11 | it('should provide the initial state', () => { 12 | expect(cart(undefined, {})).toEqual(initialState) 13 | }) 14 | 15 | it('should handle CHECKOUT_REQUEST action', () => { 16 | expect(cart({}, { type: 'CHECKOUT_REQUEST' })).toEqual(initialState) 17 | }) 18 | 19 | it('should handle CHECKOUT_FAILURE action', () => { 20 | expect(cart({}, { type: 'CHECKOUT_FAILURE', cart: 'cart state' })).toEqual('cart state') 21 | }) 22 | 23 | it('should handle ADD_TO_CART action', () => { 24 | expect(cart(initialState, { type: 'ADD_TO_CART', productId: 1 })).toEqual({ 25 | addedIds: [ 1 ], 26 | quantityById: { 1: 1 } 27 | }) 28 | }) 29 | 30 | describe('when product is already in cart', () => { 31 | it('should handle ADD_TO_CART action', () => { 32 | const state = { 33 | addedIds: [ 1, 2 ], 34 | quantityById: { 1: 1, 2: 1 } 35 | } 36 | 37 | expect(cart(state, { type: 'ADD_TO_CART', productId: 2 })).toEqual({ 38 | addedIds: [ 1, 2 ], 39 | quantityById: { 1: 1, 2: 2 } 40 | }) 41 | }) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /examples/shopping-cart/test/reducers/products.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import products from '../../reducers/products' 3 | 4 | describe('reducers', () => { 5 | describe('products', () => { 6 | it('should handle RECEIVE_PRODUCTS action', () => { 7 | const action = { 8 | type: 'RECEIVE_PRODUCTS', 9 | products: [ 10 | { 11 | id: 1, 12 | title: 'Product 1' 13 | }, 14 | { 15 | id: 2, 16 | title: 'Product 2' 17 | } 18 | ] 19 | } 20 | 21 | expect(products({}, action)).toEqual({ 22 | byId: { 23 | 1: { 24 | id: 1, 25 | title: 'Product 1' 26 | }, 27 | 2: { 28 | id: 2, 29 | title: 'Product 2' 30 | } 31 | }, 32 | visibleIds: [ 1, 2 ] 33 | }) 34 | }) 35 | 36 | it('should handle ADD_TO_CART action', () => { 37 | const state = { 38 | byId: { 39 | 1: { 40 | id: 1, 41 | title: 'Product 1', 42 | inventory: 1 43 | } 44 | } 45 | } 46 | 47 | expect(products(state, { type: 'ADD_TO_CART', productId: 1 })).toEqual({ 48 | byId: { 49 | 1: { 50 | id: 1, 51 | title: 'Product 1', 52 | inventory: 0 53 | } 54 | }, 55 | visibleIds: [] 56 | }) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /examples/shopping-cart/test/reducers/selectors.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { getTotal, getCartProducts } from '../../reducers' 3 | 4 | describe('selectors', () => { 5 | describe('getTotal', () => { 6 | it('should return price total', () => { 7 | const state = { 8 | cart: { 9 | addedIds: [ 1, 2, 3 ], 10 | quantityById: { 11 | 1: 4, 12 | 2: 2, 13 | 3: 1 14 | } 15 | }, 16 | products: { 17 | byId: { 18 | 1: { 19 | id: 1, 20 | price: 1.99 21 | }, 22 | 2: { 23 | id: 1, 24 | price: 4.99 25 | }, 26 | 3: { 27 | id: 1, 28 | price: 9.99 29 | } 30 | } 31 | } 32 | } 33 | expect(getTotal(state)).toBe('27.93') 34 | }) 35 | }) 36 | 37 | describe('getCartProducts', () => { 38 | it('should return products with quantity', () => { 39 | const state = { 40 | cart: { 41 | addedIds: [ 1, 2, 3 ], 42 | quantityById: { 43 | 1: 4, 44 | 2: 2, 45 | 3: 1 46 | } 47 | }, 48 | products: { 49 | byId: { 50 | 1: { 51 | id: 1, 52 | price: 1.99 53 | }, 54 | 2: { 55 | id: 1, 56 | price: 4.99 57 | }, 58 | 3: { 59 | id: 1, 60 | price: 9.99 61 | } 62 | } 63 | } 64 | } 65 | 66 | expect(getCartProducts(state)).toEqual([ 67 | { 68 | id: 1, 69 | price: 1.99, 70 | quantity: 4 71 | }, 72 | { 73 | id: 1, 74 | price: 4.99, 75 | quantity: 2 76 | }, 77 | { 78 | id: 1, 79 | price: 9.99, 80 | quantity: 1 81 | } 82 | ]) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /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.OccurrenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | }, 27 | { 28 | test: /\.json$/, 29 | loaders: [ 'json' ], 30 | exclude: /node_modules/, 31 | include: __dirname 32 | } 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/testAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs an ordered set of commands within each of the build directories. 3 | */ 4 | 5 | import fs from 'fs' 6 | import path from 'path' 7 | import { spawnSync } from 'child_process' 8 | 9 | var exampleDirs = fs.readdirSync(__dirname).filter((file) => { 10 | return fs.statSync(path.join(__dirname, file)).isDirectory() 11 | }) 12 | 13 | // Ordering is important here. `npm install` must come first. 14 | var cmdArgs = [ 15 | { cmd: 'npm', args: [ 'install' ] }, 16 | { cmd: 'npm', args: [ 'test' ] } 17 | ] 18 | 19 | for (const dir of exampleDirs) { 20 | for (const cmdArg of cmdArgs) { 21 | // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 22 | const opts = { 23 | cwd: path.join(__dirname, dir), 24 | stdio: 'inherit' 25 | } 26 | 27 | let result = {} 28 | if (process.platform === 'win32') { 29 | result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts) 30 | } else { 31 | result = spawnSync(cmdArg.cmd, cmdArg.args, opts) 32 | } 33 | if (result.status !== 0) { 34 | throw new Error('Building examples exited with non-zero') 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/todomvc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/todomvc/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | 3 | export function addTodo(text) { 4 | return { type: types.ADD_TODO, text } 5 | } 6 | 7 | export function deleteTodo(id) { 8 | return { type: types.DELETE_TODO, id } 9 | } 10 | 11 | export function editTodo(id, text) { 12 | return { type: types.EDIT_TODO, id, text } 13 | } 14 | 15 | export function completeTodo(id) { 16 | return { type: types.COMPLETE_TODO, id } 17 | } 18 | 19 | export function completeAll() { 20 | return { type: types.COMPLETE_ALL } 21 | } 22 | 23 | export function clearCompleted() { 24 | return { type: types.CLEAR_COMPLETED } 25 | } 26 | -------------------------------------------------------------------------------- /examples/todomvc/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import classnames from 'classnames' 3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 4 | 5 | const FILTER_TITLES = { 6 | [SHOW_ALL]: 'All', 7 | [SHOW_ACTIVE]: 'Active', 8 | [SHOW_COMPLETED]: 'Completed' 9 | } 10 | 11 | class Footer extends Component { 12 | renderTodoCount() { 13 | const { activeCount } = this.props 14 | const itemWord = activeCount === 1 ? 'item' : 'items' 15 | 16 | return ( 17 | 18 | {activeCount || 'No'} {itemWord} left 19 | 20 | ) 21 | } 22 | 23 | renderFilterLink(filter) { 24 | const title = FILTER_TITLES[filter] 25 | const { filter: selectedFilter, onShow } = this.props 26 | 27 | return ( 28 | onShow(filter)}> 31 | {title} 32 | 33 | ) 34 | } 35 | 36 | renderClearButton() { 37 | const { completedCount, onClearCompleted } = this.props 38 | if (completedCount > 0) { 39 | return ( 40 | 44 | ) 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 |
51 | {this.renderTodoCount()} 52 |
    53 | {[ SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED ].map(filter => 54 |
  • 55 | {this.renderFilterLink(filter)} 56 |
  • 57 | )} 58 |
59 | {this.renderClearButton()} 60 |
61 | ) 62 | } 63 | } 64 | 65 | Footer.propTypes = { 66 | completedCount: PropTypes.number.isRequired, 67 | activeCount: PropTypes.number.isRequired, 68 | filter: PropTypes.string.isRequired, 69 | onClearCompleted: PropTypes.func.isRequired, 70 | onShow: PropTypes.func.isRequired 71 | } 72 | 73 | export default Footer 74 | -------------------------------------------------------------------------------- /examples/todomvc/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import TodoTextInput from './TodoTextInput' 3 | 4 | class Header extends Component { 5 | handleSave(text) { 6 | if (text.length !== 0) { 7 | this.props.addTodo(text) 8 | } 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |

todos

15 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | Header.propTypes = { 24 | addTodo: PropTypes.func.isRequired 25 | } 26 | 27 | export default Header 28 | -------------------------------------------------------------------------------- /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 | this.props.actions.clearCompleted() 20 | } 21 | 22 | handleShow(filter) { 23 | this.setState({ filter }) 24 | } 25 | 26 | renderToggleAll(completedCount) { 27 | const { todos, actions } = this.props 28 | if (todos.length > 0) { 29 | return ( 30 | 34 | ) 35 | } 36 | } 37 | 38 | renderFooter(completedCount) { 39 | const { todos } = this.props 40 | const { filter } = this.state 41 | const activeCount = todos.length - completedCount 42 | 43 | if (todos.length) { 44 | return ( 45 |