├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── PATRONS.md ├── README.md ├── book.json ├── build ├── es3ify.js └── use-lodash-es.js ├── docs ├── 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 │ ├── MigratingToRedux.md │ ├── README.md │ ├── ReducingBoilerplate.md │ ├── ServerRendering.md │ ├── UsingObjectSpreadOperator.md │ └── WritingTests.md ├── examples ├── README.md ├── async │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── components │ │ ├── Picker.js │ │ └── Posts.js │ ├── containers │ │ └── App.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ └── index.js │ ├── server.js │ ├── store │ │ └── configureStore.js │ └── webpack.config.js ├── buildAll.js ├── counter-vanilla │ └── index.html ├── counter │ ├── .babelrc │ ├── components │ │ └── Counter.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ └── index.js │ ├── server.js │ ├── test │ │ ├── .eslintrc │ │ ├── components │ │ │ └── Counter.spec.js │ │ ├── reducers │ │ │ └── counter.spec.js │ │ └── setup.js │ └── webpack.config.js ├── real-world │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── components │ │ ├── Explore.js │ │ ├── List.js │ │ ├── Repo.js │ │ └── User.js │ ├── containers │ │ ├── App.js │ │ ├── DevTools.js │ │ ├── RepoPage.js │ │ ├── Root.dev.js │ │ ├── Root.js │ │ ├── Root.prod.js │ │ └── UserPage.js │ ├── index.html │ ├── index.js │ ├── middleware │ │ └── api.js │ ├── package.json │ ├── reducers │ │ ├── index.js │ │ └── paginate.js │ ├── routes.js │ ├── server.js │ ├── store │ │ ├── configureStore.dev.js │ │ ├── configureStore.js │ │ └── configureStore.prod.js │ └── webpack.config.js ├── shopping-cart │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── api │ │ ├── products.json │ │ └── shop.js │ ├── components │ │ ├── Cart.js │ │ ├── Product.js │ │ ├── ProductItem.js │ │ └── ProductsList.js │ ├── constants │ │ └── ActionTypes.js │ ├── containers │ │ ├── App.js │ │ ├── CartContainer.js │ │ └── ProductsContainer.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ ├── cart.js │ │ ├── index.js │ │ └── products.js │ ├── server.js │ └── webpack.config.js ├── testAll.js ├── todomvc │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── components │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── MainSection.js │ │ ├── TodoItem.js │ │ └── TodoTextInput.js │ ├── constants │ │ ├── ActionTypes.js │ │ └── TodoFilters.js │ ├── containers │ │ └── App.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ ├── index.js │ │ └── todos.js │ ├── server.js │ ├── store │ │ └── configureStore.js │ ├── test │ │ ├── .eslintrc │ │ ├── actions │ │ │ └── todos.spec.js │ │ ├── components │ │ │ ├── Footer.spec.js │ │ │ ├── Header.spec.js │ │ │ ├── MainSection.spec.js │ │ │ ├── TodoItem.spec.js │ │ │ └── TodoTextInput.spec.js │ │ ├── reducers │ │ │ └── todos.spec.js │ │ └── setup.js │ └── webpack.config.js ├── todos-with-undo │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── components │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Link.js │ │ ├── Todo.js │ │ └── TodoList.js │ ├── containers │ │ ├── AddTodo.js │ │ ├── FilterLink.js │ │ ├── UndoRedo.js │ │ └── VisibleTodoList.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ ├── index.js │ │ ├── todos.js │ │ └── visibilityFilter.js │ ├── server.js │ └── webpack.config.js ├── todos │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── components │ │ ├── AddTodoForm.js │ │ ├── App.js │ │ ├── Footer.js │ │ ├── Link.js │ │ ├── Todo.js │ │ └── TodoList.js │ ├── containers │ │ ├── AddTodo.js │ │ ├── FilterLink.js │ │ └── VisibleTodoList.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ ├── index.js │ │ ├── todos.js │ │ └── visibilityFilter.js │ ├── server.js │ ├── test │ │ ├── .eslintrc │ │ ├── actions │ │ │ └── todos.spec.js │ │ ├── reducers │ │ │ └── todos.spec.js │ │ └── setup.js │ └── webpack.config.js ├── tree-view │ ├── .babelrc │ ├── actions │ │ └── index.js │ ├── containers │ │ └── Node.js │ ├── generateTree.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ │ └── index.js │ ├── server.js │ ├── store │ │ └── configureStore.js │ ├── test │ │ ├── .eslintrc │ │ └── reducer.spec.js │ └── webpack.config.js └── universal │ ├── .babelrc │ ├── client │ └── index.js │ ├── common │ ├── actions │ │ └── index.js │ ├── api │ │ └── counter.js │ ├── components │ │ └── Counter.js │ ├── containers │ │ └── App.js │ ├── reducers │ │ ├── counter.js │ │ └── index.js │ └── store │ │ └── configureStore.js │ ├── index.js │ ├── package.json │ ├── server │ ├── index.js │ └── server.js │ └── webpack.config.js ├── 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 └── 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 | ], 22 | "env": { 23 | "commonjs": { 24 | "plugins": [ 25 | ["transform-es2015-modules-commonjs", { "loose": true }] 26 | ] 27 | }, 28 | "es": { 29 | "plugins": [ 30 | "./build/use-lodash-es" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/* 2 | **/node_modules/* 3 | **/server.js 4 | **/webpack.config*.js 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "rackt", 3 | "rules": { 4 | "valid-jsdoc": 2, 5 | // Disable until Flow supports let and const 6 | "no-var": 0, 7 | "react/jsx-uses-react": 1, 8 | "react/jsx-no-undef": 2, 9 | "react/wrap-multilines": 2 10 | }, 11 | "plugins": [ 12 | "react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/lib 3 | .*/test 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /.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 | script: 6 | - npm run check:src 7 | - npm run build 8 | - npm run check:examples 9 | branches: 10 | only: 11 | - master 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dan.abramov@me.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 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 | * [Herman J. Radtke III](http://hermanradtke.com) 9 | * [Ken Wheeler](http://kenwheeler.github.io/) 10 | * [Chung Yen Li](https://www.facebook.com/prototocal.lee) 11 | * [Sunil Pai](https://twitter.com/threepointone) 12 | * [Charlie Cheever](https://twitter.com/ccheever) 13 | * [Eugene G](https://twitter.com/e1g) 14 | * [Matt Apperson](https://twitter.com/mattapperson) 15 | * [Jed Watson](https://twitter.com/jedwatson) 16 | * [Sasha Aickin](https://twitter.com/xander76) 17 | * [Stefan Tennigkeit](https://twitter.com/whobubble) 18 | * [Sam Vincent](https://twitter.com/samvincent) 19 | * Olegzandr Denman 20 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "2.4.3", 3 | "structure": { 4 | "summary": "docs/README.md" 5 | }, 6 | "plugins": ["edit-link", "prism", "-highlight", "github"], 7 | "pluginsConfig": { 8 | "edit-link": { 9 | "base": "https://github.com/rackt/redux/tree/master", 10 | "label": "Edit This Page" 11 | }, 12 | "github": { 13 | "url": "https://github.com/rackt/redux/" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build/es3ify.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob') 2 | var fs = require('fs') 3 | var es3ify = require('es3ify') 4 | 5 | glob('./@(lib|dist|es)/**/*.js', function (err, files) { 6 | if (err) { 7 | throw err 8 | } 9 | 10 | files.forEach(function (file) { 11 | fs.readFile(file, 'utf8', function (err, data) { 12 | if (err) { 13 | throw err 14 | } 15 | 16 | fs.writeFile(file, es3ify.transform(data), function (err) { 17 | if (err) { 18 | throw err 19 | } 20 | 21 | console.log('es3ified ' + file) // eslint-disable-line no-console 22 | }) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /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/README.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | * [Read Me](/README.md) 4 | * [Introduction](/docs/introduction/README.md) 5 | * [Motivation](/docs/introduction/Motivation.md) 6 | * [Three Principles](/docs/introduction/ThreePrinciples.md) 7 | * [Prior Art](/docs/introduction/PriorArt.md) 8 | * [Ecosystem](/docs/introduction/Ecosystem.md) 9 | * [Examples](/docs/introduction/Examples.md) 10 | * [Basics](/docs/basics/README.md) 11 | * [Actions](/docs/basics/Actions.md) 12 | * [Reducers](/docs/basics/Reducers.md) 13 | * [Store](/docs/basics/Store.md) 14 | * [Data Flow](/docs/basics/DataFlow.md) 15 | * [Usage with React](/docs/basics/UsageWithReact.md) 16 | * [Example: Todo List](/docs/basics/ExampleTodoList.md) 17 | * [Advanced](/docs/advanced/README.md) 18 | * [Async Actions](/docs/advanced/AsyncActions.md) 19 | * [Async Flow](/docs/advanced/AsyncFlow.md) 20 | * [Middleware](/docs/advanced/Middleware.md) 21 | * Usage with React Router 22 | * [Example: Reddit API](/docs/advanced/ExampleRedditAPI.md) 23 | * Next Steps 24 | * [Recipes](/docs/recipes/README.md) 25 | * [Migrating to Redux](/docs/recipes/MigratingToRedux.md) 26 | * [Using Object Spread Operator](/docs/recipes/UsingObjectSpreadOperator.md) 27 | * [Reducing Boilerplate](/docs/recipes/ReducingBoilerplate.md) 28 | * [Server Rendering](/docs/recipes/ServerRendering.md) 29 | * [Writing Tests](/docs/recipes/WritingTests.md) 30 | * [Computing Derived Data](/docs/recipes/ComputingDerivedData.md) 31 | * [Implementing Undo History](/docs/recipes/ImplementingUndoHistory.md) 32 | * [Troubleshooting](/docs/Troubleshooting.md) 33 | * [Glossary](/docs/Glossary.md) 34 | * [API Reference](/docs/api/README.md) 35 | * [createStore](/docs/api/createStore.md) 36 | * [Store](/docs/api/Store.md) 37 | * [combineReducers](/docs/api/combineReducers.md) 38 | * [applyMiddleware](/docs/api/applyMiddleware.md) 39 | * [bindActionCreators](/docs/api/bindActionCreators.md) 40 | * [compose](/docs/api/compose.md) 41 | * [Change Log](/CHANGELOG.md) 42 | * [Patrons](/PATRONS.md) 43 | -------------------------------------------------------------------------------- /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 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/advanced/UsageWithReactRouter.md: -------------------------------------------------------------------------------- 1 | # Usage with React Router 2 | 3 | Sorry, but we’re still writing this doc. 4 | Stay tuned, it will appear in a day or two. 5 | -------------------------------------------------------------------------------- /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, [initialState])](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/api/createStore.md: -------------------------------------------------------------------------------- 1 | # `createStore(reducer, [initialState], [enhancer])` 2 | 3 | Creates a Redux [store](Store.md) that holds the complete state tree of your app. 4 | There should only be a single store in your app. 5 | 6 | #### Arguments 7 | 8 | 1. `reducer` *(Function)*: A [reducing function](../Glossary.md#reducer) that returns the next [state tree](../Glossary.md#state), given the current state tree and an [action](../Glossary.md#action) to handle. 9 | 10 | 2. [`initialState`] *(any)*: The initial state. You may optionally specify it to hydrate the state from the server in universal apps, or to restore a previously serialized user session. If you produced `reducer` with [`combineReducers`](combineReducers.md), this must be a plain object with the same shape as the keys passed to it. Otherwise, you are free to pass anything that your `reducer` can understand. 11 | 12 | 3. [`enhancer`] *(Function)*: The store enhancer. You may optionally specify it to enhance the store with third-party capabilities such as middleware, time travel, persistence, etc. The only store enhancer that ships with Redux is [`applyMiddleware()`](./applyMiddleware.md). 13 | 14 | #### Returns 15 | 16 | ([*`Store`*](Store.md)): An object that holds the complete state of your app. The only way to change its state is by [dispatching actions](Store.md#dispatch). You may also [subscribe](Store.md#subscribe) to the changes to its state to update the UI. 17 | 18 | #### Example 19 | 20 | ```js 21 | import { createStore } from 'redux' 22 | 23 | function todos(state = [], action) { 24 | switch (action.type) { 25 | case 'ADD_TODO': 26 | return state.concat([ action.text ]) 27 | default: 28 | return state 29 | } 30 | } 31 | 32 | let store = createStore(todos, [ 'Use Redux' ]) 33 | 34 | store.dispatch({ 35 | type: 'ADD_TODO', 36 | text: 'Read the docs' 37 | }) 38 | 39 | console.log(store.getState()) 40 | // [ 'Use Redux', 'Read the docs' ] 41 | ``` 42 | 43 | #### Tips 44 | 45 | * Don’t create more than one store in an application! Instead, use [`combineReducers`](combineReducers.md) to create a single root reducer out of many. 46 | 47 | * It is up to you to choose the state format. You can use plain objects or something like [Immutable](http://facebook.github.io/immutable-js/). If you’re not sure, start with plain objects. 48 | 49 | * If your state is a plain object, make sure you never mutate it! For example, instead of returning something like `Object.assign(state, newData)` from your reducers, return `Object.assign({}, state, newData)`. This way you don’t override the previous `state`. You can also write `return { ...state, ...newData }` if you enable the [object spread operator proposal](../recipes/UsingObjectSpreadOperator.md). 50 | 51 | * For universal apps that run on the server, create a store instance with every request so that they are isolated. Dispatch a few data fetching actions to a store instance and wait for them to complete before rendering the app on the server. 52 | 53 | * When a store is created, Redux dispatches a dummy action to your reducer to populate the store with the initial state. You are not meant to handle the dummy action directly. Just remember that your reducer should return some kind of initial state if the state given to it as the first argument is `undefined`, and you’re all set. 54 | 55 | * To apply multiple store enhancers, you may use [`compose()`](./compose.md). 56 | -------------------------------------------------------------------------------- /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/basics/Store.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | In the previous sections, we defined the [actions](Actions.md) that represent the facts about “what happened” and the [reducers](Reducers.md) that update the state according to those actions. 4 | 5 | The **Store** is the object that brings them together. The store has the following responsibilities: 6 | 7 | * Holds application state; 8 | * Allows access to state via [`getState()`](../api/Store.md#getState); 9 | * Allows state to be updated via [`dispatch(action)`](../api/Store.md#dispatch); 10 | * Registers listeners via [`subscribe(listener)`](../api/Store.md#subscribe); 11 | * Handles unregistering of listeners via the function returned by [`subscribe(listener)`](../api/Store.md#subscribe). 12 | 13 | It’s important to note that you’ll only have a single store in a Redux application. When you want to split your data handling logic, you’ll use [reducer composition](Reducers.md#splitting-reducers) instead of many stores. 14 | 15 | It’s easy to create a store if you have a reducer. In the [previous section](Reducers.md), we used [`combineReducers()`](../api/combineReducers.md) to combine several reducers into one. We will now import it, and pass it to [`createStore()`](../api/createStore.md). 16 | 17 | ```js 18 | import { createStore } from 'redux' 19 | import todoApp from './reducers' 20 | let store = createStore(todoApp) 21 | ``` 22 | 23 | You may optionally specify the initial state as the second argument to [`createStore()`](../api/createStore.md). This is useful for hydrating the state of the client to match the state of a Redux application running on the server. 24 | 25 | ```js 26 | let store = createStore(todoApp, window.STATE_FROM_SERVER) 27 | ``` 28 | 29 | ## Dispatching Actions 30 | 31 | Now that we have created a store, let’s verify our program works! Even without any UI, we can already test the update logic. 32 | 33 | ```js 34 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from './actions' 35 | 36 | // Log the initial state 37 | console.log(store.getState()) 38 | 39 | // Every time the state changes, log it 40 | // Note that subscribe() returns a function for unregistering the listener 41 | let unsubscribe = store.subscribe(() => 42 | console.log(store.getState()) 43 | ) 44 | 45 | // Dispatch some actions 46 | store.dispatch(addTodo('Learn about actions')) 47 | store.dispatch(addTodo('Learn about reducers')) 48 | store.dispatch(addTodo('Learn about store')) 49 | store.dispatch(completeTodo(0)) 50 | store.dispatch(completeTodo(1)) 51 | store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) 52 | 53 | // Stop listening to state updates 54 | unsubscribe() 55 | ``` 56 | 57 | You can see how this causes the state held by the store to change: 58 | 59 | 60 | 61 | We specified the behavior of our app before we even started writing the UI. We won’t do this in this tutorial, but at this point you can write tests for your reducers and action creators. You won’t need to mock anything because they are just functions. Call them, and make assertions on what they return. 62 | 63 | ## Source Code 64 | 65 | #### `index.js` 66 | 67 | ```js 68 | import { createStore } from 'redux' 69 | import todoApp from './reducers' 70 | 71 | let store = createStore(todoApp) 72 | ``` 73 | 74 | ## Next Steps 75 | 76 | Before creating a UI for our todo app, we will take a detour to see [how the data flows in a Redux application](DataFlow.md). 77 | -------------------------------------------------------------------------------- /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/introduction/ThreePrinciples.md: -------------------------------------------------------------------------------- 1 | # Three Principles 2 | 3 | Redux can be described in three fundamental principles: 4 | 5 | ### Single source of truth 6 | 7 | **The [state](../Glossary.md#state) of your whole application is stored in an object tree within a single [store](../Glossary.md#store).** 8 | 9 | This makes it easy to create universal apps, as the state from your server can be serialized and hydrated into the client with no extra coding effort. A single state tree also makes it easier to debug or introspect an application; it also enables you to persist your app’s state in development, for a faster development cycle. Some functionality which has been traditionally difficult to implement - Undo/Redo, for example - can suddenly become trivial to implement, if all of your state is stored in a single tree. 10 | 11 | ```js 12 | console.log(store.getState()) 13 | 14 | /* Prints 15 | { 16 | visibilityFilter: 'SHOW_ALL', 17 | todos: [ 18 | { 19 | text: 'Consider using Redux', 20 | completed: true, 21 | }, 22 | { 23 | text: 'Keep all state in a single tree', 24 | completed: false 25 | } 26 | ] 27 | } 28 | */ 29 | ``` 30 | 31 | ### State is read-only 32 | 33 | **The only way to mutate the state is to emit an [action](../Glossary.md#action), an object describing what happened.** 34 | 35 | This ensures that neither the views nor the network callbacks will ever write directly to the state. Instead, they express an intent to mutate. Because all mutations are centralized and happen one by one in a strict order, there are no subtle race conditions to watch out for. As actions are just plain objects, they can be logged, serialized, stored, and later replayed for debugging or testing purposes. 36 | 37 | ```js 38 | store.dispatch({ 39 | type: 'COMPLETE_TODO', 40 | index: 1 41 | }) 42 | 43 | store.dispatch({ 44 | type: 'SET_VISIBILITY_FILTER', 45 | filter: 'SHOW_COMPLETED' 46 | }) 47 | ``` 48 | 49 | ### Changes are made with pure functions 50 | 51 | **To specify how the state tree is transformed by actions, you write pure [reducers](../Glossary.md#reducer).** 52 | 53 | Reducers are just pure functions that take the previous state and an action, and return the next state. Remember to return new state objects, instead of mutating the previous state. You can start with a single reducer, and as your app grows, split it off into smaller reducers that manage specific parts of the state tree. Because reducers are just functions, you can control the order in which they are called, pass additional data, or even make reusable reducers for common tasks such as pagination. 54 | 55 | ```js 56 | 57 | function visibilityFilter(state = 'SHOW_ALL', action) { 58 | switch (action.type) { 59 | case 'SET_VISIBILITY_FILTER': 60 | return action.filter 61 | default: 62 | return state 63 | } 64 | } 65 | 66 | function todos(state = [], action) { 67 | switch (action.type) { 68 | case 'ADD_TODO': 69 | return [ 70 | ...state, 71 | { 72 | text: action.text, 73 | completed: false 74 | } 75 | ] 76 | case 'COMPLETE_TODO': 77 | return [ 78 | ...state.slice(0, action.index), 79 | Object.assign({}, state[action.index], { 80 | completed: true 81 | }), 82 | ...state.slice(action.index + 1) 83 | ] 84 | default: 85 | return state 86 | } 87 | } 88 | 89 | import { combineReducers, createStore } from 'redux' 90 | let reducer = combineReducers({ visibilityFilter, todos }) 91 | let store = createStore(reducer) 92 | ``` 93 | 94 | That’s it! Now you know what Redux is all about. 95 | -------------------------------------------------------------------------------- /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 | 13 | -------------------------------------------------------------------------------- /docs/recipes/UsingObjectSpreadOperator.md: -------------------------------------------------------------------------------- 1 | # Using Object Spread Operator 2 | 3 | Since one of the core tenets of Redux is to never mutate state, you’ll often find yourself using [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) to create 4 | copies of objects with new or updated values. For example, in the `todoApp` below `Object.assign()` is used to return a new 5 | `state` object with an updated `visibilityFilter` property: 6 | 7 | ```js 8 | function todoApp(state = initialState, action) { 9 | switch (action.type) { 10 | case SET_VISIBILITY_FILTER: 11 | return Object.assign({}, state, { 12 | visibilityFilter: action.filter 13 | }) 14 | default: 15 | return state 16 | } 17 | } 18 | ``` 19 | 20 | While effective, using `Object.assign()` can quickly make simple reducers difficult to read given its rather verbose syntax. 21 | 22 | An alternative approach is to use the [object spread syntax](https://github.com/sebmarkbage/ecmascript-rest-spread) proposed for the next versions of JavaScript which lets you use the spread (`...`) operator to copy enumerable properties from one object to another in a more succinct way. The object spread operator is conceptually similar to the ES6 [array spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator). We 23 | can simplify the `todoApp` example above by using the object spread syntax: 24 | 25 | ```js 26 | function todoApp(state = initialState, action) { 27 | switch (action.type) { 28 | case SET_VISIBILITY_FILTER: 29 | return { ...state, visibilityFilter: action.filter } 30 | default: 31 | return state 32 | } 33 | } 34 | ``` 35 | 36 | The advantage of using the object spread syntax becomes more apparent when you’re composing complex objects. Below `getAddedIds` maps an array of `id` values to an array of objects with values returned from `getProduct` and `getQuantity`. 37 | 38 | ```js 39 | return getAddedIds(state.cart).map(id => Object.assign( 40 | {}, 41 | getProduct(state.products, id), 42 | { 43 | quantity: getQuantity(state.cart, id) 44 | } 45 | )) 46 | ``` 47 | 48 | Object spread lets us simplify the above `map` call to: 49 | 50 | ```js 51 | return getAddedIds(state.cart).map(id => ({ 52 | ...getProduct(state.products, id), 53 | quantity: getQuantity(state.cart, id) 54 | })) 55 | ``` 56 | 57 | Since the object spread syntax is still a Stage 2 proposal for ECMAScript you’ll need to use a transpiler such as [Babel](http://babeljs.io/) to use it in production. You can use your existing `es2015` preset, install [`babel-plugin-transform-object-rest-spread`](http://babeljs.io/docs/plugins/transform-object-rest-spread/) and add it individually to the `plugins` array in your `.babelrc`. 58 | 59 | ```js 60 | { 61 | "presets": ["es2015"], 62 | "plugins": ["transform-object-rest-spread"] 63 | } 64 | ``` 65 | 66 | Note that this is still an experimental language feature proposal so it may change in the future. Nevertheless some large projects such as [React Native](https://github.com/facebook/react-native) already use it extensively so it is safe to say that there will be a good automated migration path if it changes. 67 | -------------------------------------------------------------------------------- /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/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/async/actions/index.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | 3 | export const REQUEST_POSTS = 'REQUEST_POSTS' 4 | export const RECEIVE_POSTS = 'RECEIVE_POSTS' 5 | export const SELECT_REDDIT = 'SELECT_REDDIT' 6 | export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT' 7 | 8 | export function selectReddit(reddit) { 9 | return { 10 | type: SELECT_REDDIT, 11 | reddit 12 | } 13 | } 14 | 15 | export function invalidateReddit(reddit) { 16 | return { 17 | type: INVALIDATE_REDDIT, 18 | reddit 19 | } 20 | } 21 | 22 | function requestPosts(reddit) { 23 | return { 24 | type: REQUEST_POSTS, 25 | reddit 26 | } 27 | } 28 | 29 | function receivePosts(reddit, json) { 30 | return { 31 | type: RECEIVE_POSTS, 32 | reddit: reddit, 33 | posts: json.data.children.map(child => child.data), 34 | receivedAt: Date.now() 35 | } 36 | } 37 | 38 | function fetchPosts(reddit) { 39 | return dispatch => { 40 | dispatch(requestPosts(reddit)) 41 | return fetch(`https://www.reddit.com/r/${reddit}.json`) 42 | .then(response => response.json()) 43 | .then(json => dispatch(receivePosts(reddit, json))) 44 | } 45 | } 46 | 47 | function shouldFetchPosts(state, reddit) { 48 | const posts = state.postsByReddit[reddit] 49 | if (!posts) { 50 | return true 51 | } 52 | if (posts.isFetching) { 53 | return false 54 | } 55 | return posts.didInvalidate 56 | } 57 | 58 | export function fetchPostsIfNeeded(reddit) { 59 | return (dispatch, getState) => { 60 | if (shouldFetchPosts(getState(), reddit)) { 61 | return dispatch(fetchPosts(reddit)) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/async/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class Picker extends Component { 4 | render() { 5 | const { value, onChange, options } = this.props 6 | 7 | return ( 8 | 9 |

{value}

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

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

60 | {isEmpty 61 | ? (isFetching ?

Loading...

:

Empty.

) 62 | :
63 | 64 |
65 | } 66 |
67 | ) 68 | } 69 | } 70 | 71 | App.propTypes = { 72 | selectedReddit: PropTypes.string.isRequired, 73 | posts: PropTypes.array.isRequired, 74 | isFetching: PropTypes.bool.isRequired, 75 | lastUpdated: PropTypes.number, 76 | dispatch: PropTypes.func.isRequired 77 | } 78 | 79 | function mapStateToProps(state) { 80 | const { selectedReddit, postsByReddit } = state 81 | const { 82 | isFetching, 83 | lastUpdated, 84 | items: posts 85 | } = postsByReddit[selectedReddit] || { 86 | isFetching: true, 87 | items: [] 88 | } 89 | 90 | return { 91 | selectedReddit, 92 | posts, 93 | isFetching, 94 | lastUpdated 95 | } 96 | } 97 | 98 | export default connect(mapStateToProps)(App) 99 | -------------------------------------------------------------------------------- /examples/async/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux async example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/async/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { Provider } from 'react-redux' 5 | import App from './containers/App' 6 | import configureStore from './store/configureStore' 7 | 8 | const store = configureStore() 9 | 10 | render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ) 16 | -------------------------------------------------------------------------------- /examples/async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-async-example", 3 | "version": "0.0.0", 4 | "description": "Redux async example", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/reactjs/redux.git" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "reactjs", 15 | "hot", 16 | "reload", 17 | "hmr", 18 | "live", 19 | "edit", 20 | "webpack", 21 | "flux" 22 | ], 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/reactjs/redux/issues" 26 | }, 27 | "homepage": "http://redux.js.org", 28 | "dependencies": { 29 | "babel-polyfill": "^6.3.14", 30 | "isomorphic-fetch": "^2.1.1", 31 | "react": "^0.14.7", 32 | "react-dom": "^0.14.7", 33 | "react-redux": "^4.2.1", 34 | "redux": "^3.2.1", 35 | "redux-logger": "^2.4.0", 36 | "redux-thunk": "^1.0.3" 37 | }, 38 | "devDependencies": { 39 | "babel-core": "^6.3.15", 40 | "babel-loader": "^6.2.0", 41 | "babel-preset-es2015": "^6.3.13", 42 | "babel-preset-react": "^6.3.13", 43 | "babel-preset-react-hmre": "^1.0.1", 44 | "expect": "^1.6.0", 45 | "express": "^4.13.3", 46 | "node-libs-browser": "^0.5.2", 47 | "webpack": "^1.9.11", 48 | "webpack-dev-middleware": "^1.2.0", 49 | "webpack-hot-middleware": "^2.2.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/async/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { 3 | SELECT_REDDIT, INVALIDATE_REDDIT, 4 | REQUEST_POSTS, RECEIVE_POSTS 5 | } from '../actions' 6 | 7 | function selectedReddit(state = 'reactjs', action) { 8 | switch (action.type) { 9 | case SELECT_REDDIT: 10 | return action.reddit 11 | default: 12 | return state 13 | } 14 | } 15 | 16 | function posts(state = { 17 | isFetching: false, 18 | didInvalidate: false, 19 | items: [] 20 | }, action) { 21 | switch (action.type) { 22 | case INVALIDATE_REDDIT: 23 | return Object.assign({}, state, { 24 | didInvalidate: true 25 | }) 26 | case REQUEST_POSTS: 27 | return Object.assign({}, state, { 28 | isFetching: true, 29 | didInvalidate: false 30 | }) 31 | case RECEIVE_POSTS: 32 | return Object.assign({}, state, { 33 | isFetching: false, 34 | didInvalidate: false, 35 | items: action.posts, 36 | lastUpdated: action.receivedAt 37 | }) 38 | default: 39 | return state 40 | } 41 | } 42 | 43 | function postsByReddit(state = { }, action) { 44 | switch (action.type) { 45 | case INVALIDATE_REDDIT: 46 | case RECEIVE_POSTS: 47 | case REQUEST_POSTS: 48 | return Object.assign({}, state, { 49 | [action.reddit]: posts(state[action.reddit], action) 50 | }) 51 | default: 52 | return state 53 | } 54 | } 55 | 56 | const rootReducer = combineReducers({ 57 | postsByReddit, 58 | selectedReddit 59 | }) 60 | 61 | export default rootReducer 62 | -------------------------------------------------------------------------------- /examples/async/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /examples/async/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunkMiddleware from 'redux-thunk' 3 | import createLogger from 'redux-logger' 4 | import rootReducer from '../reducers' 5 | 6 | export default function configureStore(initialState) { 7 | const store = createStore( 8 | rootReducer, 9 | initialState, 10 | applyMiddleware(thunkMiddleware, createLogger()) 11 | ) 12 | 13 | if (module.hot) { 14 | // Enable Webpack hot module replacement for reducers 15 | module.hot.accept('../reducers', () => { 16 | const nextRootReducer = require('../reducers').default 17 | store.replaceReducer(nextRootReducer) 18 | }) 19 | } 20 | 21 | return store 22 | } 23 | -------------------------------------------------------------------------------- /examples/async/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | loaders: ['babel'], 25 | exclude: /node_modules/, 26 | include: __dirname 27 | } 28 | ] 29 | } 30 | } 31 | 32 | 33 | // When inside Redux repo, prefer src to compiled version. 34 | // You can safely delete these lines in your project. 35 | var reduxSrc = path.join(__dirname, '..', '..', 'src') 36 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules') 37 | var fs = require('fs') 38 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) { 39 | // Resolve Redux to source 40 | module.exports.resolve = { alias: { 'redux': reduxSrc } } 41 | // Our root .babelrc needs this flag for CommonJS output 42 | process.env.BABEL_ENV = 'commonjs' 43 | // Compile Redux from source 44 | module.exports.module.loaders.push({ 45 | test: /\.js$/, 46 | loaders: ['babel'], 47 | include: reduxSrc 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /examples/buildAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs an ordered set of commands within each of the build directories. 3 | */ 4 | 5 | import fs from 'fs' 6 | import path from 'path' 7 | import { spawnSync } from 'child_process' 8 | 9 | var exampleDirs = fs.readdirSync(__dirname).filter((file) => { 10 | return fs.statSync(path.join(__dirname, file)).isDirectory() 11 | }) 12 | 13 | // Ordering is important here. `npm install` must come first. 14 | var cmdArgs = [ 15 | { cmd: 'npm', args: [ 'install' ] }, 16 | { cmd: 'webpack', args: [ 'index.js' ] } 17 | ] 18 | 19 | for (const dir of exampleDirs) { 20 | for (const cmdArg of cmdArgs) { 21 | // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 22 | const opts = { 23 | cwd: path.join(__dirname, dir), 24 | stdio: 'inherit' 25 | } 26 | let result = {} 27 | if (process.platform === 'win32') { 28 | result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts) 29 | } else { 30 | result = spawnSync(cmdArg.cmd, cmdArg.args, opts) 31 | } 32 | if (result.status !== 0) { 33 | throw new Error('Building examples exited with non-zero') 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/counter-vanilla/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux basic example 5 | 6 | 7 | 8 |
9 |

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

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

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

