├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE-logo.md ├── LICENSE.md ├── PATRONS.md ├── README.md ├── SUMMARY.md ├── book.json ├── build ├── gitbook.css └── 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 │ ├── StructuringReducers.md │ ├── UsingObjectSpreadOperator.md │ ├── WritingTests.md │ └── reducers │ ├── BasicReducerStructure.md │ ├── BeyondCombineReducers.md │ ├── ImmutableUpdatePatterns.md │ ├── InitializingState.md │ ├── NormalizingStateShape.md │ ├── PrerequisiteConcepts.md │ ├── RefactoringReducersExample.md │ ├── ReusingReducerLogic.md │ ├── SplittingReducerLogic.md │ ├── UpdatingNormalizedData.md │ └── UsingCombineReducers.md ├── examples ├── README.md ├── async │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── actions │ │ └── index.js │ │ ├── components │ │ ├── Picker.js │ │ └── Posts.js │ │ ├── containers │ │ └── App.js │ │ ├── index.js │ │ └── reducers │ │ └── index.js ├── buildAll.js ├── counter-vanilla │ └── index.html ├── counter │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── components │ │ ├── Counter.js │ │ └── Counter.spec.js │ │ ├── index.js │ │ └── reducers │ │ ├── index.js │ │ └── index.spec.js ├── real-world │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── 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.js │ │ ├── middleware │ │ └── api.js │ │ ├── reducers │ │ ├── index.js │ │ └── paginate.js │ │ ├── routes.js │ │ └── store │ │ ├── configureStore.dev.js │ │ ├── configureStore.js │ │ └── configureStore.prod.js ├── shopping-cart │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── actions │ │ └── index.js │ │ ├── api │ │ ├── products.json │ │ └── shop.js │ │ ├── components │ │ ├── Cart.js │ │ ├── Cart.spec.js │ │ ├── Product.js │ │ ├── Product.spec.js │ │ ├── ProductItem.js │ │ ├── ProductItem.spec.js │ │ ├── ProductsList.js │ │ └── ProductsList.spec.js │ │ ├── constants │ │ └── ActionTypes.js │ │ ├── containers │ │ ├── App.js │ │ ├── CartContainer.js │ │ └── ProductsContainer.js │ │ ├── index.js │ │ └── reducers │ │ ├── cart.js │ │ ├── cart.spec.js │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── products.js │ │ └── products.spec.js ├── testAll.js ├── todomvc │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── actions │ │ ├── index.js │ │ └── index.spec.js │ │ ├── components │ │ ├── Footer.js │ │ ├── Footer.spec.js │ │ ├── Header.js │ │ ├── Header.spec.js │ │ ├── MainSection.js │ │ ├── MainSection.spec.js │ │ ├── TodoItem.js │ │ ├── TodoItem.spec.js │ │ ├── TodoTextInput.js │ │ └── TodoTextInput.spec.js │ │ ├── constants │ │ ├── ActionTypes.js │ │ └── TodoFilters.js │ │ ├── containers │ │ └── App.js │ │ ├── index.js │ │ └── reducers │ │ ├── index.js │ │ ├── todos.js │ │ └── todos.spec.js ├── todos-flow │ ├── .flowconfig │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── actions │ │ ├── index.js │ │ └── index.spec.js │ │ ├── components │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Link.js │ │ ├── Todo.js │ │ └── TodoList.js │ │ ├── containers │ │ ├── AddTodo.js │ │ ├── FilterLink.js │ │ └── VisibleTodoList.js │ │ ├── index.js │ │ ├── reducers │ │ ├── index.js │ │ ├── todos.js │ │ ├── todos.spec.js │ │ └── visibilityFilter.js │ │ └── types │ │ └── index.js ├── todos-with-undo │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── actions │ │ └── index.js │ │ ├── components │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Link.js │ │ ├── Todo.js │ │ └── TodoList.js │ │ ├── containers │ │ ├── AddTodo.js │ │ ├── FilterLink.js │ │ ├── UndoRedo.js │ │ └── VisibleTodoList.js │ │ ├── index.js │ │ └── reducers │ │ ├── index.js │ │ ├── todos.js │ │ └── visibilityFilter.js ├── todos │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── actions │ │ ├── index.js │ │ └── index.spec.js │ │ ├── components │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Link.js │ │ ├── Todo.js │ │ └── TodoList.js │ │ ├── containers │ │ ├── AddTodo.js │ │ ├── FilterLink.js │ │ └── VisibleTodoList.js │ │ ├── index.js │ │ └── reducers │ │ ├── index.js │ │ ├── todos.js │ │ ├── todos.spec.js │ │ └── visibilityFilter.js ├── tree-view │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── actions │ │ └── index.js │ │ ├── containers │ │ ├── Node.js │ │ └── Node.spec.js │ │ ├── generateTree.js │ │ ├── index.js │ │ └── reducers │ │ ├── index.js │ │ └── index.spec.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 ├── flow-typed ├── react-redux.js └── redux.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 | **/examples/** 3 | **/node_modules/** 4 | **/server.js 5 | **/webpack.config*.js 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | # Change Log 2 | 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | Every release, along with the migration instructions, is documented on the Github [Releases](https://github.com/reactjs/redux/releases) page. 5 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | redux.js.org 2 | -------------------------------------------------------------------------------- /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 | # Patrons 2 | 3 | The work on Redux was [funded by the community](https://www.patreon.com/reactdx). 4 | Meet some of the outstanding companies and individuals that made it possible: 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 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | docs/README.md -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": ">=3.2.1", 3 | "title": "Redux", 4 | "plugins": ["edit-link", "prism", "-highlight", "github", "anchorjs", "algolia"], 5 | "pluginsConfig": { 6 | "edit-link": { 7 | "base": "https://github.com/reactjs/redux/tree/master", 8 | "label": "Edit This Page" 9 | }, 10 | "github": { 11 | "url": "https://github.com/reactjs/redux/" 12 | }, 13 | "algolia": { 14 | "index": "redux-docs", 15 | "applicationID": "BC2QB3G2G4", 16 | "publicKey": "75eb88bb56d8cf8d17b6366a2c8e5fcd", 17 | "freeAccount": "false" 18 | }, 19 | "theme-default": { 20 | "showLevel": true, 21 | "styles": { 22 | "website": "build/gitbook.css" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /build/gitbook.css: -------------------------------------------------------------------------------- 1 | .book-summary ul.summary li span { 2 | cursor: not-allowed; 3 | opacity: 0.3; 4 | } 5 | 6 | .book-summary ul.summary li a:hover { 7 | color: #008cff; 8 | text-decoration: none; 9 | } 10 | -------------------------------------------------------------------------------- /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 | We appreciate feedback from the community. You can post feature requests and bug reports on [Product Pains](https://productpains.com/product/redux). 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/advanced/AsyncFlow.md: -------------------------------------------------------------------------------- 1 | # Async Flow 2 | 3 | Without [middleware](Middleware.md), Redux store only supports [synchronous data flow](../basics/DataFlow.md). This is what you get by default with [`createStore()`](../api/createStore.md). 4 | 5 | You may enhance [`createStore()`](../api/createStore.md) with [`applyMiddleware()`](../api/applyMiddleware.md). It is not required, but it lets you [express asynchronous actions in a convenient way](AsyncActions.md). 6 | 7 | Asynchronous middleware like [redux-thunk](https://github.com/gaearon/redux-thunk) or [redux-promise](https://github.com/acdlite/redux-promise) wraps the store's [`dispatch()`](../api/Store.md#dispatch) method and allows you to dispatch something other than actions, for example, functions or Promises. Any middleware you use can then interpret anything you dispatch, and in turn, can pass actions to the next middleware in the chain. For example, a Promise middleware can intercept Promises and dispatch a pair of begin/end actions asynchronously in response to each Promise. 8 | 9 | When the last middleware in the chain dispatches an action, it has to be a plain object. This is when the [synchronous Redux data flow](../basics/DataFlow.md) takes place. 10 | 11 | Check out [the full source code for the async example](ExampleRedditAPI.md). 12 | 13 | ## Next Steps 14 | 15 | Now you saw an example of what middleware can do in Redux, it's time to learn how it actually works, and how you can create your own. Go on to the next detailed section about [Middleware](Middleware.md). 16 | -------------------------------------------------------------------------------- /docs/advanced/NextSteps.md: -------------------------------------------------------------------------------- 1 | # Next Steps 2 | 3 | Sorry, but we're still writing this doc. 4 | Stay tuned, it will appear in a day or two. 5 | -------------------------------------------------------------------------------- /docs/advanced/README.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | 3 | In the [basics walkthrough](../basics/README.md), we explored how to structure a simple Redux application. In this walkthrough, we will explore how AJAX and routing fit into the picture. 4 | 5 | * [Async Actions](AsyncActions.md) 6 | * [Async Flow](AsyncFlow.md) 7 | * [Middleware](Middleware.md) 8 | * [Usage with React Router](UsageWithReactRouter.md) 9 | * [Example: Reddit API](ExampleRedditAPI.md) 10 | * [Next Steps](NextSteps.md) 11 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | The Redux API surface is tiny. Redux defines a set of contracts for you to implement (such as [reducers](../Glossary.md#reducer)) and provides a few helper functions to tie these contracts together. 4 | 5 | This section documents the complete Redux API. Keep in mind that Redux is only concerned with managing the state. In a real app, you'll also want to use UI bindings like [react-redux](https://github.com/gaearon/react-redux). 6 | 7 | ### Top-Level Exports 8 | 9 | * [createStore(reducer, [preloadedState], [enhancer])](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 | Every function described above is a top-level export. You can import any of them like this: 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 | Composes functions from right to left. 4 | 5 | This is a functional programming utility, and is included in Redux as a convenience. 6 | You might want to use it to apply several [store enhancers](../Glossary.md#store-enhancer) in a row. 7 | 8 | #### Arguments 9 | 10 | 1. (*arguments*): The functions to compose. Each function is expected to accept a single parameter. Its return value will be provided as an argument to the function standing to the left, and so on. The exception is the right-most argument which can accept multiple parameters, as it will provide the signature for the resulting composed function. 11 | 12 | #### Returns 13 | 14 | (*Function*): The final function obtained by composing the given functions from right to left. 15 | 16 | #### Example 17 | 18 | This example demonstrates how to use `compose` to enhance a [store](Store.md) with [`applyMiddleware`](applyMiddleware.md) and a few developer tools from the [redux-devtools](https://github.com/gaearon/redux-devtools) package. 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 | #### Tips 36 | 37 | * All `compose` does is let you write deeply nested function transformations without the rightward drift of the code. Don't give it too much credit! 38 | -------------------------------------------------------------------------------- /docs/basics/README.md: -------------------------------------------------------------------------------- 1 | # Basics 2 | 3 | Don't be fooled by all the fancy talk about reducers, middleware, store enhancers—Redux is incredibly simple. If you've ever built a Flux application, you will feel right at home. If you're new to Flux, it's easy too! 4 | 5 | In this guide, we'll walk through the process of creating a simple Todo app. 6 | 7 | * [Actions](Actions.md) 8 | * [Reducers](Reducers.md) 9 | * [Store](Store.md) 10 | * [Data Flow](DataFlow.md) 11 | * [Usage with React](UsageWithReact.md) 12 | * [Example: Todo List](ExampleTodoList.md) 13 | -------------------------------------------------------------------------------- /docs/introduction/Motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | As the requirements for JavaScript single-page applications have become increasingly complicated, **our code must manage more state than ever before**. This state can include server responses and cached data, as well as locally created data that has not yet been persisted to the server. UI state is also increasing in complexity, as we need to manage the active route, the selected tab, whether to show a spinner or not, should pagination controls be displayed, and so on. 4 | 5 | Managing this ever-changing state is hard. If a model can update another model, then a view can update a model, which updates another model, and this, in turn, might cause another view to update. At some point, you no longer understand what happens in your app as you have **lost control over the when, why, and how of its state.** When a system is opaque and non-deterministic, it's hard to reproduce bugs or add new features. 6 | 7 | As if this wasn't bad enough, consider the **new requirements becoming common in front-end product development**. As developers, we are expected to handle optimistic updates, server-side rendering, fetching data before performing route transitions, and so on. We find ourselves trying to manage a complexity that we have never had to deal with before, and we inevitably ask the question: [is it time to give up?](http://www.quirksmode.org/blog/archives/2015/07/stop_pushing_th.html) The answer is _no_. 8 | 9 | This complexity is difficult to handle as **we're mixing two concepts** that are very hard for the human mind to reason about: **mutation and asynchronicity.** I call them [Mentos and Coke](https://en.wikipedia.org/wiki/Diet_Coke_and_Mentos_eruption). Both can be great in separation, but together they create a mess. Libraries like [React](http://facebook.github.io/react) attempt to solve this problem in the view layer by removing both asynchrony and direct DOM manipulation. However, managing the state of your data is left up to you. This is where Redux enters. 10 | 11 | Following in the steps of [Flux](http://facebook.github.io/flux), [CQRS](http://martinfowler.com/bliki/CQRS.html), and [Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html), **Redux attempts to make state mutations predictable** by imposing certain restrictions on how and when updates can happen. These restrictions are reflected in the [three principles](ThreePrinciples.md) of Redux. 12 | -------------------------------------------------------------------------------- /docs/introduction/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | * [Motivation](Motivation.md) 4 | * [Three Principles](ThreePrinciples.md) 5 | * [Prior Art](PriorArt.md) 6 | * [Ecosystem](Ecosystem.md) 7 | * [Examples](Examples.md) 8 | -------------------------------------------------------------------------------- /docs/recipes/IsolatingSubapps.md: -------------------------------------------------------------------------------- 1 | # Isolating Redux Sub-Apps 2 | 3 | Consider the case of a “big” app (contained in a `` component) 4 | that embeds smaller “sub-apps” (contained in `` components): 5 | 6 | ```js 7 | import React, { Component } from 'react' 8 | import SubApp from './subapp' 9 | 10 | class BigApp extends Component { 11 | render() { 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | ) 19 | } 20 | } 21 | ``` 22 | 23 | These ``s will be completely independent. They won't share data or 24 | actions, and won't see or communicate with each other. 25 | 26 | It's best not to mix this approach with standard Redux reducer composition. 27 | For typical web apps, stick with reducer composition. For 28 | “product hubs”, “dashboards”, or enterprise software that groups disparate 29 | tools into a unified package, give the sub-app approach a try. 30 | 31 | The sub-app approach is also useful for large teams that are divided by product 32 | or feature verticals. These teams can ship sub-apps independently or in combination 33 | with an enclosing “app shell”. 34 | 35 | Below is a sub-app's root connected component. 36 | As usual, it can render more components, connected or not, as children. 37 | Usually we'd render it in `` and be done with it. 38 | 39 | ```js 40 | class App extends Component { ... } 41 | export default connect(mapStateToProps)(App) 42 | ``` 43 | 44 | However, we don't have to call `ReactDOM.render()` 45 | if we're interested in hiding the fact that the sub-app component is a Redux app. 46 | 47 | Maybe we want to be able to run multiple instances of it in the same “bigger” app 48 | and keep it as a complete black box, with Redux being an implementation detail. 49 | 50 | To hide Redux behind a React API, we can wrap it in a special component that 51 | initializes the store in the constructor: 52 | 53 | ```js 54 | import React, { Component } from 'react' 55 | import { Provider } from 'react-redux' 56 | import reducer from './reducers' 57 | import App from './App' 58 | 59 | class SubApp extends Component { 60 | constructor(props) { 61 | super(props) 62 | this.store = createStore(reducer) 63 | } 64 | 65 | render() { 66 | return ( 67 | 68 | 69 | 70 | ) 71 | } 72 | } 73 | ``` 74 | 75 | This way every instance will be independent. 76 | 77 | This pattern is *not* recommended for parts of the same app that share data. 78 | However, it can be useful when the bigger app has zero access to the smaller apps' internals, 79 | and we'd like to keep the fact that they are implemented with Redux as an implementation detail. 80 | Each component instance will have its own store, so they won't “know” about each other. 81 | 82 | -------------------------------------------------------------------------------- /docs/recipes/MigratingToRedux.md: -------------------------------------------------------------------------------- 1 | # Migrating to Redux 2 | 3 | Redux is not a monolithic framework, but a set of contracts and a [few functions that make them work together](../api/README.md). The majority of your “Redux code” will not even use Redux APIs, as most of the time you'll be writing functions. 4 | 5 | This makes it easy to migrate both to and from Redux. 6 | We don't want to lock you in! 7 | 8 | ## From Flux 9 | 10 | [Reducers](../Glossary.md#reducer) capture “the essence” of Flux Stores, so it's possible to gradually migrate an existing Flux project towards Redux, whether you are using [Flummox](http://github.com/acdlite/flummox), [Alt](http://github.com/goatslacker/alt), [traditional Flux](https://github.com/facebook/flux), or any other Flux library. 11 | 12 | It is also possible to do the reverse and migrate from Redux to any of these libraries following the same steps. 13 | 14 | Your process will look like this: 15 | 16 | * Create a function called `createFluxStore(reducer)` that creates a Flux store compatible with your existing app from a reducer function. Internally it might look similar to [`createStore`](../api/createStore.md) ([source](https://github.com/reactjs/redux/blob/master/src/createStore.js)) implementation from Redux. Its dispatch handler should just call the `reducer` for any action, store the next state, and emit change. 17 | 18 | * This allows you to gradually rewrite every Flux Store in your app as a reducer, but still export `createFluxStore(reducer)` so the rest of your app is not aware that this is happening and sees the Flux stores. 19 | 20 | * As you rewrite your Stores, you will find that you need to avoid certain Flux anti-patterns such as fetching API inside the Store, or triggering actions inside the Stores. Your Flux code will be easier to follow once you port it to be based on reducers! 21 | 22 | * When you have ported all of your Flux Stores to be implemented on top of reducers, you can replace the Flux library with a single Redux store, and combine those reducers you already have into one using [`combineReducers(reducers)`](../api/combineReducers.md). 23 | 24 | * Now all that's left to do is to port the UI to [use react-redux](../basics/UsageWithReact.md) or equivalent. 25 | 26 | * Finally, you might want to begin using some Redux idioms like middleware to further simplify your asynchronous code. 27 | 28 | ## From Backbone 29 | 30 | Backbone's model layer is quite different from Redux, so we don't suggest mixing them. If possible, it is best that you rewrite your app's model layer from scratch instead of connecting Backbone to Redux. However, if a rewrite is not feasible, you may use [backbone-redux](https://github.com/redbooth/backbone-redux) to migrate gradually, and keep the Redux store in sync with Backbone models and collections. 31 | -------------------------------------------------------------------------------- /docs/recipes/README.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | These are some use cases and code snippets to get you started with Redux in a real app. They assume you understand the topics in [basic](../basics/README.md) and [advanced](../advanced/README.md) tutorials. 4 | 5 | * [Migrating to Redux](MigratingToRedux.md) 6 | * [Using Object Spread Operator](UsingObjectSpreadOperator.md) 7 | * [Reducing Boilerplate](ReducingBoilerplate.md) 8 | * [Server Rendering](ServerRendering.md) 9 | * [Writing Tests](WritingTests.md) 10 | * [Computing Derived Data](ComputingDerivedData.md) 11 | * [Implementing Undo History](ImplementingUndoHistory.md) 12 | * [Isolating Subapps](IsolatingSubapps.md) 13 | 14 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Read the descriptions for every example on the [Examples](../docs/introduction/Examples.md) documentation page. 4 | -------------------------------------------------------------------------------- /examples/async/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/async/README.md: -------------------------------------------------------------------------------- 1 | # Redux Async Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.
15 | You will also see any lint errors in the console. 16 | 17 | ### `npm run build` 18 | 19 | Builds the app for production to the `build` folder.
20 | It correctly bundles React in production mode and optimizes the build for the best performance. 21 | 22 | The build is minified and the filenames include the hashes.
23 | Your app is ready to be deployed! 24 | 25 | ### `npm run eject` 26 | 27 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 28 | 29 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 30 | 31 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 32 | 33 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 34 | 35 | -------------------------------------------------------------------------------- /examples/async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^0.6.0", 7 | "redux-logger": "^2.6.1" 8 | }, 9 | "dependencies": { 10 | "react": "^15.3.0", 11 | "react-dom": "^15.3.0", 12 | "react-redux": "^4.4.5", 13 | "redux": "^3.5.2", 14 | "redux-thunk": "^2.1.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "./node_modules/react-scripts/config/eslint.js" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/async/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Async Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/async/src/actions/index.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_POSTS = 'REQUEST_POSTS' 2 | export const RECEIVE_POSTS = 'RECEIVE_POSTS' 3 | export const SELECT_REDDIT = 'SELECT_REDDIT' 4 | export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT' 5 | 6 | export const selectReddit = reddit => ({ 7 | type: SELECT_REDDIT, 8 | reddit 9 | }) 10 | 11 | export const invalidateReddit = reddit => ({ 12 | type: INVALIDATE_REDDIT, 13 | reddit 14 | }) 15 | 16 | export const requestPosts = reddit => ({ 17 | type: REQUEST_POSTS, 18 | reddit 19 | }) 20 | 21 | export const receivePosts = (reddit, json) => ({ 22 | type: RECEIVE_POSTS, 23 | reddit, 24 | posts: json.data.children.map(child => child.data), 25 | receivedAt: Date.now() 26 | }) 27 | 28 | const fetchPosts = reddit => dispatch => { 29 | dispatch(requestPosts(reddit)) 30 | return fetch(`https://www.reddit.com/r/${reddit}.json`) 31 | .then(response => response.json()) 32 | .then(json => dispatch(receivePosts(reddit, json))) 33 | } 34 | 35 | const shouldFetchPosts = (state, reddit) => { 36 | const posts = state.postsByReddit[reddit] 37 | if (!posts) { 38 | return true 39 | } 40 | if (posts.isFetching) { 41 | return false 42 | } 43 | return posts.didInvalidate 44 | } 45 | 46 | export const fetchPostsIfNeeded = reddit => (dispatch, getState) => { 47 | if (shouldFetchPosts(getState(), reddit)) { 48 | return dispatch(fetchPosts(reddit)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/async/src/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const Picker = ({ value, onChange, options }) => ( 4 | 5 |