42 | ) 43 | } 44 | } 45 | 46 | Counter.propTypes = { 47 | value: PropTypes.number.isRequired, 48 | onIncrement: PropTypes.func.isRequired, 49 | onDecrement: PropTypes.func.isRequired 50 | } 51 | 52 | export default Counter 53 | -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux counter example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/counter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { createStore } from 'redux' 4 | import Counter from './components/Counter' 5 | import counter from './reducers' 6 | 7 | const store = createStore(counter) 8 | const rootEl = document.getElementById('root') 9 | 10 | function render() { 11 | ReactDOM.render( 12 | store.dispatch({ type: 'INCREMENT' })} 15 | onDecrement={() => store.dispatch({ type: 'DECREMENT' })} 16 | />, 17 | rootEl 18 | ) 19 | } 20 | 21 | render() 22 | store.subscribe(render) 23 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-counter-example", 3 | "version": "0.0.0", 4 | "description": "Redux counter example", 5 | "scripts": { 6 | "start": "node server.js", 7 | "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js", 8 | "test:watch": "npm test -- --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/reactjs/redux.git" 13 | }, 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/reactjs/redux/issues" 17 | }, 18 | "homepage": "http://redux.js.org", 19 | "dependencies": { 20 | "react": "^0.14.7", 21 | "react-dom": "^0.14.7", 22 | "react-redux": "^4.2.1", 23 | "redux": "^3.2.1" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.3.15", 27 | "babel-loader": "^6.2.0", 28 | "babel-preset-es2015": "^6.3.13", 29 | "babel-preset-react": "^6.3.13", 30 | "babel-preset-react-hmre": "^1.0.1", 31 | "babel-register": "^6.3.13", 32 | "cross-env": "^1.0.7", 33 | "expect": "^1.6.0", 34 | "express": "^4.13.3", 35 | "jsdom": "^5.6.1", 36 | "mocha": "^2.2.5", 37 | "node-libs-browser": "^0.5.2", 38 | "react-addons-test-utils": "^0.14.7", 39 | "webpack": "^1.9.11", 40 | "webpack-dev-middleware": "^1.2.0", 41 | "webpack-hot-middleware": "^2.2.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/counter/reducers/index.js: -------------------------------------------------------------------------------- 1 | export default function counter(state = 0, action) { 2 | switch (action.type) { 3 | case 'INCREMENT': 4 | return state + 1 5 | case 'DECREMENT': 6 | return state - 1 7 | default: 8 | return state 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/counter/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /examples/counter/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/counter/test/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import TestUtils from 'react-addons-test-utils' 4 | import Counter from '../../components/Counter' 5 | 6 | function setup(value = 0) { 7 | const actions = { 8 | onIncrement: expect.createSpy(), 9 | onDecrement: expect.createSpy() 10 | } 11 | const component = TestUtils.renderIntoDocument( 12 | 13 | ) 14 | return { 15 | component: component, 16 | actions: actions, 17 | buttons: TestUtils.scryRenderedDOMComponentsWithTag(component, 'button'), 18 | p: TestUtils.findRenderedDOMComponentWithTag(component, 'p') 19 | } 20 | } 21 | 22 | describe('Counter component', () => { 23 | it('should display count', () => { 24 | const { p } = setup() 25 | expect(p.textContent).toMatch(/^Clicked: 0 times/) 26 | }) 27 | 28 | it('first button should call onIncrement', () => { 29 | const { buttons, actions } = setup() 30 | TestUtils.Simulate.click(buttons[0]) 31 | expect(actions.onIncrement).toHaveBeenCalled() 32 | }) 33 | 34 | it('second button should call onDecrement', () => { 35 | const { buttons, actions } = setup() 36 | TestUtils.Simulate.click(buttons[1]) 37 | expect(actions.onDecrement).toHaveBeenCalled() 38 | }) 39 | 40 | it('third button should not call onIncrement if the counter is even', () => { 41 | const { buttons, actions } = setup(42) 42 | TestUtils.Simulate.click(buttons[2]) 43 | expect(actions.onIncrement).toNotHaveBeenCalled() 44 | }) 45 | 46 | it('third button should call onIncrement if the counter is odd', () => { 47 | const { buttons, actions } = setup(43) 48 | TestUtils.Simulate.click(buttons[2]) 49 | expect(actions.onIncrement).toHaveBeenCalled() 50 | }) 51 | 52 | it('third button should call onIncrement if the counter is odd and negative', () => { 53 | const { buttons, actions } = setup(-43) 54 | TestUtils.Simulate.click(buttons[2]) 55 | expect(actions.onIncrement).toHaveBeenCalled() 56 | }) 57 | 58 | it('fourth button should call onIncrement in a second', (done) => { 59 | const { buttons, actions } = setup() 60 | TestUtils.Simulate.click(buttons[3]) 61 | setTimeout(() => { 62 | expect(actions.onIncrement).toHaveBeenCalled() 63 | done() 64 | }, 1000) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /examples/counter/test/reducers/counter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import counter from '../../reducers' 3 | 4 | describe('reducers', () => { 5 | describe('counter', () => { 6 | it('should provide the initial state', () => { 7 | expect(counter(undefined, {})).toBe(0) 8 | }) 9 | 10 | it('should handle INCREMENT action', () => { 11 | expect(counter(1, { type: 'INCREMENT' })).toBe(2) 12 | }) 13 | 14 | it('should handle DECREMENT action', () => { 15 | expect(counter(1, { type: 'DECREMENT' })).toBe(0) 16 | }) 17 | 18 | it('should ignore unknown actions', () => { 19 | expect(counter(1, { type: 'unknown' })).toBe(1) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /examples/counter/test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom' 2 | 3 | global.document = jsdom('') 4 | global.window = document.defaultView 5 | global.navigator = global.window.navigator 6 | -------------------------------------------------------------------------------- /examples/counter/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | loaders: [ 'babel' ], 25 | exclude: /node_modules/, 26 | include: __dirname 27 | } 28 | ] 29 | } 30 | } 31 | 32 | 33 | // When inside Redux repo, prefer src to compiled version. 34 | // You can safely delete these lines in your project. 35 | var reduxSrc = path.join(__dirname, '..', '..', 'src') 36 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules') 37 | var fs = require('fs') 38 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) { 39 | // Resolve Redux to source 40 | module.exports.resolve = { alias: { 'redux': reduxSrc } } 41 | // Our root .babelrc needs this flag for CommonJS output 42 | process.env.BABEL_ENV = 'commonjs' 43 | // Compile Redux from source 44 | module.exports.module.loaders.push({ 45 | test: /\.js$/, 46 | loaders: [ 'babel' ], 47 | include: reduxSrc 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /examples/real-world/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/real-world/components/Explore.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | const GITHUB_REPO = 'https://github.com/reactjs/redux' 4 | 5 | export default class Explore extends Component { 6 | constructor(props) { 7 | super(props) 8 | this.handleKeyUp = this.handleKeyUp.bind(this) 9 | this.handleGoClick = this.handleGoClick.bind(this) 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | if (nextProps.value !== this.props.value) { 14 | this.setInputValue(nextProps.value) 15 | } 16 | } 17 | 18 | getInputValue() { 19 | return this.refs.input.value 20 | } 21 | 22 | setInputValue(val) { 23 | // Generally mutating DOM is a bad idea in React components, 24 | // but doing this for a single uncontrolled field is less fuss 25 | // than making it controlled and maintaining a state for it. 26 | this.refs.input.value = val 27 | } 28 | 29 | handleKeyUp(e) { 30 | if (e.keyCode === 13) { 31 | this.handleGoClick() 32 | } 33 | } 34 | 35 | handleGoClick() { 36 | this.props.onChange(this.getInputValue()) 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |

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

43 | 47 | 50 |

51 | Code on Github. 52 |

53 |

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

56 |
57 | ) 58 | } 59 | } 60 | 61 | Explore.propTypes = { 62 | value: PropTypes.string.isRequired, 63 | onChange: PropTypes.func.isRequired 64 | } 65 | -------------------------------------------------------------------------------- /examples/real-world/components/List.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class List extends Component { 4 | renderLoadMore() { 5 | const { isFetching, onLoadMoreClick } = this.props 6 | return ( 7 | 12 | ) 13 | } 14 | 15 | render() { 16 | const { 17 | isFetching, nextPageUrl, pageCount, 18 | items, renderItem, loadingLabel 19 | } = this.props 20 | 21 | const isEmpty = items.length === 0 22 | if (isEmpty && isFetching) { 23 | return

{loadingLabel}

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

Nothing here!

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

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

22 | {description && 23 |

{description}

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

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

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

31 | {errorMessage} 32 | {' '} 33 | ( 35 | Dismiss 36 | ) 37 |

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

Loading {name} details...

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

Loading {login}’s profile...

48 | } 49 | 50 | const { starredRepos, starredRepoOwners, starredPagination } = this.props 51 | return ( 52 |
53 | 54 |
55 | 60 |
61 | ) 62 | } 63 | } 64 | 65 | UserPage.propTypes = { 66 | login: PropTypes.string.isRequired, 67 | user: PropTypes.object, 68 | starredPagination: PropTypes.object, 69 | starredRepos: PropTypes.array.isRequired, 70 | starredRepoOwners: PropTypes.array.isRequired, 71 | loadUser: PropTypes.func.isRequired, 72 | loadStarred: PropTypes.func.isRequired 73 | } 74 | 75 | function mapStateToProps(state, ownProps) { 76 | const { login } = ownProps.params 77 | const { 78 | pagination: { starredByUser }, 79 | entities: { users, repos } 80 | } = state 81 | 82 | const starredPagination = starredByUser[login] || { ids: [] } 83 | const starredRepos = starredPagination.ids.map(id => repos[id]) 84 | const starredRepoOwners = starredRepos.map(repo => users[repo.owner]) 85 | 86 | return { 87 | login, 88 | starredRepos, 89 | starredRepoOwners, 90 | starredPagination, 91 | user: users[login] 92 | } 93 | } 94 | 95 | export default connect(mapStateToProps, { 96 | loadUser, 97 | loadStarred 98 | })(UserPage) 99 | -------------------------------------------------------------------------------- /examples/real-world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux real-world example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/real-world/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { browserHistory } from 'react-router' 5 | import { syncHistoryWithStore } from 'react-router-redux' 6 | import Root from './containers/Root' 7 | import configureStore from './store/configureStore' 8 | 9 | const store = configureStore() 10 | const history = syncHistoryWithStore(browserHistory, store) 11 | 12 | render( 13 | , 14 | document.getElementById('root') 15 | ) 16 | -------------------------------------------------------------------------------- /examples/real-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-real-world-example", 3 | "version": "0.0.0", 4 | "description": "Redux real-world example", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/reactjs/redux.git" 11 | }, 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/reactjs/redux/issues" 15 | }, 16 | "homepage": "http://redux.js.org", 17 | "dependencies": { 18 | "babel-polyfill": "^6.3.14", 19 | "humps": "^0.6.0", 20 | "isomorphic-fetch": "^2.1.1", 21 | "lodash": "^4.0.0", 22 | "normalizr": "^2.0.0", 23 | "react": "^0.14.7", 24 | "react-dom": "^0.14.7", 25 | "react-redux": "^4.2.1", 26 | "react-router": "2.0.0", 27 | "react-router-redux": "^4.0.0-rc.1", 28 | "redux": "^3.2.1", 29 | "redux-logger": "^2.4.0", 30 | "redux-thunk": "^1.0.3" 31 | }, 32 | "devDependencies": { 33 | "babel-core": "^6.3.15", 34 | "babel-loader": "^6.2.0", 35 | "babel-preset-es2015": "^6.3.13", 36 | "babel-preset-react": "^6.3.13", 37 | "babel-preset-react-hmre": "^1.0.1", 38 | "concurrently": "^0.1.1", 39 | "express": "^4.13.3", 40 | "redux-devtools": "^3.1.0", 41 | "redux-devtools-dock-monitor": "^1.0.1", 42 | "redux-devtools-log-monitor": "^1.0.3", 43 | "webpack": "^1.9.11", 44 | "webpack-dev-middleware": "^1.2.0", 45 | "webpack-hot-middleware": "^2.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/real-world/reducers/index.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../actions' 2 | import merge from 'lodash/merge' 3 | import paginate from './paginate' 4 | import { routerReducer as routing } from 'react-router-redux' 5 | import { combineReducers } from 'redux' 6 | 7 | // Updates an entity cache in response to any action with response.entities. 8 | function entities(state = { users: {}, repos: {} }, action) { 9 | if (action.response && action.response.entities) { 10 | return merge({}, state, action.response.entities) 11 | } 12 | 13 | return state 14 | } 15 | 16 | // Updates error message to notify about the failed fetches. 17 | function errorMessage(state = null, action) { 18 | const { type, error } = action 19 | 20 | if (type === ActionTypes.RESET_ERROR_MESSAGE) { 21 | return null 22 | } else if (error) { 23 | return action.error 24 | } 25 | 26 | return state 27 | } 28 | 29 | // Updates the pagination data for different actions. 30 | const pagination = combineReducers({ 31 | starredByUser: paginate({ 32 | mapActionToKey: action => action.login, 33 | types: [ 34 | ActionTypes.STARRED_REQUEST, 35 | ActionTypes.STARRED_SUCCESS, 36 | ActionTypes.STARRED_FAILURE 37 | ] 38 | }), 39 | stargazersByRepo: paginate({ 40 | mapActionToKey: action => action.fullName, 41 | types: [ 42 | ActionTypes.STARGAZERS_REQUEST, 43 | ActionTypes.STARGAZERS_SUCCESS, 44 | ActionTypes.STARGAZERS_FAILURE 45 | ] 46 | }) 47 | }) 48 | 49 | const rootReducer = combineReducers({ 50 | entities, 51 | pagination, 52 | errorMessage, 53 | routing 54 | }) 55 | 56 | export default rootReducer 57 | -------------------------------------------------------------------------------- /examples/real-world/reducers/paginate.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge' 2 | import union from 'lodash/union' 3 | 4 | // Creates a reducer managing pagination, given the action types to handle, 5 | // and a function telling how to extract the key from an action. 6 | export default function paginate({ types, mapActionToKey }) { 7 | if (!Array.isArray(types) || types.length !== 3) { 8 | throw new Error('Expected types to be an array of three elements.') 9 | } 10 | if (!types.every(t => typeof t === 'string')) { 11 | throw new Error('Expected types to be strings.') 12 | } 13 | if (typeof mapActionToKey !== 'function') { 14 | throw new Error('Expected mapActionToKey to be a function.') 15 | } 16 | 17 | const [ requestType, successType, failureType ] = types 18 | 19 | function updatePagination(state = { 20 | isFetching: false, 21 | nextPageUrl: undefined, 22 | pageCount: 0, 23 | ids: [] 24 | }, action) { 25 | switch (action.type) { 26 | case requestType: 27 | return merge({}, state, { 28 | isFetching: true 29 | }) 30 | case successType: 31 | return merge({}, state, { 32 | isFetching: false, 33 | ids: union(state.ids, action.response.result), 34 | nextPageUrl: action.response.nextPageUrl, 35 | pageCount: state.pageCount + 1 36 | }) 37 | case failureType: 38 | return merge({}, state, { 39 | isFetching: false 40 | }) 41 | default: 42 | return state 43 | } 44 | } 45 | 46 | return function updatePaginationByKey(state = {}, action) { 47 | switch (action.type) { 48 | case requestType: 49 | case successType: 50 | case failureType: 51 | const key = mapActionToKey(action) 52 | if (typeof key !== 'string') { 53 | throw new Error('Expected key to be a string.') 54 | } 55 | return merge({}, state, { 56 | [key]: updatePagination(state[key], action) 57 | }) 58 | default: 59 | return state 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/real-world/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route } from 'react-router' 3 | import App from './containers/App' 4 | import UserPage from './containers/UserPage' 5 | import RepoPage from './containers/RepoPage' 6 | 7 | export default ( 8 | 9 | 11 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /examples/real-world/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.use(function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /examples/real-world/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import createLogger from 'redux-logger' 4 | import api from '../middleware/api' 5 | import rootReducer from '../reducers' 6 | import DevTools from '../containers/DevTools' 7 | 8 | export default function configureStore(initialState) { 9 | const store = createStore( 10 | rootReducer, 11 | initialState, 12 | compose( 13 | applyMiddleware(thunk, api, createLogger()), 14 | DevTools.instrument() 15 | ) 16 | ) 17 | 18 | if (module.hot) { 19 | // Enable Webpack hot module replacement for reducers 20 | module.hot.accept('../reducers', () => { 21 | const nextRootReducer = require('../reducers').default 22 | store.replaceReducer(nextRootReducer) 23 | }) 24 | } 25 | 26 | return store 27 | } 28 | -------------------------------------------------------------------------------- /examples/real-world/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.prod') 3 | } else { 4 | module.exports = require('./configureStore.dev') 5 | } 6 | -------------------------------------------------------------------------------- /examples/real-world/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import api from '../middleware/api' 4 | import rootReducer from '../reducers' 5 | 6 | export default function configureStore(initialState) { 7 | return createStore( 8 | rootReducer, 9 | initialState, 10 | applyMiddleware(thunk, api) 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /examples/real-world/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | loaders: [ 'babel' ], 25 | exclude: /node_modules/, 26 | include: __dirname 27 | } 28 | ] 29 | } 30 | } 31 | 32 | 33 | // When inside Redux repo, prefer src to compiled version. 34 | // You can safely delete these lines in your project. 35 | var reduxSrc = path.join(__dirname, '..', '..', 'src') 36 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules') 37 | var fs = require('fs') 38 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) { 39 | // Resolve Redux to source 40 | module.exports.resolve = { alias: { 'redux': reduxSrc } } 41 | // Our root .babelrc needs this flag for CommonJS output 42 | process.env.BABEL_ENV = 'commonjs' 43 | // Compile Redux from source 44 | module.exports.module.loaders.push({ 45 | test: /\.js$/, 46 | loaders: [ 'babel' ], 47 | include: reduxSrc 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /examples/shopping-cart/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/shopping-cart/actions/index.js: -------------------------------------------------------------------------------- 1 | import shop from '../api/shop' 2 | import * as types from '../constants/ActionTypes' 3 | 4 | function receiveProducts(products) { 5 | return { 6 | type: types.RECEIVE_PRODUCTS, 7 | products: products 8 | } 9 | } 10 | 11 | export function getAllProducts() { 12 | return dispatch => { 13 | shop.getProducts(products => { 14 | dispatch(receiveProducts(products)) 15 | }) 16 | } 17 | } 18 | 19 | function addToCartUnsafe(productId) { 20 | return { 21 | type: types.ADD_TO_CART, 22 | productId 23 | } 24 | } 25 | 26 | export function addToCart(productId) { 27 | return (dispatch, getState) => { 28 | if (getState().products.byId[productId].inventory > 0) { 29 | dispatch(addToCartUnsafe(productId)) 30 | } 31 | } 32 | } 33 | 34 | export function checkout(products) { 35 | return (dispatch, getState) => { 36 | const cart = getState().cart 37 | 38 | dispatch({ 39 | type: types.CHECKOUT_REQUEST 40 | }) 41 | shop.buyProducts(products, () => { 42 | dispatch({ 43 | type: types.CHECKOUT_SUCCESS, 44 | cart 45 | }) 46 | // Replace the line above with line below to rollback on failure: 47 | // dispatch({ type: types.CHECKOUT_FAILURE, cart }) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/shopping-cart/api/products.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2}, 3 | {"id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10}, 4 | {"id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5} 5 | ] 6 | -------------------------------------------------------------------------------- /examples/shopping-cart/api/shop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocking client-server processing 3 | */ 4 | import _products from './products.json' 5 | 6 | const TIMEOUT = 100 7 | 8 | export default { 9 | getProducts(cb, timeout) { 10 | setTimeout(() => cb(_products), timeout || TIMEOUT) 11 | }, 12 | 13 | buyProducts(payload, cb, timeout) { 14 | setTimeout(() => cb(), timeout || TIMEOUT) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/shopping-cart/components/Cart.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import Product from './Product' 3 | 4 | export default class Cart extends Component { 5 | render() { 6 | const { products, total, onCheckoutClicked } = this.props 7 | 8 | const hasProducts = products.length > 0 9 | const nodes = !hasProducts ? 10 | Please add some products to cart. : 11 | products.map(product => 12 | 17 | ) 18 | 19 | return ( 20 |
21 |

Your Cart

22 |
{nodes}
23 |

Total: ${total}

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

{this.props.title}

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

Shopping Cart Example

10 |
11 | 12 |
13 | 14 |
15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/shopping-cart/containers/CartContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { checkout } from '../actions' 4 | import { getTotal, getCartProducts } from '../reducers' 5 | import Cart from '../components/Cart' 6 | 7 | class CartContainer extends Component { 8 | render() { 9 | const { products, total } = this.props 10 | 11 | return ( 12 | this.props.checkout()} /> 16 | ) 17 | } 18 | } 19 | 20 | CartContainer.propTypes = { 21 | products: PropTypes.arrayOf(PropTypes.shape({ 22 | id: PropTypes.number.isRequired, 23 | title: PropTypes.string.isRequired, 24 | price: PropTypes.number.isRequired, 25 | quantity: PropTypes.number.isRequired 26 | })).isRequired, 27 | total: PropTypes.string, 28 | checkout: PropTypes.func.isRequired 29 | } 30 | 31 | const mapStateToProps = (state) => { 32 | return { 33 | products: getCartProducts(state), 34 | total: getTotal(state) 35 | } 36 | } 37 | 38 | export default connect( 39 | mapStateToProps, 40 | { checkout } 41 | )(CartContainer) 42 | -------------------------------------------------------------------------------- /examples/shopping-cart/containers/ProductsContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { addToCart } from '../actions' 4 | import { getVisibleProducts } from '../reducers/products' 5 | import ProductItem from '../components/ProductItem' 6 | import ProductsList from '../components/ProductsList' 7 | 8 | class ProductsContainer extends Component { 9 | render() { 10 | const { products } = this.props 11 | return ( 12 | 13 | {products.map(product => 14 | this.props.addToCart(product.id)} /> 18 | )} 19 | 20 | ) 21 | } 22 | } 23 | 24 | ProductsContainer.propTypes = { 25 | products: PropTypes.arrayOf(PropTypes.shape({ 26 | id: PropTypes.number.isRequired, 27 | title: PropTypes.string.isRequired, 28 | price: PropTypes.number.isRequired, 29 | inventory: PropTypes.number.isRequired 30 | })).isRequired, 31 | addToCart: PropTypes.func.isRequired 32 | } 33 | 34 | function mapStateToProps(state) { 35 | return { 36 | products: getVisibleProducts(state.products) 37 | } 38 | } 39 | 40 | export default connect( 41 | mapStateToProps, 42 | { addToCart } 43 | )(ProductsContainer) 44 | -------------------------------------------------------------------------------- /examples/shopping-cart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux shopping cart example 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/shopping-cart/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { createStore, applyMiddleware } from 'redux' 5 | import { Provider } from 'react-redux' 6 | import logger from 'redux-logger' 7 | import thunk from 'redux-thunk' 8 | import reducer from './reducers' 9 | import { getAllProducts } from './actions' 10 | import App from './containers/App' 11 | 12 | const middleware = process.env.NODE_ENV === 'production' ? 13 | [ thunk ] : 14 | [ thunk, logger() ] 15 | 16 | const store = createStore( 17 | reducer, 18 | applyMiddleware(...middleware) 19 | ) 20 | 21 | store.dispatch(getAllProducts()) 22 | 23 | render( 24 | 25 | 26 | , 27 | document.getElementById('root') 28 | ) 29 | -------------------------------------------------------------------------------- /examples/shopping-cart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-shopping-cart-example", 3 | "version": "0.0.0", 4 | "description": "Redux shopping-cart example", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/reactjs/redux.git" 11 | }, 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/reactjs/redux/issues" 15 | }, 16 | "homepage": "http://redux.js.org", 17 | "dependencies": { 18 | "babel-polyfill": "^6.3.14", 19 | "react": "^0.14.7", 20 | "react-dom": "^0.14.7", 21 | "react-redux": "^4.2.1", 22 | "redux": "^3.2.1", 23 | "redux-thunk": "^1.0.3" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.3.15", 27 | "babel-loader": "^6.2.0", 28 | "babel-preset-es2015": "^6.3.13", 29 | "babel-preset-react": "^6.3.13", 30 | "babel-preset-react-hmre": "^1.0.1", 31 | "express": "^4.13.3", 32 | "json-loader": "^0.5.3", 33 | "redux-logger": "^2.0.1", 34 | "webpack": "^1.9.11", 35 | "webpack-dev-middleware": "^1.2.0", 36 | "webpack-hot-middleware": "^2.2.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/shopping-cart/reducers/cart.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_TO_CART, 3 | CHECKOUT_REQUEST, 4 | CHECKOUT_FAILURE 5 | } from '../constants/ActionTypes' 6 | 7 | const initialState = { 8 | addedIds: [], 9 | quantityById: {} 10 | } 11 | 12 | function addedIds(state = initialState.addedIds, action) { 13 | switch (action.type) { 14 | case ADD_TO_CART: 15 | if (state.indexOf(action.productId) !== -1) { 16 | return state 17 | } 18 | return [ ...state, action.productId ] 19 | default: 20 | return state 21 | } 22 | } 23 | 24 | function quantityById(state = initialState.quantityById, action) { 25 | switch (action.type) { 26 | case ADD_TO_CART: 27 | const { productId } = action 28 | return Object.assign({}, state, { 29 | [productId]: (state[productId] || 0) + 1 30 | }) 31 | default: 32 | return state 33 | } 34 | } 35 | 36 | export default function cart(state = initialState, action) { 37 | switch (action.type) { 38 | case CHECKOUT_REQUEST: 39 | return initialState 40 | case CHECKOUT_FAILURE: 41 | return action.cart 42 | default: 43 | return { 44 | addedIds: addedIds(state.addedIds, action), 45 | quantityById: quantityById(state.quantityById, action) 46 | } 47 | } 48 | } 49 | 50 | export function getQuantity(state, productId) { 51 | return state.quantityById[productId] || 0 52 | } 53 | 54 | export function getAddedIds(state) { 55 | return state.addedIds 56 | } 57 | -------------------------------------------------------------------------------- /examples/shopping-cart/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { default as cart, getQuantity, getAddedIds } from './cart' 3 | import { default as products, getProduct } from './products' 4 | 5 | export function getTotal(state) { 6 | return getAddedIds(state.cart).reduce((total, id) => 7 | total + getProduct(state.products, id).price * getQuantity(state.cart, id), 8 | 0 9 | ).toFixed(2) 10 | } 11 | 12 | export function getCartProducts(state) { 13 | return getAddedIds(state.cart).map(id => Object.assign( 14 | {}, 15 | getProduct(state.products, id), 16 | { 17 | quantity: getQuantity(state.cart, id) 18 | } 19 | )) 20 | } 21 | 22 | export default combineReducers({ 23 | cart, 24 | products 25 | }) 26 | -------------------------------------------------------------------------------- /examples/shopping-cart/reducers/products.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { RECEIVE_PRODUCTS, ADD_TO_CART } from '../constants/ActionTypes' 3 | 4 | function products(state, action) { 5 | switch (action.type) { 6 | case ADD_TO_CART: 7 | return Object.assign({}, state, { 8 | inventory: state.inventory - 1 9 | }) 10 | default: 11 | return state 12 | } 13 | } 14 | 15 | function byId(state = {}, action) { 16 | switch (action.type) { 17 | case RECEIVE_PRODUCTS: 18 | return Object.assign({}, 19 | state, 20 | action.products.reduce((obj, product) => { 21 | obj[product.id] = product 22 | return obj 23 | }, {}) 24 | ) 25 | default: 26 | const { productId } = action 27 | if (productId) { 28 | return Object.assign({}, state, { 29 | [productId]: products(state[productId], action) 30 | }) 31 | } 32 | return state 33 | } 34 | } 35 | 36 | function visibleIds(state = [], action) { 37 | switch (action.type) { 38 | case RECEIVE_PRODUCTS: 39 | return action.products.map(product => product.id) 40 | default: 41 | return state 42 | } 43 | } 44 | 45 | export default combineReducers({ 46 | byId, 47 | visibleIds 48 | }) 49 | 50 | export function getProduct(state, id) { 51 | return state.byId[id] 52 | } 53 | 54 | export function getVisibleProducts(state) { 55 | return state.visibleIds.map(id => getProduct(state, id)) 56 | } 57 | -------------------------------------------------------------------------------- /examples/shopping-cart/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /examples/shopping-cart/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | loaders: [ 'babel' ], 25 | exclude: /node_modules/, 26 | include: __dirname 27 | }, 28 | { 29 | test: /\.json$/, 30 | loaders: [ 'json' ], 31 | exclude: /node_modules/, 32 | include: __dirname 33 | } 34 | ] 35 | } 36 | } 37 | 38 | 39 | // When inside Redux repo, prefer src to compiled version. 40 | // You can safely delete these lines in your project. 41 | var reduxSrc = path.join(__dirname, '..', '..', 'src') 42 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules') 43 | var fs = require('fs') 44 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) { 45 | // Resolve Redux to source 46 | module.exports.resolve = { alias: { 'redux': reduxSrc } } 47 | // Our root .babelrc needs this flag for CommonJS output 48 | process.env.BABEL_ENV = 'commonjs' 49 | // Compile Redux from source 50 | module.exports.module.loaders.push({ 51 | test: /\.js$/, 52 | loaders: [ 'babel' ], 53 | include: reduxSrc 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /examples/testAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs an ordered set of commands within each of the build directories. 3 | */ 4 | 5 | import fs from 'fs' 6 | import path from 'path' 7 | import { spawnSync } from 'child_process' 8 | 9 | var exampleDirs = fs.readdirSync(__dirname).filter((file) => { 10 | return fs.statSync(path.join(__dirname, file)).isDirectory() 11 | }) 12 | 13 | // Ordering is important here. `npm install` must come first. 14 | var cmdArgs = [ 15 | { cmd: 'npm', args: [ 'install' ] }, 16 | { cmd: 'npm', args: [ 'test' ] } 17 | ] 18 | 19 | for (const dir of exampleDirs) { 20 | for (const cmdArg of cmdArgs) { 21 | // declare opts in this scope to avoid https://github.com/joyent/node/issues/9158 22 | const opts = { 23 | cwd: path.join(__dirname, dir), 24 | stdio: 'inherit' 25 | } 26 | 27 | let result = {} 28 | if (process.platform === 'win32') { 29 | result = spawnSync(cmdArg.cmd + '.cmd', cmdArg.args, opts) 30 | } else { 31 | result = spawnSync(cmdArg.cmd, cmdArg.args, opts) 32 | } 33 | if (result.status !== 0) { 34 | throw new Error('Building examples exited with non-zero') 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/todomvc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/todomvc/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | 3 | export function addTodo(text) { 4 | return { type: types.ADD_TODO, text } 5 | } 6 | 7 | export function deleteTodo(id) { 8 | return { type: types.DELETE_TODO, id } 9 | } 10 | 11 | export function editTodo(id, text) { 12 | return { type: types.EDIT_TODO, id, text } 13 | } 14 | 15 | export function completeTodo(id) { 16 | return { type: types.COMPLETE_TODO, id } 17 | } 18 | 19 | export function completeAll() { 20 | return { type: types.COMPLETE_ALL } 21 | } 22 | 23 | export function clearCompleted() { 24 | return { type: types.CLEAR_COMPLETED } 25 | } 26 | -------------------------------------------------------------------------------- /examples/todomvc/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import classnames from 'classnames' 3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 4 | 5 | const FILTER_TITLES = { 6 | [SHOW_ALL]: 'All', 7 | [SHOW_ACTIVE]: 'Active', 8 | [SHOW_COMPLETED]: 'Completed' 9 | } 10 | 11 | class Footer extends Component { 12 | renderTodoCount() { 13 | const { activeCount } = this.props 14 | const itemWord = activeCount === 1 ? 'item' : 'items' 15 | 16 | return ( 17 | 18 | {activeCount || 'No'} {itemWord} left 19 | 20 | ) 21 | } 22 | 23 | renderFilterLink(filter) { 24 | const title = FILTER_TITLES[filter] 25 | const { filter: selectedFilter, onShow } = this.props 26 | 27 | return ( 28 | onShow(filter)}> 31 | {title} 32 | 33 | ) 34 | } 35 | 36 | renderClearButton() { 37 | const { completedCount, onClearCompleted } = this.props 38 | if (completedCount > 0) { 39 | return ( 40 | 44 | ) 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 |
51 | {this.renderTodoCount()} 52 |
    53 | {[ SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED ].map(filter => 54 |
  • 55 | {this.renderFilterLink(filter)} 56 |
  • 57 | )} 58 |
59 | {this.renderClearButton()} 60 |
61 | ) 62 | } 63 | } 64 | 65 | Footer.propTypes = { 66 | completedCount: PropTypes.number.isRequired, 67 | activeCount: PropTypes.number.isRequired, 68 | filter: PropTypes.string.isRequired, 69 | onClearCompleted: PropTypes.func.isRequired, 70 | onShow: PropTypes.func.isRequired 71 | } 72 | 73 | export default Footer 74 | -------------------------------------------------------------------------------- /examples/todomvc/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import TodoTextInput from './TodoTextInput' 3 | 4 | class Header extends Component { 5 | handleSave(text) { 6 | if (text.length !== 0) { 7 | this.props.addTodo(text) 8 | } 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |

todos

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