{value}

6 | 14 |
15 | ) 16 | 17 | Picker.propTypes = { 18 | options: PropTypes.arrayOf( 19 | PropTypes.string.isRequired 20 | ).isRequired, 21 | value: PropTypes.string.isRequired, 22 | onChange: PropTypes.func.isRequired 23 | } 24 | 25 | export default Picker 26 | -------------------------------------------------------------------------------- /examples/async/src/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const Posts = ({posts}) => ( 4 |
    5 | {posts.map((post, i) => 6 |
  • {post.title}
  • 7 | )} 8 |
9 | ) 10 | 11 | Posts.propTypes = { 12 | posts: PropTypes.array.isRequired 13 | } 14 | 15 | export default Posts 16 | -------------------------------------------------------------------------------- /examples/async/src/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 | static propTypes = { 9 | selectedReddit: PropTypes.string.isRequired, 10 | posts: PropTypes.array.isRequired, 11 | isFetching: PropTypes.bool.isRequired, 12 | lastUpdated: PropTypes.number, 13 | dispatch: PropTypes.func.isRequired 14 | } 15 | 16 | componentDidMount() { 17 | const { dispatch, selectedReddit } = this.props 18 | dispatch(fetchPostsIfNeeded(selectedReddit)) 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | if (nextProps.selectedReddit !== this.props.selectedReddit) { 23 | const { dispatch, selectedReddit } = nextProps 24 | dispatch(fetchPostsIfNeeded(selectedReddit)) 25 | } 26 | } 27 | 28 | handleChange = nextReddit => { 29 | this.props.dispatch(selectReddit(nextReddit)) 30 | } 31 | 32 | handleRefreshClick = e => { 33 | e.preventDefault() 34 | 35 | const { dispatch, selectedReddit } = this.props 36 | dispatch(invalidateReddit(selectedReddit)) 37 | dispatch(fetchPostsIfNeeded(selectedReddit)) 38 | } 39 | 40 | render() { 41 | const { selectedReddit, posts, isFetching, lastUpdated } = this.props 42 | const isEmpty = posts.length === 0 43 | return ( 44 |
45 | 48 |

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

62 | {isEmpty 63 | ? (isFetching ?

Loading...

:

Empty.

) 64 | :
65 | 66 |
67 | } 68 |
69 | ) 70 | } 71 | } 72 | 73 | const mapStateToProps = state => { 74 | const { selectedReddit, postsByReddit } = state 75 | const { 76 | isFetching, 77 | lastUpdated, 78 | items: posts 79 | } = postsByReddit[selectedReddit] || { 80 | isFetching: true, 81 | items: [] 82 | } 83 | 84 | return { 85 | selectedReddit, 86 | posts, 87 | isFetching, 88 | lastUpdated 89 | } 90 | } 91 | 92 | export default connect(mapStateToProps)(App) 93 | -------------------------------------------------------------------------------- /examples/async/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import thunk from 'redux-thunk' 6 | import createLogger from 'redux-logger' 7 | import reducer from './reducers' 8 | import App from './containers/App' 9 | 10 | const middleware = [ thunk ] 11 | if (process.env.NODE_ENV !== 'production') { 12 | middleware.push(createLogger()) 13 | } 14 | 15 | const store = createStore( 16 | reducer, 17 | applyMiddleware(...middleware) 18 | ) 19 | 20 | render( 21 | 22 | 23 | , 24 | document.getElementById('root') 25 | ) 26 | -------------------------------------------------------------------------------- /examples/async/src/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 | const 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 | const posts = (state = { 17 | isFetching: false, 18 | didInvalidate: false, 19 | items: [] 20 | }, action) => { 21 | switch (action.type) { 22 | case INVALIDATE_REDDIT: 23 | return { 24 | ...state, 25 | didInvalidate: true 26 | } 27 | case REQUEST_POSTS: 28 | return { 29 | ...state, 30 | isFetching: true, 31 | didInvalidate: false 32 | } 33 | case RECEIVE_POSTS: 34 | return { 35 | ...state, 36 | isFetching: false, 37 | didInvalidate: false, 38 | items: action.posts, 39 | lastUpdated: action.receivedAt 40 | } 41 | default: 42 | return state 43 | } 44 | } 45 | 46 | const postsByReddit = (state = { }, action) => { 47 | switch (action.type) { 48 | case INVALIDATE_REDDIT: 49 | case RECEIVE_POSTS: 50 | case REQUEST_POSTS: 51 | return { 52 | ...state, 53 | [action.reddit]: posts(state[action.reddit], action) 54 | } 55 | default: 56 | return state 57 | } 58 | } 59 | 60 | const rootReducer = combineReducers({ 61 | postsByReddit, 62 | selectedReddit 63 | }) 64 | 65 | export default rootReducer 66 | -------------------------------------------------------------------------------- /examples/buildAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs an ordered set of commands within each of the build directories. 3 | */ 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | var { spawnSync } = require('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/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | # Redux Counter Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.
15 | You will also see any lint errors in the console. 16 | 17 | ### `npm run build` 18 | 19 | Builds the app for production to the `build` folder.
20 | It correctly bundles React in production mode and optimizes the build for the best performance. 21 | 22 | The build is minified and the filenames include the hashes.
23 | Your app is ready to be deployed! 24 | 25 | ### `npm run eject` 26 | 27 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 28 | 29 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 30 | 31 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 32 | 33 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 34 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "enzyme": "^2.4.1", 7 | "react-addons-test-utils": "^15.3.0", 8 | "react-scripts": "^0.6.0" 9 | }, 10 | "dependencies": { 11 | "react": "^15.3.0", 12 | "react-dom": "^15.3.0", 13 | "react-redux": "^4.4.5", 14 | "redux": "^3.5.2" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "eject": "react-scripts eject", 20 | "test": "react-scripts test" 21 | }, 22 | "eslintConfig": { 23 | "extends": "./node_modules/react-scripts/config/eslint.js" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Counter Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/counter/src/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | class Counter extends Component { 4 | static propTypes = { 5 | value: PropTypes.number.isRequired, 6 | onIncrement: PropTypes.func.isRequired, 7 | onDecrement: PropTypes.func.isRequired 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 | export default Counter 47 | -------------------------------------------------------------------------------- /examples/counter/src/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import Counter from './Counter' 4 | 5 | function setup(value = 0) { 6 | const actions = { 7 | onIncrement: jest.fn(), 8 | onDecrement: jest.fn() 9 | } 10 | const component = shallow( 11 | 12 | ) 13 | 14 | return { 15 | component: component, 16 | actions: actions, 17 | buttons: component.find('button'), 18 | p: component.find('p') 19 | } 20 | } 21 | 22 | describe('Counter component', () => { 23 | it('should display count', () => { 24 | const { p } = setup() 25 | expect(p.text()).toMatch(/^Clicked: 0 times/) 26 | }) 27 | 28 | it('first button should call onIncrement', () => { 29 | const { buttons, actions } = setup() 30 | buttons.at(0).simulate('click') 31 | expect(actions.onIncrement).toBeCalled() 32 | }) 33 | 34 | it('second button should call onDecrement', () => { 35 | const { buttons, actions } = setup() 36 | buttons.at(1).simulate('click') 37 | expect(actions.onDecrement).toBeCalled() 38 | }) 39 | 40 | it('third button should not call onIncrement if the counter is even', () => { 41 | const { buttons, actions } = setup(42) 42 | buttons.at(2).simulate('click') 43 | expect(actions.onIncrement).not.toBeCalled() 44 | }) 45 | 46 | it('third button should call onIncrement if the counter is odd', () => { 47 | const { buttons, actions } = setup(43) 48 | buttons.at(2).simulate('click') 49 | expect(actions.onIncrement).toBeCalled() 50 | }) 51 | 52 | it('third button should call onIncrement if the counter is odd and negative', () => { 53 | const { buttons, actions } = setup(-43) 54 | buttons.at(2).simulate('click') 55 | expect(actions.onIncrement).toBeCalled() 56 | }) 57 | 58 | it('fourth button should call onIncrement in a second', (done) => { 59 | const { buttons, actions } = setup() 60 | buttons.at(3).simulate('click') 61 | setTimeout(() => { 62 | expect(actions.onIncrement).toBeCalled() 63 | done() 64 | }, 1000) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /examples/counter/src/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 | const render = () => ReactDOM.render( 11 | store.dispatch({ type: 'INCREMENT' })} 14 | onDecrement={() => store.dispatch({ type: 'DECREMENT' })} 15 | />, 16 | rootEl 17 | ) 18 | 19 | render() 20 | store.subscribe(render) 21 | -------------------------------------------------------------------------------- /examples/counter/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | export default (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/src/reducers/index.spec.js: -------------------------------------------------------------------------------- 1 | import counter from './index' 2 | 3 | describe('reducers', () => { 4 | describe('counter', () => { 5 | it('should provide the initial state', () => { 6 | expect(counter(undefined, {})).toBe(0) 7 | }) 8 | 9 | it('should handle INCREMENT action', () => { 10 | expect(counter(1, { type: 'INCREMENT' })).toBe(2) 11 | }) 12 | 13 | it('should handle DECREMENT action', () => { 14 | expect(counter(1, { type: 'DECREMENT' })).toBe(0) 15 | }) 16 | 17 | it('should ignore unknown actions', () => { 18 | expect(counter(1, { type: 'unknown' })).toBe(1) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /examples/real-world/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/real-world/README.md: -------------------------------------------------------------------------------- 1 | # Redux Real World Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.
15 | You will also see any lint errors in the console. 16 | 17 | ### `npm run build` 18 | 19 | Builds the app for production to the `build` folder.
20 | It correctly bundles React in production mode and optimizes the build for the best performance. 21 | 22 | The build is minified and the filenames include the hashes.
23 | Your app is ready to be deployed! 24 | 25 | ### `npm run eject` 26 | 27 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 28 | 29 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 30 | 31 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 32 | 33 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 34 | -------------------------------------------------------------------------------- /examples/real-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "real-world", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^0.6.0", 7 | "redux-devtools": "^3.3.1", 8 | "redux-devtools-dock-monitor": "^1.1.1", 9 | "redux-devtools-log-monitor": "^1.0.11", 10 | "redux-logger": "^2.6.1" 11 | }, 12 | "dependencies": { 13 | "humps": "^1.1.0", 14 | "lodash": "^4.16.1", 15 | "normalizr": "^2.2.1", 16 | "react": "^15.3.0", 17 | "react-dom": "^15.3.0", 18 | "react-redux": "^4.4.5", 19 | "react-router": "^2.6.1", 20 | "react-router-redux": "^4.0.5", 21 | "redux": "^3.5.2", 22 | "redux-thunk": "^2.1.0" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "./node_modules/react-scripts/config/eslint.js" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/real-world/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React App 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/real-world/src/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 | static propTypes = { 7 | value: PropTypes.string.isRequired, 8 | onChange: PropTypes.func.isRequired 9 | } 10 | 11 | componentWillReceiveProps(nextProps) { 12 | if (nextProps.value !== this.props.value) { 13 | this.setInputValue(nextProps.value) 14 | } 15 | } 16 | 17 | getInputValue = () => { 18 | return this.refs.input.value 19 | } 20 | 21 | setInputValue = (val) => { 22 | // Generally mutating DOM is a bad idea in React components, 23 | // but doing this for a single uncontrolled field is less fuss 24 | // than making it controlled and maintaining a state for it. 25 | this.refs.input.value = val 26 | } 27 | 28 | handleKeyUp = (e) => { 29 | if (e.keyCode === 13) { 30 | this.handleGoClick() 31 | } 32 | } 33 | 34 | handleGoClick = () => { 35 | this.props.onChange(this.getInputValue()) 36 | } 37 | 38 | render() { 39 | return ( 40 |
41 |

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

42 | 46 | 49 |

50 | Code on Github. 51 |

52 |

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

55 |
56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/real-world/src/components/List.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class List extends Component { 4 | static propTypes = { 5 | loadingLabel: PropTypes.string.isRequired, 6 | pageCount: PropTypes.number, 7 | renderItem: PropTypes.func.isRequired, 8 | items: PropTypes.array.isRequired, 9 | isFetching: PropTypes.bool.isRequired, 10 | onLoadMoreClick: PropTypes.func.isRequired, 11 | nextPageUrl: PropTypes.string 12 | } 13 | 14 | static defaultProps = { 15 | isFetching: true, 16 | loadingLabel: 'Loading...' 17 | } 18 | 19 | renderLoadMore() { 20 | const { isFetching, onLoadMoreClick } = this.props 21 | return ( 22 | 27 | ) 28 | } 29 | 30 | render() { 31 | const { 32 | isFetching, nextPageUrl, pageCount, 33 | items, renderItem, loadingLabel 34 | } = this.props 35 | 36 | const isEmpty = items.length === 0 37 | if (isEmpty && isFetching) { 38 | return

{loadingLabel}

39 | } 40 | 41 | const isLastPage = !nextPageUrl 42 | if (isEmpty && isLastPage) { 43 | return

Nothing here!

44 | } 45 | 46 | return ( 47 |
48 | {items.map(renderItem)} 49 | {pageCount > 0 && !isLastPage && this.renderLoadMore()} 50 |
51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/real-world/src/components/Repo.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | const Repo = ({ repo, owner }) => { 5 | const { login } = owner 6 | const { name, description } = repo 7 | 8 | return ( 9 |
10 |

11 | 12 | {name} 13 | 14 | {' by '} 15 | 16 | {login} 17 | 18 |

19 | {description && 20 |

{description}

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

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

14 | 15 |
16 | ) 17 | } 18 | 19 | User.propTypes = { 20 | user: PropTypes.shape({ 21 | login: PropTypes.string.isRequired, 22 | avatarUrl: PropTypes.string.isRequired, 23 | name: PropTypes.string 24 | }).isRequired 25 | } 26 | 27 | export default User 28 | -------------------------------------------------------------------------------- /examples/real-world/src/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 | static propTypes = { 9 | // Injected by React Redux 10 | errorMessage: PropTypes.string, 11 | resetErrorMessage: PropTypes.func.isRequired, 12 | inputValue: PropTypes.string.isRequired, 13 | // Injected by React Router 14 | children: PropTypes.node 15 | } 16 | 17 | handleDismissClick = e => { 18 | this.props.resetErrorMessage() 19 | e.preventDefault() 20 | } 21 | 22 | handleChange = nextValue => { 23 | browserHistory.push(`/${nextValue}`) 24 | } 25 | 26 | renderErrorMessage() { 27 | const { errorMessage } = this.props 28 | if (!errorMessage) { 29 | return null 30 | } 31 | 32 | return ( 33 |

34 | {errorMessage} 35 | {' '} 36 | ( 38 | Dismiss 39 | ) 40 |

41 | ) 42 | } 43 | 44 | render() { 45 | const { children, inputValue } = this.props 46 | return ( 47 |
48 | 50 |
51 | {this.renderErrorMessage()} 52 | {children} 53 |
54 | ) 55 | } 56 | } 57 | 58 | const mapStateToProps = (state, ownProps) => ({ 59 | errorMessage: state.errorMessage, 60 | inputValue: ownProps.location.pathname.substring(1) 61 | }) 62 | 63 | export default connect(mapStateToProps, { 64 | resetErrorMessage 65 | })(App) 66 | -------------------------------------------------------------------------------- /examples/real-world/src/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/src/containers/Root.dev.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Provider } from 'react-redux' 3 | import routes from '../routes' 4 | import DevTools from './DevTools' 5 | import { Router } from 'react-router' 6 | 7 | const Root = ({ store, history }) => ( 8 | 9 |
10 | 11 | 12 |
13 |
14 | ) 15 | 16 | Root.propTypes = { 17 | store: PropTypes.object.isRequired, 18 | history: PropTypes.object.isRequired 19 | } 20 | 21 | export default Root 22 | -------------------------------------------------------------------------------- /examples/real-world/src/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/src/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 | const Root = ({ store, history }) => ( 7 | 8 | 9 | 10 | ) 11 | 12 | Root.propTypes = { 13 | store: PropTypes.object.isRequired, 14 | history: PropTypes.object.isRequired 15 | } 16 | -------------------------------------------------------------------------------- /examples/real-world/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { browserHistory } from 'react-router' 4 | import { syncHistoryWithStore } from 'react-router-redux' 5 | import Root from './containers/Root' 6 | import configureStore from './store/configureStore' 7 | 8 | const store = configureStore() 9 | const history = syncHistoryWithStore(browserHistory, store) 10 | 11 | render( 12 | , 13 | document.getElementById('root') 14 | ) 15 | -------------------------------------------------------------------------------- /examples/real-world/src/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 | const 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 | const 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/src/reducers/paginate.js: -------------------------------------------------------------------------------- 1 | import union from 'lodash/union' 2 | 3 | // Creates a reducer managing pagination, given the action types to handle, 4 | // and a function telling how to extract the key from an action. 5 | const paginate = ({ types, mapActionToKey }) => { 6 | if (!Array.isArray(types) || types.length !== 3) { 7 | throw new Error('Expected types to be an array of three elements.') 8 | } 9 | if (!types.every(t => typeof t === 'string')) { 10 | throw new Error('Expected types to be strings.') 11 | } 12 | if (typeof mapActionToKey !== 'function') { 13 | throw new Error('Expected mapActionToKey to be a function.') 14 | } 15 | 16 | const [ requestType, successType, failureType ] = types 17 | 18 | const updatePagination = (state = { 19 | isFetching: false, 20 | nextPageUrl: undefined, 21 | pageCount: 0, 22 | ids: [] 23 | }, action) => { 24 | switch (action.type) { 25 | case requestType: 26 | return { 27 | ...state, 28 | isFetching: true 29 | } 30 | case successType: 31 | return { 32 | ...state, 33 | isFetching: false, 34 | ids: union(state.ids, action.response.result), 35 | nextPageUrl: action.response.nextPageUrl, 36 | pageCount: state.pageCount + 1 37 | } 38 | case failureType: 39 | return { 40 | ...state, 41 | isFetching: false 42 | } 43 | default: 44 | return state 45 | } 46 | } 47 | 48 | return (state = {}, action) => { 49 | // Update pagination by key 50 | switch (action.type) { 51 | case requestType: 52 | case successType: 53 | case failureType: 54 | const key = mapActionToKey(action) 55 | if (typeof key !== 'string') { 56 | throw new Error('Expected key to be a string.') 57 | } 58 | return { ...state, 59 | [key]: updatePagination(state[key], action) 60 | } 61 | default: 62 | return state 63 | } 64 | } 65 | } 66 | 67 | export default paginate 68 | -------------------------------------------------------------------------------- /examples/real-world/src/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 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /examples/real-world/src/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 | const 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 | 29 | export default configureStore 30 | -------------------------------------------------------------------------------- /examples/real-world/src/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/src/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 | const configureStore = preloadedState => createStore( 7 | rootReducer, 8 | preloadedState, 9 | applyMiddleware(thunk, api) 10 | ) 11 | 12 | export default configureStore 13 | -------------------------------------------------------------------------------- /examples/shopping-cart/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/shopping-cart/README.md: -------------------------------------------------------------------------------- 1 | # Redux Shopping Cart Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.
15 | You will also see any lint errors in the console. 16 | 17 | ### `npm run build` 18 | 19 | Builds the app for production to the `build` folder.
20 | It correctly bundles React in production mode and optimizes the build for the best performance. 21 | 22 | The build is minified and the filenames include the hashes.
23 | Your app is ready to be deployed! 24 | 25 | ### `npm run eject` 26 | 27 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 28 | 29 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 30 | 31 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 32 | 33 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 34 | -------------------------------------------------------------------------------- /examples/shopping-cart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopping-cart", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "enzyme": "^2.4.1", 7 | "react-addons-test-utils": "^15.3.0", 8 | "react-scripts": "^0.6.0" 9 | }, 10 | "dependencies": { 11 | "react": "^15.3.0", 12 | "react-dom": "^15.3.0", 13 | "react-redux": "^4.4.5", 14 | "redux": "^3.5.2", 15 | "redux-logger": "^2.6.1", 16 | "redux-thunk": "^2.1.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "eject": "react-scripts eject", 22 | "test": "react-scripts test" 23 | }, 24 | "eslintConfig": { 25 | "extends": "./node_modules/react-scripts/config/eslint.js" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/shopping-cart/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Shopping Cart Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import shop from '../api/shop' 2 | import * as types from '../constants/ActionTypes' 3 | 4 | const receiveProducts = products => ({ 5 | type: types.RECEIVE_PRODUCTS, 6 | products: products 7 | }) 8 | 9 | export const getAllProducts = () => dispatch => { 10 | shop.getProducts(products => { 11 | dispatch(receiveProducts(products)) 12 | }) 13 | } 14 | 15 | const addToCartUnsafe = productId => ({ 16 | type: types.ADD_TO_CART, 17 | productId 18 | }) 19 | 20 | export const addToCart = productId => (dispatch, getState) => { 21 | if (getState().products.byId[productId].inventory > 0) { 22 | dispatch(addToCartUnsafe(productId)) 23 | } 24 | } 25 | 26 | export const checkout = products => (dispatch, getState) => { 27 | const { cart } = getState() 28 | 29 | dispatch({ 30 | type: types.CHECKOUT_REQUEST 31 | }) 32 | shop.buyProducts(products, () => { 33 | dispatch({ 34 | type: types.CHECKOUT_SUCCESS, 35 | cart 36 | }) 37 | // Replace the line above with line below to rollback on failure: 38 | // dispatch({ type: types.CHECKOUT_FAILURE, cart }) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/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/src/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) => setTimeout(() => cb(_products), timeout || TIMEOUT), 10 | buyProducts: (payload, cb, timeout) => setTimeout(() => cb(), timeout || TIMEOUT) 11 | } 12 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/components/Cart.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import Product from './Product' 3 | 4 | const Cart = ({ products, total, onCheckoutClicked }) => { 5 | const hasProducts = products.length > 0 6 | const nodes = hasProducts ? ( 7 | products.map(product => 8 | 14 | ) 15 | ) : ( 16 | Please add some products to cart. 17 | ) 18 | 19 | return ( 20 |
21 |

Your Cart

22 |
{nodes}
23 |

Total: ${total}

24 | 28 |
29 | ) 30 | } 31 | 32 | Cart.propTypes = { 33 | products: PropTypes.array, 34 | total: PropTypes.string, 35 | onCheckoutClicked: PropTypes.func 36 | } 37 | 38 | export default Cart 39 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/components/Cart.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import Cart from './Cart' 4 | import Product from './Product' 5 | 6 | const setup = (total, products = []) => { 7 | const actions = { 8 | onCheckoutClicked: jest.fn() 9 | } 10 | 11 | const component = shallow( 12 | 13 | ) 14 | 15 | return { 16 | component: component, 17 | actions: actions, 18 | button: component.find('button'), 19 | products: component.find(Product), 20 | em: component.find('em'), 21 | p: component.find('p') 22 | } 23 | } 24 | 25 | describe('Cart component', () => { 26 | it('should display total', () => { 27 | const { p } = setup('76') 28 | expect(p.text()).toMatch(/^Total: \$76/) 29 | }) 30 | 31 | it('should display add some products message', () => { 32 | const { em } = setup() 33 | expect(em.text()).toMatch(/^Please add some products to cart/) 34 | }) 35 | 36 | it('should disable button', () => { 37 | const { button } = setup() 38 | expect(button.prop('disabled')).toEqual('disabled') 39 | }) 40 | 41 | describe('when given product', () => { 42 | const product = [ 43 | { 44 | id: 1, 45 | title: 'Product 1', 46 | price: 9.99, 47 | quantity: 1 48 | } 49 | ] 50 | 51 | it('should render products', () => { 52 | const { products } = setup('9.99', product) 53 | const props = { 54 | title: product[0].title, 55 | price: product[0].price, 56 | quantity: product[0].quantity 57 | } 58 | 59 | expect(products.at(0).props()).toEqual(props) 60 | }) 61 | 62 | it('should not disable button', () => { 63 | const { button } = setup('9.99', product) 64 | expect(button.prop('disabled')).toEqual('') 65 | }) 66 | 67 | it('should call action on button click', () => { 68 | const { button, actions } = setup('9.99', product) 69 | button.simulate('click') 70 | expect(actions.onCheckoutClicked).toBeCalled() 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/components/Product.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const Product = ({ price, quantity, title }) => ( 4 |
5 | {title} - ${price}{quantity ? ` x ${quantity}` : null} 6 |
7 | ) 8 | 9 | Product.propTypes = { 10 | price: PropTypes.number, 11 | quantity: PropTypes.number, 12 | title: PropTypes.string 13 | } 14 | 15 | export default Product 16 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/components/Product.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import Product from './Product' 4 | 5 | const setup = props => { 6 | const component = shallow( 7 | 8 | ) 9 | 10 | return { 11 | component: component 12 | } 13 | } 14 | 15 | describe('Product component', () => { 16 | it('should render title and price', () => { 17 | const { component } = setup({ title: 'Test Product', price: 9.99 }) 18 | expect(component.text()).toBe('Test Product - $9.99') 19 | }) 20 | 21 | describe('when given quantity', () => { 22 | it('should render title, price, and quantity', () => { 23 | const { component } = setup({ title: 'Test Product', price: 9.99, quantity: 6 }) 24 | expect(component.text()).toBe('Test Product - $9.99 x 6') 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/components/ProductItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import Product from './Product' 3 | 4 | const ProductItem = ({ product, onAddToCartClicked }) => ( 5 |
6 | 9 | 14 |
15 | ) 16 | 17 | ProductItem.propTypes = { 18 | product: PropTypes.shape({ 19 | title: PropTypes.string.isRequired, 20 | price: PropTypes.number.isRequired, 21 | inventory: PropTypes.number.isRequired 22 | }).isRequired, 23 | onAddToCartClicked: PropTypes.func.isRequired 24 | } 25 | 26 | export default ProductItem 27 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/components/ProductItem.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import Product from './Product' 4 | import ProductItem from './ProductItem' 5 | 6 | const setup = product => { 7 | const actions = { 8 | onAddToCartClicked: jest.fn() 9 | } 10 | 11 | const component = shallow( 12 | 13 | ) 14 | 15 | return { 16 | component: component, 17 | actions: actions, 18 | button: component.find('button'), 19 | product: component.find(Product) 20 | } 21 | } 22 | 23 | let productProps 24 | 25 | describe('ProductItem component', () => { 26 | beforeEach(() => { 27 | productProps = { 28 | title: 'Product 1', 29 | price: 9.99, 30 | inventory: 6 31 | } 32 | }) 33 | 34 | it('should render product', () => { 35 | const { product } = setup(productProps) 36 | expect(product.props()).toEqual({ title: 'Product 1', price: 9.99 }) 37 | }) 38 | 39 | it('should render Add To Cart message', () => { 40 | const { button } = setup(productProps) 41 | expect(button.text()).toMatch(/^Add to cart/) 42 | }) 43 | 44 | it('should not disable button', () => { 45 | const { button } = setup(productProps) 46 | expect(button.prop('disabled')).toEqual('') 47 | }) 48 | 49 | it('should call action on button click', () => { 50 | const { button, actions } = setup(productProps) 51 | button.simulate('click') 52 | expect(actions.onAddToCartClicked).toBeCalled() 53 | }) 54 | 55 | describe('when product inventory is 0', () => { 56 | beforeEach(() => { 57 | productProps.inventory = 0 58 | }) 59 | 60 | it('should render Sold Out message', () => { 61 | const { button } = setup(productProps) 62 | expect(button.text()).toMatch(/^Sold Out/) 63 | }) 64 | 65 | it('should disable button', () => { 66 | const { button } = setup(productProps) 67 | expect(button.prop('disabled')).toEqual('disabled') 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/components/ProductsList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const ProductsList = ({ title, children }) => ( 4 |
5 |

{title}

6 |
{children}
7 |
8 | ) 9 | 10 | ProductsList.propTypes = { 11 | children: PropTypes.node, 12 | title: PropTypes.string.isRequired 13 | } 14 | 15 | export default ProductsList 16 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/components/ProductsList.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import ProductsList from './ProductsList' 4 | 5 | const setup = props => { 6 | const component = shallow( 7 | {props.children} 8 | ) 9 | 10 | return { 11 | component: component, 12 | children: component.children().at(1), 13 | h3: component.find('h3') 14 | } 15 | } 16 | 17 | describe('ProductsList component', () => { 18 | it('should render title', () => { 19 | const { h3 } = setup({ title: 'Test Products' }) 20 | expect(h3.text()).toMatch(/^Test Products$/) 21 | }) 22 | 23 | it('should render children', () => { 24 | const { children } = setup({ title: 'Test Products', children: 'Test Children' }) 25 | expect(children.text()).toMatch(/^Test Children$/) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/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/src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ProductsContainer from './ProductsContainer' 3 | import CartContainer from './CartContainer' 4 | 5 | const App = () => ( 6 |
7 |

Shopping Cart Example

8 |
9 | 10 |
11 | 12 |
13 | ) 14 | 15 | export default App 16 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/containers/CartContainer.js: -------------------------------------------------------------------------------- 1 | import React, { 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 | const CartContainer = ({ products, total, checkout }) => ( 8 | checkout(products)} /> 12 | ) 13 | 14 | CartContainer.propTypes = { 15 | products: PropTypes.arrayOf(PropTypes.shape({ 16 | id: PropTypes.number.isRequired, 17 | title: PropTypes.string.isRequired, 18 | price: PropTypes.number.isRequired, 19 | quantity: PropTypes.number.isRequired 20 | })).isRequired, 21 | total: PropTypes.string, 22 | checkout: PropTypes.func.isRequired 23 | } 24 | 25 | const mapStateToProps = (state) => ({ 26 | products: getCartProducts(state), 27 | total: getTotal(state) 28 | }) 29 | 30 | export default connect( 31 | mapStateToProps, 32 | { checkout } 33 | )(CartContainer) 34 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/containers/ProductsContainer.js: -------------------------------------------------------------------------------- 1 | import React, { 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 | const ProductsContainer = ({ products, addToCart }) => ( 9 | 10 | {products.map(product => 11 | addToCart(product.id)} /> 15 | )} 16 | 17 | ) 18 | 19 | ProductsContainer.propTypes = { 20 | products: PropTypes.arrayOf(PropTypes.shape({ 21 | id: PropTypes.number.isRequired, 22 | title: PropTypes.string.isRequired, 23 | price: PropTypes.number.isRequired, 24 | inventory: PropTypes.number.isRequired 25 | })).isRequired, 26 | addToCart: PropTypes.func.isRequired 27 | } 28 | 29 | const mapStateToProps = state => ({ 30 | products: getVisibleProducts(state.products) 31 | }) 32 | 33 | export default connect( 34 | mapStateToProps, 35 | { addToCart } 36 | )(ProductsContainer) 37 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import createLogger from 'redux-logger' 6 | import thunk from 'redux-thunk' 7 | import reducer from './reducers' 8 | import { getAllProducts } from './actions' 9 | import App from './containers/App' 10 | 11 | const middleware = [ thunk ]; 12 | if (process.env.NODE_ENV !== 'production') { 13 | middleware.push(createLogger()); 14 | } 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/src/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 | const 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 | const quantityById = (state = initialState.quantityById, action) => { 25 | switch (action.type) { 26 | case ADD_TO_CART: 27 | const { productId } = action 28 | return { ...state, 29 | [productId]: (state[productId] || 0) + 1 30 | } 31 | default: 32 | return state 33 | } 34 | } 35 | 36 | export const getQuantity = (state, productId) => 37 | state.quantityById[productId] || 0 38 | 39 | export const getAddedIds = state => state.addedIds 40 | 41 | const cart = (state = initialState, action) => { 42 | switch (action.type) { 43 | case CHECKOUT_REQUEST: 44 | return initialState 45 | case CHECKOUT_FAILURE: 46 | return action.cart 47 | default: 48 | return { 49 | addedIds: addedIds(state.addedIds, action), 50 | quantityById: quantityById(state.quantityById, action) 51 | } 52 | } 53 | } 54 | 55 | export default cart 56 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/reducers/cart.spec.js: -------------------------------------------------------------------------------- 1 | import cart from './cart' 2 | 3 | describe('reducers', () => { 4 | describe('cart', () => { 5 | const initialState = { 6 | addedIds: [], 7 | quantityById: {} 8 | } 9 | 10 | it('should provide the initial state', () => { 11 | expect(cart(undefined, {})).toEqual(initialState) 12 | }) 13 | 14 | it('should handle CHECKOUT_REQUEST action', () => { 15 | expect(cart({}, { type: 'CHECKOUT_REQUEST' })).toEqual(initialState) 16 | }) 17 | 18 | it('should handle CHECKOUT_FAILURE action', () => { 19 | expect(cart({}, { type: 'CHECKOUT_FAILURE', cart: 'cart state' })).toEqual('cart state') 20 | }) 21 | 22 | it('should handle ADD_TO_CART action', () => { 23 | expect(cart(initialState, { type: 'ADD_TO_CART', productId: 1 })).toEqual({ 24 | addedIds: [ 1 ], 25 | quantityById: { 1: 1 } 26 | }) 27 | }) 28 | 29 | describe('when product is already in cart', () => { 30 | it('should handle ADD_TO_CART action', () => { 31 | const state = { 32 | addedIds: [ 1, 2 ], 33 | quantityById: { 1: 1, 2: 1 } 34 | } 35 | 36 | expect(cart(state, { type: 'ADD_TO_CART', productId: 2 })).toEqual({ 37 | addedIds: [ 1, 2 ], 38 | quantityById: { 1: 1, 2: 2 } 39 | }) 40 | }) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/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 | const getAddedIds = state => fromCart.getAddedIds(state.cart) 11 | const getQuantity = (state, id) => fromCart.getQuantity(state.cart, id) 12 | const getProduct = (state, id) => fromProducts.getProduct(state.products, id) 13 | 14 | export const getTotal = state => 15 | getAddedIds(state) 16 | .reduce((total, id) => 17 | total + getProduct(state, id).price * getQuantity(state, id), 18 | 0 19 | ) 20 | .toFixed(2) 21 | 22 | export const getCartProducts = state => 23 | getAddedIds(state).map(id => ({ 24 | ...getProduct(state, id), 25 | quantity: getQuantity(state, id) 26 | })) 27 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/reducers/index.spec.js: -------------------------------------------------------------------------------- 1 | import { getTotal, getCartProducts } from './index' 2 | 3 | describe('selectors', () => { 4 | describe('getTotal', () => { 5 | it('should return price total', () => { 6 | const state = { 7 | cart: { 8 | addedIds: [ 1, 2, 3 ], 9 | quantityById: { 10 | 1: 4, 11 | 2: 2, 12 | 3: 1 13 | } 14 | }, 15 | products: { 16 | byId: { 17 | 1: { 18 | id: 1, 19 | price: 1.99 20 | }, 21 | 2: { 22 | id: 1, 23 | price: 4.99 24 | }, 25 | 3: { 26 | id: 1, 27 | price: 9.99 28 | } 29 | } 30 | } 31 | } 32 | expect(getTotal(state)).toBe('27.93') 33 | }) 34 | }) 35 | 36 | describe('getCartProducts', () => { 37 | it('should return products with quantity', () => { 38 | const state = { 39 | cart: { 40 | addedIds: [ 1, 2, 3 ], 41 | quantityById: { 42 | 1: 4, 43 | 2: 2, 44 | 3: 1 45 | } 46 | }, 47 | products: { 48 | byId: { 49 | 1: { 50 | id: 1, 51 | price: 1.99 52 | }, 53 | 2: { 54 | id: 1, 55 | price: 4.99 56 | }, 57 | 3: { 58 | id: 1, 59 | price: 9.99 60 | } 61 | } 62 | } 63 | } 64 | 65 | expect(getCartProducts(state)).toEqual([ 66 | { 67 | id: 1, 68 | price: 1.99, 69 | quantity: 4 70 | }, 71 | { 72 | id: 1, 73 | price: 4.99, 74 | quantity: 2 75 | }, 76 | { 77 | id: 1, 78 | price: 9.99, 79 | quantity: 1 80 | } 81 | ]) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/reducers/products.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { RECEIVE_PRODUCTS, ADD_TO_CART } from '../constants/ActionTypes' 3 | 4 | const products = (state, action) => { 5 | switch (action.type) { 6 | case ADD_TO_CART: 7 | return { 8 | ...state, 9 | inventory: state.inventory - 1 10 | } 11 | default: 12 | return state 13 | } 14 | } 15 | 16 | const byId = (state = {}, action) => { 17 | switch (action.type) { 18 | case RECEIVE_PRODUCTS: 19 | return { 20 | ...state, 21 | ...action.products.reduce((obj, product) => { 22 | obj[product.id] = product 23 | return obj 24 | }, {}) 25 | } 26 | default: 27 | const { productId } = action 28 | if (productId) { 29 | return { 30 | ...state, 31 | [productId]: products(state[productId], action) 32 | } 33 | } 34 | return state 35 | } 36 | } 37 | 38 | const visibleIds = (state = [], action) => { 39 | switch (action.type) { 40 | case RECEIVE_PRODUCTS: 41 | return action.products.map(product => product.id) 42 | default: 43 | return state 44 | } 45 | } 46 | 47 | export default combineReducers({ 48 | byId, 49 | visibleIds 50 | }) 51 | 52 | export const getProduct = (state, id) => 53 | state.byId[id] 54 | 55 | export const getVisibleProducts = state => 56 | state.visibleIds.map(id => getProduct(state, id)) 57 | -------------------------------------------------------------------------------- /examples/shopping-cart/src/reducers/products.spec.js: -------------------------------------------------------------------------------- 1 | import products from './products' 2 | 3 | describe('reducers', () => { 4 | describe('products', () => { 5 | it('should handle RECEIVE_PRODUCTS action', () => { 6 | const action = { 7 | type: 'RECEIVE_PRODUCTS', 8 | products: [ 9 | { 10 | id: 1, 11 | title: 'Product 1' 12 | }, 13 | { 14 | id: 2, 15 | title: 'Product 2' 16 | } 17 | ] 18 | } 19 | 20 | expect(products({}, action)).toEqual({ 21 | byId: { 22 | 1: { 23 | id: 1, 24 | title: 'Product 1' 25 | }, 26 | 2: { 27 | id: 2, 28 | title: 'Product 2' 29 | } 30 | }, 31 | visibleIds: [ 1, 2 ] 32 | }) 33 | }) 34 | 35 | it('should handle ADD_TO_CART action', () => { 36 | const state = { 37 | byId: { 38 | 1: { 39 | id: 1, 40 | title: 'Product 1', 41 | inventory: 1 42 | } 43 | } 44 | } 45 | 46 | expect(products(state, { type: 'ADD_TO_CART', productId: 1 })).toEqual({ 47 | byId: { 48 | 1: { 49 | id: 1, 50 | title: 'Product 1', 51 | inventory: 0 52 | } 53 | }, 54 | visibleIds: [] 55 | }) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /examples/testAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs an ordered set of commands within each of the build directories. 3 | */ 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | var { spawnSync } = require('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/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Redux TodoMVC Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.
15 | You will also see any lint errors in the console. 16 | 17 | ### `npm run build` 18 | 19 | Builds the app for production to the `build` folder.
20 | It correctly bundles React in production mode and optimizes the build for the best performance. 21 | 22 | The build is minified and the filenames include the hashes.
23 | Your app is ready to be deployed! 24 | 25 | ### `npm run eject` 26 | 27 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 28 | 29 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 30 | 31 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 32 | 33 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 34 | -------------------------------------------------------------------------------- /examples/todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "enzyme": "^2.4.1", 7 | "react-addons-test-utils": "^15.3.0", 8 | "react-scripts": "^0.6.0" 9 | }, 10 | "dependencies": { 11 | "classnames": "^2.2.5", 12 | "react": "^15.3.0", 13 | "react-dom": "^15.3.0", 14 | "react-redux": "^4.4.5", 15 | "redux": "^3.5.2", 16 | "todomvc-app-css": "^2.0.6" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "eject": "react-scripts eject", 22 | "test": "react-scripts test" 23 | }, 24 | "eslintConfig": { 25 | "extends": "./node_modules/react-scripts/config/eslint.js" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/todomvc/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux TodoMVC Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/todomvc/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | 3 | export const addTodo = text => ({ type: types.ADD_TODO, text }) 4 | export const deleteTodo = id => ({ type: types.DELETE_TODO, id }) 5 | export const editTodo = (id, text) => ({ type: types.EDIT_TODO, id, text }) 6 | export const completeTodo = id => ({ type: types.COMPLETE_TODO, id }) 7 | export const completeAll = () => ({ type: types.COMPLETE_ALL }) 8 | export const clearCompleted = () => ({ type: types.CLEAR_COMPLETED }) 9 | -------------------------------------------------------------------------------- /examples/todomvc/src/actions/index.spec.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | import * as actions from './index' 3 | 4 | describe('todo actions', () => { 5 | it('addTodo should create ADD_TODO action', () => { 6 | expect(actions.addTodo('Use Redux')).toEqual({ 7 | type: types.ADD_TODO, 8 | text: 'Use Redux' 9 | }) 10 | }) 11 | 12 | it('deleteTodo should create DELETE_TODO action', () => { 13 | expect(actions.deleteTodo(1)).toEqual({ 14 | type: types.DELETE_TODO, 15 | id: 1 16 | }) 17 | }) 18 | 19 | it('editTodo should create EDIT_TODO action', () => { 20 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({ 21 | type: types.EDIT_TODO, 22 | id: 1, 23 | text: 'Use Redux everywhere' 24 | }) 25 | }) 26 | 27 | it('completeTodo should create COMPLETE_TODO action', () => { 28 | expect(actions.completeTodo(1)).toEqual({ 29 | type: types.COMPLETE_TODO, 30 | id: 1 31 | }) 32 | }) 33 | 34 | it('completeAll should create COMPLETE_ALL action', () => { 35 | expect(actions.completeAll()).toEqual({ 36 | type: types.COMPLETE_ALL 37 | }) 38 | }) 39 | 40 | it('clearCompleted should create CLEAR_COMPLETED action', () => { 41 | expect(actions.clearCompleted()).toEqual({ 42 | type: types.CLEAR_COMPLETED 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /examples/todomvc/src/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 | export default class Footer extends Component { 12 | static propTypes = { 13 | completedCount: PropTypes.number.isRequired, 14 | activeCount: PropTypes.number.isRequired, 15 | filter: PropTypes.string.isRequired, 16 | onClearCompleted: PropTypes.func.isRequired, 17 | onShow: PropTypes.func.isRequired 18 | } 19 | 20 | renderTodoCount() { 21 | const { activeCount } = this.props 22 | const itemWord = activeCount === 1 ? 'item' : 'items' 23 | 24 | return ( 25 | 26 | {activeCount || 'No'} {itemWord} left 27 | 28 | ) 29 | } 30 | 31 | renderFilterLink(filter) { 32 | const title = FILTER_TITLES[filter] 33 | const { filter: selectedFilter, onShow } = this.props 34 | 35 | return ( 36 | onShow(filter)}> 39 | {title} 40 | 41 | ) 42 | } 43 | 44 | renderClearButton() { 45 | const { completedCount, onClearCompleted } = this.props 46 | if (completedCount > 0) { 47 | return ( 48 | 52 | ) 53 | } 54 | } 55 | 56 | render() { 57 | return ( 58 |
59 | {this.renderTodoCount()} 60 |
    61 | {[ SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED ].map(filter => 62 |
  • 63 | {this.renderFilterLink(filter)} 64 |
  • 65 | )} 66 |
67 | {this.renderClearButton()} 68 |
69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import TodoTextInput from './TodoTextInput' 3 | 4 | export default class Header extends Component { 5 | static propTypes = { 6 | addTodo: PropTypes.func.isRequired 7 | } 8 | 9 | handleSave = text => { 10 | if (text.length !== 0) { 11 | this.props.addTodo(text) 12 | } 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 |

todos

19 | 22 |
23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/Header.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TestUtils from 'react-addons-test-utils' 3 | import Header from './Header' 4 | import TodoTextInput from './TodoTextInput' 5 | 6 | const setup = () => { 7 | const props = { 8 | addTodo: jest.fn() 9 | } 10 | 11 | const renderer = TestUtils.createRenderer() 12 | renderer.render(
) 13 | const output = renderer.getRenderOutput() 14 | 15 | return { 16 | props: props, 17 | output: output, 18 | renderer: renderer 19 | } 20 | } 21 | 22 | describe('components', () => { 23 | describe('Header', () => { 24 | it('should render correctly', () => { 25 | const { output } = setup() 26 | 27 | expect(output.type).toBe('header') 28 | expect(output.props.className).toBe('header') 29 | 30 | const [ h1, input ] = output.props.children 31 | 32 | expect(h1.type).toBe('h1') 33 | expect(h1.props.children).toBe('todos') 34 | 35 | expect(input.type).toBe(TodoTextInput) 36 | expect(input.props.newTodo).toBe(true) 37 | expect(input.props.placeholder).toBe('What needs to be done?') 38 | }) 39 | 40 | it('should call addTodo if length of text is greater than 0', () => { 41 | const { output, props } = setup() 42 | const input = output.props.children[1] 43 | input.props.onSave('') 44 | expect(props.addTodo).not.toBeCalled() 45 | input.props.onSave('Use Redux') 46 | expect(props.addTodo).toBeCalled() 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /examples/todomvc/src/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 | export default class MainSection extends Component { 13 | static propTypes = { 14 | todos: PropTypes.array.isRequired, 15 | actions: PropTypes.object.isRequired 16 | } 17 | 18 | state = { filter: SHOW_ALL } 19 | 20 | handleClearCompleted = () => { 21 | this.props.actions.clearCompleted() 22 | } 23 | 24 | handleShow = filter => { 25 | this.setState({ filter }) 26 | } 27 | 28 | renderToggleAll(completedCount) { 29 | const { todos, actions } = this.props 30 | if (todos.length > 0) { 31 | return ( 32 | 36 | ) 37 | } 38 | } 39 | 40 | renderFooter(completedCount) { 41 | const { todos } = this.props 42 | const { filter } = this.state 43 | const activeCount = todos.length - completedCount 44 | 45 | if (todos.length) { 46 | return ( 47 |