├── .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 | onChange(e.target.value)}
11 | value={value}>
12 | {options.map(option =>
13 |
14 | {option}
15 | )
16 | }
17 |
18 |
19 | )
20 | }
21 | }
22 |
23 | Picker.propTypes = {
24 | options: PropTypes.arrayOf(
25 | PropTypes.string.isRequired
26 | ).isRequired,
27 | value: PropTypes.string.isRequired,
28 | onChange: PropTypes.func.isRequired
29 | }
30 |
--------------------------------------------------------------------------------
/examples/async/components/Posts.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 |
3 | export default class Posts extends Component {
4 | render() {
5 | return (
6 |
7 | {this.props.posts.map((post, i) =>
8 | {post.title}
9 | )}
10 |
11 | )
12 | }
13 | }
14 |
15 | Posts.propTypes = {
16 | posts: PropTypes.array.isRequired
17 | }
18 |
--------------------------------------------------------------------------------
/examples/async/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { connect } from 'react-redux'
3 | import { selectReddit, fetchPostsIfNeeded, invalidateReddit } from '../actions'
4 | import Picker from '../components/Picker'
5 | import Posts from '../components/Posts'
6 |
7 | class App extends Component {
8 | constructor(props) {
9 | super(props)
10 | this.handleChange = this.handleChange.bind(this)
11 | this.handleRefreshClick = this.handleRefreshClick.bind(this)
12 | }
13 |
14 | componentDidMount() {
15 | const { dispatch, selectedReddit } = this.props
16 | dispatch(fetchPostsIfNeeded(selectedReddit))
17 | }
18 |
19 | componentWillReceiveProps(nextProps) {
20 | if (nextProps.selectedReddit !== this.props.selectedReddit) {
21 | const { dispatch, selectedReddit } = nextProps
22 | dispatch(fetchPostsIfNeeded(selectedReddit))
23 | }
24 | }
25 |
26 | handleChange(nextReddit) {
27 | this.props.dispatch(selectReddit(nextReddit))
28 | }
29 |
30 | handleRefreshClick(e) {
31 | e.preventDefault()
32 |
33 | const { dispatch, selectedReddit } = this.props
34 | dispatch(invalidateReddit(selectedReddit))
35 | dispatch(fetchPostsIfNeeded(selectedReddit))
36 | }
37 |
38 | render() {
39 | const { selectedReddit, posts, isFetching, lastUpdated } = this.props
40 | const isEmpty = posts.length === 0
41 | return (
42 |
43 |
46 |
47 | {lastUpdated &&
48 |
49 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
50 | {' '}
51 |
52 | }
53 | {!isFetching &&
54 |
56 | Refresh
57 |
58 | }
59 |
60 | {isEmpty
61 | ? (isFetching ?
Loading... :
Empty. )
62 | :
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 | Increment if odd
14 | Increment async
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 |
27 | +
28 |
29 | {' '}
30 |
31 | -
32 |
33 | {' '}
34 |
35 | Increment if odd
36 |
37 | {' '}
38 |
39 | Increment async
40 |
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 |
48 | Go!
49 |
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 |
10 | {isFetching ? 'Loading...' : 'Load More'}
11 |
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 |
26 | Checkout
27 |
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 |
0 ? '' : 'disabled'}>
17 | {product.inventory > 0 ? 'Add to cart' : 'Sold Out'}
18 |
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 |
42 | Clear completed
43 |
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 |
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 |
50 | )
51 | }
52 | }
53 |
54 | render() {
55 | const { todos, actions } = this.props
56 | const { filter } = this.state
57 |
58 | const filteredTodos = todos.filter(TODO_FILTERS[filter])
59 | const completedCount = todos.reduce((count, todo) =>
60 | todo.completed ? count + 1 : count,
61 | 0
62 | )
63 |
64 | return (
65 |
66 | {this.renderToggleAll(completedCount)}
67 |
68 | {filteredTodos.map(todo =>
69 |
70 | )}
71 |
72 | {this.renderFooter(completedCount)}
73 |
74 | )
75 | }
76 | }
77 |
78 | MainSection.propTypes = {
79 | todos: PropTypes.array.isRequired,
80 | actions: PropTypes.object.isRequired
81 | }
82 |
83 | export default MainSection
84 |
--------------------------------------------------------------------------------
/examples/todomvc/components/TodoItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 | import TodoTextInput from './TodoTextInput'
4 |
5 | class TodoItem extends Component {
6 | constructor(props, context) {
7 | super(props, context)
8 | this.state = {
9 | editing: false
10 | }
11 | }
12 |
13 | handleDoubleClick() {
14 | this.setState({ editing: true })
15 | }
16 |
17 | handleSave(id, text) {
18 | if (text.length === 0) {
19 | this.props.deleteTodo(id)
20 | } else {
21 | this.props.editTodo(id, text)
22 | }
23 | this.setState({ editing: false })
24 | }
25 |
26 | render() {
27 | const { todo, completeTodo, deleteTodo } = this.props
28 |
29 | let element
30 | if (this.state.editing) {
31 | element = (
32 | this.handleSave(todo.id, text)} />
35 | )
36 | } else {
37 | element = (
38 |
39 | completeTodo(todo.id)} />
43 |
44 | {todo.text}
45 |
46 | deleteTodo(todo.id)} />
48 |
49 | )
50 | }
51 |
52 | return (
53 |
57 | {element}
58 |
59 | )
60 | }
61 | }
62 |
63 | TodoItem.propTypes = {
64 | todo: PropTypes.object.isRequired,
65 | editTodo: PropTypes.func.isRequired,
66 | deleteTodo: PropTypes.func.isRequired,
67 | completeTodo: PropTypes.func.isRequired
68 | }
69 |
70 | export default TodoItem
71 |
--------------------------------------------------------------------------------
/examples/todomvc/components/TodoTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import classnames from 'classnames'
3 |
4 | class TodoTextInput extends Component {
5 | constructor(props, context) {
6 | super(props, context)
7 | this.state = {
8 | text: this.props.text || ''
9 | }
10 | }
11 |
12 | handleSubmit(e) {
13 | const text = e.target.value.trim()
14 | if (e.which === 13) {
15 | this.props.onSave(text)
16 | if (this.props.newTodo) {
17 | this.setState({ text: '' })
18 | }
19 | }
20 | }
21 |
22 | handleChange(e) {
23 | this.setState({ text: e.target.value })
24 | }
25 |
26 | handleBlur(e) {
27 | if (!this.props.newTodo) {
28 | this.props.onSave(e.target.value)
29 | }
30 | }
31 |
32 | render() {
33 | return (
34 |
46 | )
47 | }
48 | }
49 |
50 | TodoTextInput.propTypes = {
51 | onSave: PropTypes.func.isRequired,
52 | text: PropTypes.string,
53 | placeholder: PropTypes.string,
54 | editing: PropTypes.bool,
55 | newTodo: PropTypes.bool
56 | }
57 |
58 | export default TodoTextInput
59 |
--------------------------------------------------------------------------------
/examples/todomvc/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO'
2 | export const DELETE_TODO = 'DELETE_TODO'
3 | export const EDIT_TODO = 'EDIT_TODO'
4 | export const COMPLETE_TODO = 'COMPLETE_TODO'
5 | export const COMPLETE_ALL = 'COMPLETE_ALL'
6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'
7 |
--------------------------------------------------------------------------------
/examples/todomvc/constants/TodoFilters.js:
--------------------------------------------------------------------------------
1 | export const SHOW_ALL = 'show_all'
2 | export const SHOW_COMPLETED = 'show_completed'
3 | export const SHOW_ACTIVE = 'show_active'
4 |
--------------------------------------------------------------------------------
/examples/todomvc/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { bindActionCreators } from 'redux'
3 | import { connect } from 'react-redux'
4 | import Header from '../components/Header'
5 | import MainSection from '../components/MainSection'
6 | import * as TodoActions from '../actions'
7 |
8 | class App extends Component {
9 | render() {
10 | const { todos, actions } = this.props
11 | return (
12 |
13 |
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | App.propTypes = {
21 | todos: PropTypes.array.isRequired,
22 | actions: PropTypes.object.isRequired
23 | }
24 |
25 | function mapStateToProps(state) {
26 | return {
27 | todos: state.todos
28 | }
29 | }
30 |
31 | function mapDispatchToProps(dispatch) {
32 | return {
33 | actions: bindActionCreators(TodoActions, dispatch)
34 | }
35 | }
36 |
37 | export default connect(
38 | mapStateToProps,
39 | mapDispatchToProps
40 | )(App)
41 |
--------------------------------------------------------------------------------
/examples/todomvc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux TodoMVC example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/todomvc/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 | import 'todomvc-app-css/index.css'
8 |
9 | const store = configureStore()
10 |
11 | render(
12 |
13 |
14 | ,
15 | document.getElementById('root')
16 | )
17 |
--------------------------------------------------------------------------------
/examples/todomvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-todomvc-example",
3 | "version": "0.0.0",
4 | "description": "Redux TodoMVC example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "test": "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 | "babel-polyfill": "^6.3.14",
21 | "classnames": "^2.1.2",
22 | "react": "^0.14.7",
23 | "react-dom": "^0.14.7",
24 | "react-redux": "^4.2.1",
25 | "redux": "^3.2.1"
26 | },
27 | "devDependencies": {
28 | "babel-core": "^6.3.15",
29 | "babel-loader": "^6.2.0",
30 | "babel-preset-es2015": "^6.3.13",
31 | "babel-preset-react": "^6.3.13",
32 | "babel-preset-react-hmre": "^1.0.1",
33 | "babel-register": "^6.3.13",
34 | "cross-env": "^1.0.7",
35 | "expect": "^1.8.0",
36 | "express": "^4.13.3",
37 | "jsdom": "^5.6.1",
38 | "mocha": "^2.2.5",
39 | "node-libs-browser": "^0.5.2",
40 | "raw-loader": "^0.5.1",
41 | "react-addons-test-utils": "^0.14.7",
42 | "style-loader": "^0.12.3",
43 | "todomvc-app-css": "^2.0.1",
44 | "webpack": "^1.9.11",
45 | "webpack-dev-middleware": "^1.2.0",
46 | "webpack-hot-middleware": "^2.2.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/examples/todomvc/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 |
4 | const rootReducer = combineReducers({
5 | todos
6 | })
7 |
8 | export default rootReducer
9 |
--------------------------------------------------------------------------------
/examples/todomvc/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../constants/ActionTypes'
2 |
3 | const initialState = [
4 | {
5 | text: 'Use Redux',
6 | completed: false,
7 | id: 0
8 | }
9 | ]
10 |
11 | export default function todos(state = initialState, action) {
12 | switch (action.type) {
13 | case ADD_TODO:
14 | return [
15 | {
16 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
17 | completed: false,
18 | text: action.text
19 | },
20 | ...state
21 | ]
22 |
23 | case DELETE_TODO:
24 | return state.filter(todo =>
25 | todo.id !== action.id
26 | )
27 |
28 | case EDIT_TODO:
29 | return state.map(todo =>
30 | todo.id === action.id ?
31 | Object.assign({}, todo, { text: action.text }) :
32 | todo
33 | )
34 |
35 | case COMPLETE_TODO:
36 | return state.map(todo =>
37 | todo.id === action.id ?
38 | Object.assign({}, todo, { completed: !todo.completed }) :
39 | todo
40 | )
41 |
42 | case COMPLETE_ALL:
43 | const areAllMarked = state.every(todo => todo.completed)
44 | return state.map(todo => Object.assign({}, todo, {
45 | completed: !areAllMarked
46 | }))
47 |
48 | case CLEAR_COMPLETED:
49 | return state.filter(todo => todo.completed === false)
50 |
51 | default:
52 | return state
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/examples/todomvc/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var webpackDevMiddleware = require('webpack-dev-middleware')
3 | var webpackHotMiddleware = require('webpack-hot-middleware')
4 | var config = require('./webpack.config')
5 |
6 | var app = new (require('express'))()
7 | var port = 3000
8 |
9 | var compiler = webpack(config)
10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
11 | app.use(webpackHotMiddleware(compiler))
12 |
13 | app.get("/", function(req, res) {
14 | res.sendFile(__dirname + '/index.html')
15 | })
16 |
17 | app.listen(port, function(error) {
18 | if (error) {
19 | console.error(error)
20 | } else {
21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/examples/todomvc/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux'
2 | import rootReducer from '../reducers'
3 |
4 | export default function configureStore(initialState) {
5 | const store = createStore(rootReducer, initialState)
6 |
7 | if (module.hot) {
8 | // Enable Webpack hot module replacement for reducers
9 | module.hot.accept('../reducers', () => {
10 | const nextReducer = require('../reducers').default
11 | store.replaceReducer(nextReducer)
12 | })
13 | }
14 |
15 | return store
16 | }
17 |
--------------------------------------------------------------------------------
/examples/todomvc/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/examples/todomvc/test/actions/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import * as types from '../../constants/ActionTypes'
3 | import * as actions from '../../actions'
4 |
5 | describe('todo actions', () => {
6 | it('addTodo should create ADD_TODO action', () => {
7 | expect(actions.addTodo('Use Redux')).toEqual({
8 | type: types.ADD_TODO,
9 | text: 'Use Redux'
10 | })
11 | })
12 |
13 | it('deleteTodo should create DELETE_TODO action', () => {
14 | expect(actions.deleteTodo(1)).toEqual({
15 | type: types.DELETE_TODO,
16 | id: 1
17 | })
18 | })
19 |
20 | it('editTodo should create EDIT_TODO action', () => {
21 | expect(actions.editTodo(1, 'Use Redux everywhere')).toEqual({
22 | type: types.EDIT_TODO,
23 | id: 1,
24 | text: 'Use Redux everywhere'
25 | })
26 | })
27 |
28 | it('completeTodo should create COMPLETE_TODO action', () => {
29 | expect(actions.completeTodo(1)).toEqual({
30 | type: types.COMPLETE_TODO,
31 | id: 1
32 | })
33 | })
34 |
35 | it('completeAll should create COMPLETE_ALL action', () => {
36 | expect(actions.completeAll()).toEqual({
37 | type: types.COMPLETE_ALL
38 | })
39 | })
40 |
41 | it('clearCompleted should create CLEAR_COMPLETED action', () => {
42 | expect(actions.clearCompleted()).toEqual({
43 | type: types.CLEAR_COMPLETED
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/examples/todomvc/test/components/Footer.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import Footer from '../../components/Footer'
5 | import { SHOW_ALL, SHOW_ACTIVE } from '../../constants/TodoFilters'
6 |
7 | function setup(propOverrides) {
8 | const props = Object.assign({
9 | completedCount: 0,
10 | activeCount: 0,
11 | filter: SHOW_ALL,
12 | onClearCompleted: expect.createSpy(),
13 | onShow: expect.createSpy()
14 | }, propOverrides)
15 |
16 | const renderer = TestUtils.createRenderer()
17 | renderer.render()
18 | const output = renderer.getRenderOutput()
19 |
20 | return {
21 | props: props,
22 | output: output
23 | }
24 | }
25 |
26 | function getTextContent(elem) {
27 | const children = Array.isArray(elem.props.children) ?
28 | elem.props.children : [ elem.props.children ]
29 |
30 | return children.reduce(function concatText(out, child) {
31 | // Children are either elements or text strings
32 | return out + (child.props ? getTextContent(child) : child)
33 | }, '')
34 | }
35 |
36 | describe('components', () => {
37 | describe('Footer', () => {
38 | it('should render container', () => {
39 | const { output } = setup()
40 | expect(output.type).toBe('footer')
41 | expect(output.props.className).toBe('footer')
42 | })
43 |
44 | it('should display active count when 0', () => {
45 | const { output } = setup({ activeCount: 0 })
46 | const [ count ] = output.props.children
47 | expect(getTextContent(count)).toBe('No items left')
48 | })
49 |
50 | it('should display active count when above 0', () => {
51 | const { output } = setup({ activeCount: 1 })
52 | const [ count ] = output.props.children
53 | expect(getTextContent(count)).toBe('1 item left')
54 | })
55 |
56 | it('should render filters', () => {
57 | const { output } = setup()
58 | const [ , filters ] = output.props.children
59 | expect(filters.type).toBe('ul')
60 | expect(filters.props.className).toBe('filters')
61 | expect(filters.props.children.length).toBe(3)
62 | filters.props.children.forEach(function checkFilter(filter, i) {
63 | expect(filter.type).toBe('li')
64 | const a = filter.props.children
65 | expect(a.props.className).toBe(i === 0 ? 'selected' : '')
66 | expect(a.props.children).toBe({
67 | 0: 'All',
68 | 1: 'Active',
69 | 2: 'Completed'
70 | }[i])
71 | })
72 | })
73 |
74 | it('should call onShow when a filter is clicked', () => {
75 | const { output, props } = setup()
76 | const [ , filters ] = output.props.children
77 | const filterLink = filters.props.children[1].props.children
78 | filterLink.props.onClick({})
79 | expect(props.onShow).toHaveBeenCalledWith(SHOW_ACTIVE)
80 | })
81 |
82 | it('shouldnt show clear button when no completed todos', () => {
83 | const { output } = setup({ completedCount: 0 })
84 | const [ , , clear ] = output.props.children
85 | expect(clear).toBe(undefined)
86 | })
87 |
88 | it('should render clear button when completed todos', () => {
89 | const { output } = setup({ completedCount: 1 })
90 | const [ , , clear ] = output.props.children
91 | expect(clear.type).toBe('button')
92 | expect(clear.props.children).toBe('Clear completed')
93 | })
94 |
95 | it('should call onClearCompleted on clear button click', () => {
96 | const { output, props } = setup({ completedCount: 1 })
97 | const [ , , clear ] = output.props.children
98 | clear.props.onClick({})
99 | expect(props.onClearCompleted).toHaveBeenCalled()
100 | })
101 | })
102 | })
103 |
--------------------------------------------------------------------------------
/examples/todomvc/test/components/Header.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import Header from '../../components/Header'
5 | import TodoTextInput from '../../components/TodoTextInput'
6 |
7 | function setup() {
8 | const props = {
9 | addTodo: expect.createSpy()
10 | }
11 |
12 | const renderer = TestUtils.createRenderer()
13 | renderer.render()
14 | const output = renderer.getRenderOutput()
15 |
16 | return {
17 | props: props,
18 | output: output,
19 | renderer: renderer
20 | }
21 | }
22 |
23 | describe('components', () => {
24 | describe('Header', () => {
25 | it('should render correctly', () => {
26 | const { output } = setup()
27 |
28 | expect(output.type).toBe('header')
29 | expect(output.props.className).toBe('header')
30 |
31 | const [ h1, input ] = output.props.children
32 |
33 | expect(h1.type).toBe('h1')
34 | expect(h1.props.children).toBe('todos')
35 |
36 | expect(input.type).toBe(TodoTextInput)
37 | expect(input.props.newTodo).toBe(true)
38 | expect(input.props.placeholder).toBe('What needs to be done?')
39 | })
40 |
41 | it('should call addTodo if length of text is greater than 0', () => {
42 | const { output, props } = setup()
43 | const input = output.props.children[1]
44 | input.props.onSave('')
45 | expect(props.addTodo.calls.length).toBe(0)
46 | input.props.onSave('Use Redux')
47 | expect(props.addTodo.calls.length).toBe(1)
48 | })
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/examples/todomvc/test/components/TodoTextInput.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import React from 'react'
3 | import TestUtils from 'react-addons-test-utils'
4 | import TodoTextInput from '../../components/TodoTextInput'
5 |
6 | function setup(propOverrides) {
7 | const props = Object.assign({
8 | onSave: expect.createSpy(),
9 | text: 'Use Redux',
10 | placeholder: 'What needs to be done?',
11 | editing: false,
12 | newTodo: false
13 | }, propOverrides)
14 |
15 | const renderer = TestUtils.createRenderer()
16 |
17 | renderer.render(
18 |
19 | )
20 |
21 | let output = renderer.getRenderOutput()
22 |
23 | output = renderer.getRenderOutput()
24 |
25 | return {
26 | props: props,
27 | output: output,
28 | renderer: renderer
29 | }
30 | }
31 |
32 | describe('components', () => {
33 | describe('TodoTextInput', () => {
34 | it('should render correctly', () => {
35 | const { output } = setup()
36 | expect(output.props.placeholder).toEqual('What needs to be done?')
37 | expect(output.props.value).toEqual('Use Redux')
38 | expect(output.props.className).toEqual('')
39 | })
40 |
41 | it('should render correctly when editing=true', () => {
42 | const { output } = setup({ editing: true })
43 | expect(output.props.className).toEqual('edit')
44 | })
45 |
46 | it('should render correctly when newTodo=true', () => {
47 | const { output } = setup({ newTodo: true })
48 | expect(output.props.className).toEqual('new-todo')
49 | })
50 |
51 | it('should update value on change', () => {
52 | const { output, renderer } = setup()
53 | output.props.onChange({ target: { value: 'Use Radox' } })
54 | const updated = renderer.getRenderOutput()
55 | expect(updated.props.value).toEqual('Use Radox')
56 | })
57 |
58 | it('should call onSave on return key press', () => {
59 | const { output, props } = setup()
60 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } })
61 | expect(props.onSave).toHaveBeenCalledWith('Use Redux')
62 | })
63 |
64 | it('should reset state on return key press if newTodo', () => {
65 | const { output, renderer } = setup({ newTodo: true })
66 | output.props.onKeyDown({ which: 13, target: { value: 'Use Redux' } })
67 | const updated = renderer.getRenderOutput()
68 | expect(updated.props.value).toEqual('')
69 | })
70 |
71 | it('should call onSave on blur', () => {
72 | const { output, props } = setup()
73 | output.props.onBlur({ target: { value: 'Use Redux' } })
74 | expect(props.onSave).toHaveBeenCalledWith('Use Redux')
75 | })
76 |
77 | it('shouldnt call onSave on blur if newTodo', () => {
78 | const { output, props } = setup({ newTodo: true })
79 | output.props.onBlur({ target: { value: 'Use Redux' } })
80 | expect(props.onSave.calls.length).toBe(0)
81 | })
82 | })
83 | })
84 |
--------------------------------------------------------------------------------
/examples/todomvc/test/setup.js:
--------------------------------------------------------------------------------
1 | import { jsdom } from 'jsdom'
2 |
3 | global.document = jsdom('')
4 | global.window = document.defaultView
5 | global.navigator = global.window.navigator
6 |
--------------------------------------------------------------------------------
/examples/todomvc/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.NoErrorsPlugin()
19 | ],
20 | module: {
21 | loaders: [
22 | {
23 | test: /\.js$/,
24 | loaders: [ 'babel' ],
25 | exclude: /node_modules/,
26 | include: __dirname
27 | },
28 | {
29 | test: /\.css?$/,
30 | loaders: [ 'style', 'raw' ],
31 | include: __dirname
32 | }
33 | ]
34 | }
35 | }
36 |
37 |
38 | // When inside Redux repo, prefer src to compiled version.
39 | // You can safely delete these lines in your project.
40 | var reduxSrc = path.join(__dirname, '..', '..', 'src')
41 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules')
42 | var fs = require('fs')
43 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) {
44 | // Resolve Redux to source
45 | module.exports.resolve = { alias: { 'redux': reduxSrc } }
46 | // Our root .babelrc needs this flag for CommonJS output
47 | process.env.BABEL_ENV = 'commonjs'
48 | // Compile Redux from source
49 | module.exports.module.loaders.push({
50 | test: /\.js$/,
51 | loaders: [ 'babel' ],
52 | include: reduxSrc
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/actions/index.js:
--------------------------------------------------------------------------------
1 | let nextTodoId = 0
2 | export const addTodo = (text) => {
3 | return {
4 | type: 'ADD_TODO',
5 | id: nextTodoId++,
6 | text
7 | }
8 | }
9 |
10 | export const setVisibilityFilter = (filter) => {
11 | return {
12 | type: 'SET_VISIBILITY_FILTER',
13 | filter
14 | }
15 | }
16 |
17 | export const toggleTodo = (id) => {
18 | return {
19 | type: 'TOGGLE_TODO',
20 | id
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Footer from './Footer'
3 | import AddTodo from '../containers/AddTodo'
4 | import VisibleTodoList from '../containers/VisibleTodoList'
5 | import UndoRedo from '../containers/UndoRedo'
6 |
7 | const App = () => (
8 |
14 | )
15 |
16 | export default App
17 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FilterLink from '../containers/FilterLink'
3 |
4 | const Footer = () => (
5 |
6 | Show:
7 | {" "}
8 |
9 | All
10 |
11 | {", "}
12 |
13 | Active
14 |
15 | {", "}
16 |
17 | Completed
18 |
19 |
20 | )
21 |
22 | export default Footer
23 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/Link.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const Link = ({ active, children, onClick }) => {
4 | if (active) {
5 | return {children}
6 | }
7 |
8 | return (
9 | {
11 | e.preventDefault()
12 | onClick()
13 | }}
14 | >
15 | {children}
16 |
17 | )
18 | }
19 |
20 | Link.propTypes = {
21 | active: PropTypes.bool.isRequired,
22 | children: PropTypes.node.isRequired,
23 | onClick: PropTypes.func.isRequired
24 | }
25 |
26 | export default Link
27 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const Todo = ({ onClick, completed, text }) => (
4 |
10 | {text}
11 |
12 | )
13 |
14 | Todo.propTypes = {
15 | onClick: PropTypes.func.isRequired,
16 | completed: PropTypes.bool.isRequired,
17 | text: PropTypes.string.isRequired
18 | }
19 |
20 | export default Todo
21 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/components/TodoList.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import Todo from './Todo'
3 |
4 | const TodoList = ({ todos, onTodoClick }) => (
5 |
6 | {todos.map(todo =>
7 | onTodoClick(todo.id)}
11 | />
12 | )}
13 |
14 | )
15 |
16 | TodoList.propTypes = {
17 | todos: PropTypes.arrayOf(PropTypes.shape({
18 | id: PropTypes.number.isRequired,
19 | completed: PropTypes.bool.isRequired,
20 | text: PropTypes.string.isRequired
21 | }).isRequired).isRequired,
22 | onTodoClick: PropTypes.func.isRequired
23 | }
24 |
25 | export default TodoList
26 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/containers/AddTodo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { addTodo } from '../actions'
4 |
5 | let AddTodo = ({ dispatch }) => {
6 | let input
7 |
8 | return (
9 |
10 |
25 |
26 | )
27 | }
28 | AddTodo = connect()(AddTodo)
29 |
30 | export default AddTodo
31 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/containers/FilterLink.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import { setVisibilityFilter } from '../actions'
3 | import Link from '../components/Link'
4 |
5 | const mapStateToProps = (state, ownProps) => {
6 | return {
7 | active: ownProps.filter === state.visibilityFilter
8 | }
9 | }
10 |
11 | const mapDispatchToProps = (dispatch, ownProps) => {
12 | return {
13 | onClick: () => {
14 | dispatch(setVisibilityFilter(ownProps.filter))
15 | }
16 | }
17 | }
18 |
19 | const FilterLink = connect(
20 | mapStateToProps,
21 | mapDispatchToProps
22 | )(Link)
23 |
24 | export default FilterLink
25 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/containers/UndoRedo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ActionCreators as UndoActionCreators } from 'redux-undo'
3 | import { connect } from 'react-redux'
4 |
5 | let UndoRedo = ({ canUndo, canRedo, onUndo, onRedo }) => (
6 |
7 |
8 | Undo
9 |
10 |
11 | Redo
12 |
13 |
14 | )
15 |
16 | const mapStateToProps = (state) => {
17 | return {
18 | canUndo: state.todos.past.length > 0,
19 | canRedo: state.todos.future.length > 0
20 | }
21 | }
22 |
23 | const mapDispatchToProps = (dispatch) => {
24 | return {
25 | onUndo: () => dispatch(UndoActionCreators.undo()),
26 | onRedo: () => dispatch(UndoActionCreators.redo())
27 | }
28 | }
29 |
30 | UndoRedo = connect(
31 | mapStateToProps,
32 | mapDispatchToProps
33 | )(UndoRedo)
34 |
35 | export default UndoRedo
36 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/containers/VisibleTodoList.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import { toggleTodo } from '../actions'
3 | import TodoList from '../components/TodoList'
4 |
5 | const getVisibleTodos = (todos, filter) => {
6 | switch (filter) {
7 | case 'SHOW_ALL':
8 | return todos
9 | case 'SHOW_COMPLETED':
10 | return todos.filter(t => t.completed)
11 | case 'SHOW_ACTIVE':
12 | return todos.filter(t => !t.completed)
13 | }
14 | }
15 |
16 | const mapStateToProps = (state) => {
17 | return {
18 | todos: getVisibleTodos(state.todos.present, state.visibilityFilter)
19 | }
20 | }
21 |
22 | const mapDispatchToProps = (dispatch) => {
23 | return {
24 | onTodoClick: (id) => {
25 | dispatch(toggleTodo(id))
26 | }
27 | }
28 | }
29 |
30 | const VisibleTodoList = connect(
31 | mapStateToProps,
32 | mapDispatchToProps
33 | )(TodoList)
34 |
35 | export default VisibleTodoList
36 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux Todos with Undo example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import React from 'react'
3 | import { render } from 'react-dom'
4 | import { createStore } from 'redux'
5 | import { Provider } from 'react-redux'
6 | import App from './components/App'
7 | import todoApp from './reducers'
8 |
9 | const store = createStore(todoApp)
10 |
11 | const rootElement = document.getElementById('root')
12 | render(
13 |
14 |
15 | ,
16 | rootElement
17 | )
18 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-todos-with-undo-example",
3 | "version": "0.0.0",
4 | "description": "Redux todos with undo example",
5 | "scripts": {
6 | "start": "node server.js"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/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-undo": "^1.0.0-beta2"
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 | "expect": "^1.6.0",
32 | "express": "^4.13.3",
33 | "node-libs-browser": "^0.5.2",
34 | "webpack": "^1.9.11",
35 | "webpack-dev-middleware": "^1.2.0",
36 | "webpack-hot-middleware": "^2.2.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 | import visibilityFilter from './visibilityFilter'
4 |
5 | const todoApp = combineReducers({
6 | todos,
7 | visibilityFilter
8 | })
9 |
10 | export default todoApp
11 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import undoable, { distinctState } from 'redux-undo'
2 |
3 | const todo = (state, action) => {
4 | switch (action.type) {
5 | case 'ADD_TODO':
6 | return {
7 | id: action.id,
8 | text: action.text,
9 | completed: false
10 | }
11 | case 'TOGGLE_TODO':
12 | if (state.id !== action.id) {
13 | return state
14 | }
15 |
16 | return Object.assign({}, state, {
17 | completed: !state.completed
18 | })
19 | default:
20 | return state
21 | }
22 | }
23 |
24 | const todos = (state = [], action) => {
25 | switch (action.type) {
26 | case 'ADD_TODO':
27 | return [
28 | ...state,
29 | todo(undefined, action)
30 | ]
31 | case 'TOGGLE_TODO':
32 | return state.map(t =>
33 | todo(t, action)
34 | )
35 | default:
36 | return state
37 | }
38 | }
39 |
40 | const undoableTodos = undoable(todos, {
41 | filter: distinctState()
42 | })
43 |
44 | export default undoableTodos
45 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/reducers/visibilityFilter.js:
--------------------------------------------------------------------------------
1 | const visibilityFilter = (state = 'SHOW_ALL', action) => {
2 | switch (action.type) {
3 | case 'SET_VISIBILITY_FILTER':
4 | return action.filter
5 | default:
6 | return state
7 | }
8 | }
9 |
10 | export default visibilityFilter
11 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var webpackDevMiddleware = require('webpack-dev-middleware')
3 | var webpackHotMiddleware = require('webpack-hot-middleware')
4 | var config = require('./webpack.config')
5 |
6 | var app = new (require('express'))()
7 | var port = 3000
8 |
9 | var compiler = webpack(config)
10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }))
11 | app.use(webpackHotMiddleware(compiler))
12 |
13 | app.get("/", function(req, res) {
14 | res.sendFile(__dirname + '/index.html')
15 | })
16 |
17 | app.listen(port, function(error) {
18 | if (error) {
19 | console.error(error)
20 | } else {
21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/examples/todos-with-undo/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.NoErrorsPlugin()
19 | ],
20 | module: {
21 | loaders: [{
22 | test: /\.js$/,
23 | loaders: ['babel'],
24 | exclude: /node_modules/,
25 | include: __dirname
26 | }]
27 | }
28 | }
29 |
30 |
31 | // When inside Redux repo, prefer src to compiled version.
32 | // You can safely delete these lines in your project.
33 | var reduxSrc = path.join(__dirname, '..', '..', 'src')
34 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules')
35 | var fs = require('fs')
36 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) {
37 | // Resolve Redux to source
38 | module.exports.resolve = { alias: { 'redux': reduxSrc } }
39 | // Our root .babelrc needs this flag for CommonJS output
40 | process.env.BABEL_ENV = 'commonjs'
41 | // Compile Redux from source
42 | module.exports.module.loaders.push({
43 | test: /\.js$/,
44 | loaders: ['babel'],
45 | include: reduxSrc
46 | })
47 | }
48 |
--------------------------------------------------------------------------------
/examples/todos/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/todos/actions/index.js:
--------------------------------------------------------------------------------
1 | let nextTodoId = 0
2 | export const addTodo = (text) => {
3 | return {
4 | type: 'ADD_TODO',
5 | id: nextTodoId++,
6 | text
7 | }
8 | }
9 |
10 | export const setVisibilityFilter = (filter) => {
11 | return {
12 | type: 'SET_VISIBILITY_FILTER',
13 | filter
14 | }
15 | }
16 |
17 | export const toggleTodo = (id) => {
18 | return {
19 | type: 'TOGGLE_TODO',
20 | id
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/todos/components/AddTodoForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 |
4 | let AddTodoForm = ({ onSubmit }) => {
5 | let input
6 |
7 | return (
8 |
9 |
24 |
25 | )
26 | }
27 |
28 | export default AddTodoForm
29 |
--------------------------------------------------------------------------------
/examples/todos/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Footer from './Footer'
3 | import AddTodo from '../containers/AddTodo'
4 | import VisibleTodoList from '../containers/VisibleTodoList'
5 |
6 | const App = () => (
7 |
12 | )
13 |
14 | export default App
15 |
--------------------------------------------------------------------------------
/examples/todos/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FilterLink from '../containers/FilterLink'
3 |
4 | const Footer = () => (
5 |
6 | Show:
7 | {" "}
8 |
9 | All
10 |
11 | {", "}
12 |
13 | Active
14 |
15 | {", "}
16 |
17 | Completed
18 |
19 |
20 | )
21 |
22 | export default Footer
23 |
--------------------------------------------------------------------------------
/examples/todos/components/Link.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const Link = ({ active, children, onClick }) => {
4 | if (active) {
5 | return {children}
6 | }
7 |
8 | return (
9 | {
11 | e.preventDefault()
12 | onClick()
13 | }}
14 | >
15 | {children}
16 |
17 | )
18 | }
19 |
20 | Link.propTypes = {
21 | active: PropTypes.bool.isRequired,
22 | children: PropTypes.node.isRequired,
23 | onClick: PropTypes.func.isRequired
24 | }
25 |
26 | export default Link
27 |
--------------------------------------------------------------------------------
/examples/todos/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | const Todo = ({ onClick, completed, text }) => (
4 |
10 | {text}
11 |
12 | )
13 |
14 | Todo.propTypes = {
15 | onClick: PropTypes.func.isRequired,
16 | completed: PropTypes.bool.isRequired,
17 | text: PropTypes.string.isRequired
18 | }
19 |
20 | export default Todo
21 |
--------------------------------------------------------------------------------
/examples/todos/components/TodoList.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import Todo from './Todo'
3 |
4 | const TodoList = ({ todos, onTodoClick }) => (
5 |
6 | {todos.map(todo =>
7 | onTodoClick(todo.id)}
11 | />
12 | )}
13 |
14 | )
15 |
16 | TodoList.propTypes = {
17 | todos: PropTypes.arrayOf(PropTypes.shape({
18 | id: PropTypes.number.isRequired,
19 | completed: PropTypes.bool.isRequired,
20 | text: PropTypes.string.isRequired
21 | }).isRequired).isRequired,
22 | onTodoClick: PropTypes.func.isRequired
23 | }
24 |
25 | export default TodoList
26 |
--------------------------------------------------------------------------------
/examples/todos/containers/AddTodo.js:
--------------------------------------------------------------------------------
1 | import AddTodoForm from '../components/AddTodoForm'
2 |
3 | import { connect } from 'react-redux'
4 | import { addTodo } from '../actions'
5 |
6 |
7 | const mapDispatchToProps = (dispatch) => {
8 | return {
9 | onSubmit: (text) => {
10 | dispatch(addTodo(text))
11 | }
12 | }
13 | }
14 |
15 | let AddTodo = connect(null, mapDispatchToProps)(AddTodoForm)
16 |
17 | export default AddTodo
18 |
--------------------------------------------------------------------------------
/examples/todos/containers/FilterLink.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import { setVisibilityFilter } from '../actions'
3 | import Link from '../components/Link'
4 |
5 | const mapStateToProps = (state, ownProps) => {
6 | return {
7 | active: ownProps.filter === state.visibilityFilter
8 | }
9 | }
10 |
11 | const mapDispatchToProps = (dispatch, ownProps) => {
12 | return {
13 | onClick: () => {
14 | dispatch(setVisibilityFilter(ownProps.filter))
15 | }
16 | }
17 | }
18 |
19 | const FilterLink = connect(
20 | mapStateToProps,
21 | mapDispatchToProps
22 | )(Link)
23 |
24 | export default FilterLink
25 |
--------------------------------------------------------------------------------
/examples/todos/containers/VisibleTodoList.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import { toggleTodo } from '../actions'
3 | import TodoList from '../components/TodoList'
4 |
5 | const getVisibleTodos = (todos, filter) => {
6 | switch (filter) {
7 | case 'SHOW_ALL':
8 | return todos
9 | case 'SHOW_COMPLETED':
10 | return todos.filter(t => t.completed)
11 | case 'SHOW_ACTIVE':
12 | return todos.filter(t => !t.completed)
13 | }
14 | }
15 |
16 | const mapStateToProps = (state) => {
17 | return {
18 | todos: getVisibleTodos(state.todos, state.visibilityFilter)
19 | }
20 | }
21 |
22 | const mapDispatchToProps = (dispatch) => {
23 | return {
24 | onTodoClick: (id) => {
25 | dispatch(toggleTodo(id))
26 | }
27 | }
28 | }
29 |
30 | const VisibleTodoList = connect(
31 | mapStateToProps,
32 | mapDispatchToProps
33 | )(TodoList)
34 |
35 | export default VisibleTodoList
36 |
--------------------------------------------------------------------------------
/examples/todos/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux Todos example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/todos/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 { createStore } from 'redux'
6 | import todoApp from './reducers'
7 | import App from './components/App'
8 |
9 | let store = createStore(todoApp)
10 |
11 | render(
12 |
13 |
14 | ,
15 | document.getElementById('root')
16 | )
17 |
--------------------------------------------------------------------------------
/examples/todos/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-todos-example",
3 | "version": "0.0.0",
4 | "description": "Redux Todos 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 | "babel-polyfill": "^6.3.14",
21 | "react": "^0.14.7",
22 | "react-dom": "^0.14.7",
23 | "react-redux": "^4.1.2",
24 | "redux": "^3.1.2"
25 | },
26 | "devDependencies": {
27 | "babel-core": "^6.3.15",
28 | "babel-loader": "^6.2.0",
29 | "babel-preset-es2015": "^6.3.13",
30 | "babel-preset-react": "^6.3.13",
31 | "babel-preset-react-hmre": "^1.0.1",
32 | "babel-register": "^6.3.13",
33 | "cross-env": "^1.0.7",
34 | "expect": "^1.8.0",
35 | "express": "^4.13.3",
36 | "jsdom": "^5.6.1",
37 | "mocha": "^2.2.5",
38 | "node-libs-browser": "^0.5.2",
39 | "react-addons-test-utils": "^0.14.7",
40 | "webpack": "^1.9.11",
41 | "webpack-dev-middleware": "^1.2.0",
42 | "webpack-hot-middleware": "^2.2.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/examples/todos/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import todos from './todos'
3 | import visibilityFilter from './visibilityFilter'
4 |
5 | const todoApp = combineReducers({
6 | todos,
7 | visibilityFilter
8 | })
9 |
10 | export default todoApp
11 |
--------------------------------------------------------------------------------
/examples/todos/reducers/todos.js:
--------------------------------------------------------------------------------
1 | const todo = (state, action) => {
2 | switch (action.type) {
3 | case 'ADD_TODO':
4 | return {
5 | id: action.id,
6 | text: action.text,
7 | completed: false
8 | }
9 | case 'TOGGLE_TODO':
10 | if (state.id !== action.id) {
11 | return state
12 | }
13 |
14 | return Object.assign({}, state, {
15 | completed: !state.completed
16 | })
17 | default:
18 | return state
19 | }
20 | }
21 |
22 | const todos = (state = [], action) => {
23 | switch (action.type) {
24 | case 'ADD_TODO':
25 | return [
26 | ...state,
27 | todo(undefined, action)
28 | ]
29 | case 'TOGGLE_TODO':
30 | return state.map(t =>
31 | todo(t, action)
32 | )
33 | default:
34 | return state
35 | }
36 | }
37 |
38 | export default todos
39 |
--------------------------------------------------------------------------------
/examples/todos/reducers/visibilityFilter.js:
--------------------------------------------------------------------------------
1 | const visibilityFilter = (state = 'SHOW_ALL', action) => {
2 | switch (action.type) {
3 | case 'SET_VISIBILITY_FILTER':
4 | return action.filter
5 | default:
6 | return state
7 | }
8 | }
9 |
10 | export default visibilityFilter
11 |
--------------------------------------------------------------------------------
/examples/todos/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/todos/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/examples/todos/test/actions/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import * as actions from '../../actions'
3 |
4 | describe('todo actions', () => {
5 | it('addTodo should create ADD_TODO action', () => {
6 | expect(actions.addTodo('Use Redux')).toEqual({
7 | type: 'ADD_TODO',
8 | id: 0,
9 | text: 'Use Redux'
10 | })
11 | })
12 |
13 | it('setVisibilityFilter should create SET_VISIBILITY_FILTER action', () => {
14 | expect(actions.setVisibilityFilter('active')).toEqual({
15 | type: 'SET_VISIBILITY_FILTER',
16 | filter: 'active'
17 | })
18 | })
19 |
20 | it('toogleTodo should create TOGGLE_TODO action', () => {
21 | expect(actions.toggleTodo(1)).toEqual({
22 | type: 'TOGGLE_TODO',
23 | id: 1
24 | })
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/examples/todos/test/reducers/todos.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import todos from '../../reducers/todos'
3 |
4 | describe('todos reducer', () => {
5 | it('should handle initial state', () => {
6 | expect(
7 | todos(undefined, {})
8 | ).toEqual([])
9 | })
10 |
11 | it('should handle ADD_TODO', () => {
12 | expect(
13 | todos([], {
14 | type: 'ADD_TODO',
15 | text: 'Run the tests',
16 | id: 0
17 | })
18 | ).toEqual([
19 | {
20 | text: 'Run the tests',
21 | completed: false,
22 | id: 0
23 | }
24 | ])
25 |
26 | expect(
27 | todos([
28 | {
29 | text: 'Run the tests',
30 | completed: false,
31 | id: 0
32 | }
33 | ], {
34 | type: 'ADD_TODO',
35 | text: 'Use Redux',
36 | id: 1
37 | })
38 | ).toEqual([
39 | {
40 | text: 'Run the tests',
41 | completed: false,
42 | id: 0
43 | }, {
44 | text: 'Use Redux',
45 | completed: false,
46 | id: 1
47 | }
48 | ])
49 |
50 | expect(
51 | todos([
52 | {
53 | text: 'Run the tests',
54 | completed: false,
55 | id: 0
56 | }, {
57 | text: 'Use Redux',
58 | completed: false,
59 | id: 1
60 | }
61 | ], {
62 | type: 'ADD_TODO',
63 | text: 'Fix the tests',
64 | id: 2
65 | })
66 | ).toEqual([
67 | {
68 | text: 'Run the tests',
69 | completed: false,
70 | id: 0
71 | }, {
72 | text: 'Use Redux',
73 | completed: false,
74 | id: 1
75 | }, {
76 | text: 'Fix the tests',
77 | completed: false,
78 | id: 2
79 | }
80 | ])
81 | })
82 |
83 | it('should handle TOGGLE_TODO', () => {
84 | expect(
85 | todos([
86 | {
87 | text: 'Run the tests',
88 | completed: false,
89 | id: 1
90 | }, {
91 | text: 'Use Redux',
92 | completed: false,
93 | id: 0
94 | }
95 | ], {
96 | type: 'TOGGLE_TODO',
97 | id: 1
98 | })
99 | ).toEqual([
100 | {
101 | text: 'Run the tests',
102 | completed: true,
103 | id: 1
104 | }, {
105 | text: 'Use Redux',
106 | completed: false,
107 | id: 0
108 | }
109 | ])
110 | })
111 |
112 | })
113 |
--------------------------------------------------------------------------------
/examples/todos/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/todos/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/tree-view/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/tree-view/actions/index.js:
--------------------------------------------------------------------------------
1 | export const INCREMENT = 'INCREMENT'
2 | export const CREATE_NODE = 'CREATE_NODE'
3 | export const DELETE_NODE = 'DELETE_NODE'
4 | export const ADD_CHILD = 'ADD_CHILD'
5 | export const REMOVE_CHILD = 'REMOVE_CHILD'
6 |
7 | export function increment(nodeId) {
8 | return {
9 | type: INCREMENT,
10 | nodeId
11 | }
12 | }
13 |
14 | let nextId = 0
15 | export function createNode() {
16 | return {
17 | type: CREATE_NODE,
18 | nodeId: `new_${nextId++}`
19 | }
20 | }
21 |
22 | export function deleteNode(nodeId) {
23 | return {
24 | type: DELETE_NODE,
25 | nodeId
26 | }
27 | }
28 |
29 | export function addChild(nodeId, childId) {
30 | return {
31 | type: ADD_CHILD,
32 | nodeId,
33 | childId
34 | }
35 | }
36 |
37 | export function removeChild(nodeId, childId) {
38 | return {
39 | type: REMOVE_CHILD,
40 | nodeId,
41 | childId
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/examples/tree-view/containers/Node.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Component } from 'react'
3 | import { connect } from 'react-redux'
4 | import * as actions from '../actions'
5 |
6 | class Node extends Component {
7 | constructor(props) {
8 | super(props)
9 | this.handleIncrementClick = this.handleIncrementClick.bind(this)
10 | this.handleRemoveClick = this.handleRemoveClick.bind(this)
11 | this.handleAddChildClick = this.handleAddChildClick.bind(this)
12 | this.renderChild = this.renderChild.bind(this)
13 | }
14 |
15 | handleIncrementClick() {
16 | const { increment, id } = this.props
17 | increment(id)
18 | }
19 |
20 | handleAddChildClick(e) {
21 | e.preventDefault()
22 |
23 | const { addChild, createNode, id } = this.props
24 | const childId = createNode().nodeId
25 | addChild(id, childId)
26 | }
27 |
28 | handleRemoveClick(e) {
29 | e.preventDefault()
30 |
31 | const { removeChild, deleteNode, parentId, id } = this.props
32 | removeChild(parentId, id)
33 | deleteNode(id)
34 | }
35 |
36 | renderChild(childId) {
37 | const { id } = this.props
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | render() {
46 | const { counter, parentId, childIds } = this.props
47 | return (
48 |
49 | Counter: {counter}
50 | {' '}
51 |
52 | +
53 |
54 | {' '}
55 | {typeof parentId !== 'undefined' ?
56 |
58 | ×
59 | :
60 | null
61 | }
62 |
70 |
71 | )
72 | }
73 | }
74 |
75 | function mapStateToProps(state, ownProps) {
76 | return state[ownProps.id]
77 | }
78 |
79 | const ConnectedNode = connect(mapStateToProps, actions)(Node)
80 | export default ConnectedNode
81 |
--------------------------------------------------------------------------------
/examples/tree-view/generateTree.js:
--------------------------------------------------------------------------------
1 | export default function generateTree() {
2 | let tree = {
3 | 0: {
4 | id: 0,
5 | counter: 0,
6 | childIds: []
7 | }
8 | }
9 |
10 | for (let i = 1; i < 1000; i++) {
11 | let parentId = Math.floor(Math.pow(Math.random(), 2) * i)
12 | tree[i] = {
13 | id: i,
14 | counter: 0,
15 | childIds: []
16 | }
17 | tree[parentId].childIds.push(i)
18 | }
19 |
20 | return tree
21 | }
22 |
--------------------------------------------------------------------------------
/examples/tree-view/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux tree-view example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/tree-view/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 Node from './containers/Node'
6 | import configureStore from './store/configureStore'
7 | import generateTree from './generateTree'
8 |
9 | const tree = generateTree()
10 | const store = configureStore(tree)
11 |
12 | render(
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | )
18 |
--------------------------------------------------------------------------------
/examples/tree-view/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-tree-view-example",
3 | "version": "0.0.0",
4 | "description": "Redux tree-view example",
5 | "scripts": {
6 | "start": "node server.js",
7 | "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register",
8 | "test:watch": "npm test -- --watch"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/reactjs/redux.git"
13 | },
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/reactjs/redux/issues"
17 | },
18 | "homepage": "http://redux.js.org",
19 | "dependencies": {
20 | "babel-polyfill": "^6.3.14",
21 | "react": "^0.14.7",
22 | "react-dom": "^0.14.7",
23 | "react-redux": "^4.2.1",
24 | "redux": "^3.2.1"
25 | },
26 | "devDependencies": {
27 | "babel-core": "^6.3.15",
28 | "babel-loader": "^6.2.0",
29 | "babel-preset-es2015": "^6.3.13",
30 | "babel-preset-react": "^6.3.13",
31 | "babel-preset-react-hmre": "^1.0.1",
32 | "babel-register": "^6.4.3",
33 | "deep-freeze": "0.0.1",
34 | "expect": "^1.6.0",
35 | "express": "^4.13.3",
36 | "jsdom": "^5.6.1",
37 | "mocha": "^2.2.5",
38 | "node-libs-browser": "^0.5.2",
39 | "webpack": "^1.9.11",
40 | "webpack-dev-middleware": "^1.2.0",
41 | "webpack-hot-middleware": "^2.2.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/examples/tree-view/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { INCREMENT, ADD_CHILD, REMOVE_CHILD, CREATE_NODE, DELETE_NODE } from '../actions'
2 |
3 | function childIds(state, action) {
4 | switch (action.type) {
5 | case ADD_CHILD:
6 | return [ ...state, action.childId ]
7 | case REMOVE_CHILD:
8 | const index = state.indexOf(action.childId)
9 | return [
10 | ...state.slice(0, index),
11 | ...state.slice(index + 1)
12 | ]
13 | default:
14 | return state
15 | }
16 | }
17 |
18 | function node(state, action) {
19 | switch (action.type) {
20 | case CREATE_NODE:
21 | return {
22 | id: action.nodeId,
23 | counter: 0,
24 | childIds: []
25 | }
26 | case INCREMENT:
27 | return Object.assign({}, state, {
28 | counter: state.counter + 1
29 | })
30 | case ADD_CHILD:
31 | case REMOVE_CHILD:
32 | return Object.assign({}, state, {
33 | childIds: childIds(state.childIds, action)
34 | })
35 | default:
36 | return state
37 | }
38 | }
39 |
40 | function getAllDescendantIds(state, nodeId) {
41 | return state[nodeId].childIds.reduce((acc, childId) => (
42 | [ ...acc, childId, ...getAllDescendantIds(state, childId) ]
43 | ), [])
44 | }
45 |
46 | function deleteMany(state, ids) {
47 | state = Object.assign({}, state)
48 | ids.forEach(id => delete state[id])
49 | return state
50 | }
51 |
52 | export default function (state = {}, action) {
53 | const { nodeId } = action
54 | if (typeof nodeId === 'undefined') {
55 | return state
56 | }
57 |
58 | if (action.type === DELETE_NODE) {
59 | const descendantIds = getAllDescendantIds(state, nodeId)
60 | return deleteMany(state, [ nodeId, ...descendantIds ])
61 | }
62 |
63 | return Object.assign({}, state, {
64 | [nodeId]: node(state[nodeId], action)
65 | })
66 | }
67 |
--------------------------------------------------------------------------------
/examples/tree-view/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/tree-view/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux'
2 | import reducer from '../reducers'
3 |
4 | export default function configureStore(initialState) {
5 | const store = createStore(reducer, initialState)
6 |
7 | if (module.hot) {
8 | // Enable Webpack hot module replacement for reducers
9 | module.hot.accept('../reducers', () => {
10 | const nextReducer = require('../reducers').default
11 | store.replaceReducer(nextReducer)
12 | })
13 | }
14 |
15 | return store
16 | }
17 |
--------------------------------------------------------------------------------
/examples/tree-view/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/examples/tree-view/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/universal/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | presets: ["es2015", "react"]
3 | }
--------------------------------------------------------------------------------
/examples/universal/client/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 configureStore from '../common/store/configureStore'
6 | import App from '../common/containers/App'
7 |
8 | const initialState = window.__INITIAL_STATE__
9 | const store = configureStore(initialState)
10 | const rootElement = document.getElementById('app')
11 |
12 | render(
13 |
14 |
15 | ,
16 | rootElement
17 | )
18 |
--------------------------------------------------------------------------------
/examples/universal/common/actions/index.js:
--------------------------------------------------------------------------------
1 | export const SET_COUNTER = 'SET_COUNTER'
2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'
3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'
4 |
5 | export function set(value) {
6 | return {
7 | type: SET_COUNTER,
8 | payload: value
9 | }
10 | }
11 |
12 | export function increment() {
13 | return {
14 | type: INCREMENT_COUNTER
15 | }
16 | }
17 |
18 | export function decrement() {
19 | return {
20 | type: DECREMENT_COUNTER
21 | }
22 | }
23 |
24 | export function incrementIfOdd() {
25 | return (dispatch, getState) => {
26 | const { counter } = getState()
27 |
28 | if (counter % 2 === 0) {
29 | return
30 | }
31 |
32 | dispatch(increment())
33 | }
34 | }
35 |
36 | export function incrementAsync(delay = 1000) {
37 | return dispatch => {
38 | setTimeout(() => {
39 | dispatch(increment())
40 | }, delay)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/universal/common/api/counter.js:
--------------------------------------------------------------------------------
1 | function getRandomInt(min, max) {
2 | return Math.floor(Math.random() * (max - min)) + min
3 | }
4 |
5 | export function fetchCounter(callback) {
6 | // Rather than immediately returning, we delay our code with a timeout to simulate asynchronous behavior
7 | setTimeout(() => {
8 | callback(getRandomInt(1, 100))
9 | }, 500)
10 |
11 | // In the case of a real world API call, you'll normally run into a Promise like this:
12 | // API.getUser().then(user => callback(user))
13 | }
14 |
--------------------------------------------------------------------------------
/examples/universal/common/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 |
3 | class Counter extends Component {
4 | render() {
5 | const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props
6 | return (
7 |
8 | Clicked: {counter} times
9 | {' '}
10 | +
11 | {' '}
12 | -
13 | {' '}
14 | Increment if odd
15 | {' '}
16 | incrementAsync()}>Increment async
17 |
18 | )
19 | }
20 | }
21 |
22 | Counter.propTypes = {
23 | increment: PropTypes.func.isRequired,
24 | incrementIfOdd: PropTypes.func.isRequired,
25 | incrementAsync: PropTypes.func.isRequired,
26 | decrement: PropTypes.func.isRequired,
27 | counter: PropTypes.number.isRequired
28 | }
29 |
30 | export default Counter
31 |
--------------------------------------------------------------------------------
/examples/universal/common/containers/App.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux'
2 | import { connect } from 'react-redux'
3 | import Counter from '../components/Counter'
4 | import * as CounterActions from '../actions'
5 |
6 | function mapStateToProps(state) {
7 | return {
8 | counter: state.counter
9 | }
10 | }
11 |
12 | function mapDispatchToProps(dispatch) {
13 | return bindActionCreators(CounterActions, dispatch)
14 | }
15 |
16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter)
17 |
--------------------------------------------------------------------------------
/examples/universal/common/reducers/counter.js:
--------------------------------------------------------------------------------
1 | import { SET_COUNTER, INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions'
2 |
3 | export default function counter(state = 0, action) {
4 | switch (action.type) {
5 | case SET_COUNTER:
6 | return action.payload
7 | case INCREMENT_COUNTER:
8 | return state + 1
9 | case DECREMENT_COUNTER:
10 | return state - 1
11 | default:
12 | return state
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/universal/common/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import counter from './counter'
3 |
4 | const rootReducer = combineReducers({
5 | counter
6 | })
7 |
8 | export default rootReducer
9 |
--------------------------------------------------------------------------------
/examples/universal/common/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import rootReducer from '../reducers'
4 |
5 | export default function configureStore(initialState) {
6 | const store = createStore(
7 | rootReducer,
8 | initialState,
9 | applyMiddleware(thunk)
10 | )
11 |
12 | if (module.hot) {
13 | // Enable Webpack hot module replacement for reducers
14 | module.hot.accept('../reducers', () => {
15 | const nextRootReducer = require('../reducers').default
16 | store.replaceReducer(nextRootReducer)
17 | })
18 | }
19 |
20 | return store
21 | }
22 |
--------------------------------------------------------------------------------
/examples/universal/index.js:
--------------------------------------------------------------------------------
1 | require('./client')
2 |
--------------------------------------------------------------------------------
/examples/universal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-universal-example",
3 | "version": "0.0.0",
4 | "description": "An example of a universally-rendered Redux application",
5 | "scripts": {
6 | "start": "node server/index.js"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/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 | "babel-register": "^6.4.3",
20 | "express": "^4.13.3",
21 | "qs": "^4.0.0",
22 | "react": "^0.14.7",
23 | "react-dom": "^0.14.7",
24 | "react-redux": "^4.2.1",
25 | "redux": "^3.2.1",
26 | "redux-thunk": "^1.0.3",
27 | "serve-static": "^1.10.0"
28 | },
29 | "devDependencies": {
30 | "babel-core": "^6.3.15",
31 | "babel-loader": "^6.2.0",
32 | "babel-preset-es2015": "^6.3.13",
33 | "babel-preset-react": "^6.3.13",
34 | "babel-preset-react-hmre": "^1.0.1",
35 | "babel-runtime": "^6.3.13",
36 | "webpack": "^1.11.0",
37 | "webpack-dev-middleware": "^1.4.0",
38 | "webpack-hot-middleware": "^2.6.0"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/universal/server/index.js:
--------------------------------------------------------------------------------
1 | require('babel-register')
2 | require('./server')
3 |
--------------------------------------------------------------------------------
/examples/universal/server/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, no-use-before-define */
2 |
3 | import path from 'path'
4 | import Express from 'express'
5 | import qs from 'qs'
6 |
7 | import webpack from 'webpack'
8 | import webpackDevMiddleware from 'webpack-dev-middleware'
9 | import webpackHotMiddleware from 'webpack-hot-middleware'
10 | import webpackConfig from '../webpack.config'
11 |
12 | import React from 'react'
13 | import { renderToString } from 'react-dom/server'
14 | import { Provider } from 'react-redux'
15 |
16 | import configureStore from '../common/store/configureStore'
17 | import App from '../common/containers/App'
18 | import { fetchCounter } from '../common/api/counter'
19 |
20 | const app = new Express()
21 | const port = 3000
22 |
23 | // Use this middleware to set up hot module reloading via webpack.
24 | const compiler = webpack(webpackConfig)
25 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath }))
26 | app.use(webpackHotMiddleware(compiler))
27 |
28 | // This is fired every time the server side receives a request
29 | app.use(handleRender)
30 |
31 | function handleRender(req, res) {
32 | // Query our mock API asynchronously
33 | fetchCounter(apiResult => {
34 | // Read the counter from the request, if provided
35 | const params = qs.parse(req.query)
36 | const counter = parseInt(params.counter, 10) || apiResult || 0
37 |
38 | // Compile an initial state
39 | const initialState = { counter }
40 |
41 | // Create a new Redux store instance
42 | const store = configureStore(initialState)
43 |
44 | // Render the component to a string
45 | const html = renderToString(
46 |
47 |
48 |
49 | )
50 |
51 | // Grab the initial state from our Redux store
52 | const finalState = store.getState()
53 |
54 | // Send the rendered page back to the client
55 | res.send(renderFullPage(html, finalState))
56 | })
57 | }
58 |
59 | function renderFullPage(html, initialState) {
60 | return `
61 |
62 |
63 |
64 | Redux Universal Example
65 |
66 |
67 | ${html}
68 |
71 |
72 |
73 |
74 | `
75 | }
76 |
77 | app.listen(port, (error) => {
78 | if (error) {
79 | console.error(error)
80 | } else {
81 | console.info(`==> 🌎 Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`)
82 | }
83 | })
84 |
--------------------------------------------------------------------------------
/examples/universal/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var webpack = require('webpack')
3 |
4 | module.exports = {
5 | devtool: 'inline-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './client/index.js'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | publicPath: '/static/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.NoErrorsPlugin()
19 | ],
20 | module: {
21 | loaders: [
22 | {
23 | test: /\.js$/,
24 | loader: 'babel',
25 | exclude: /node_modules/,
26 | include: __dirname,
27 | query: {
28 | presets: [ 'react-hmre' ]
29 | }
30 | }
31 | ]
32 | }
33 | }
34 |
35 | // When inside Redux repo, prefer src to compiled version.
36 | // You can safely delete these lines in your project.
37 | var reduxSrc = path.join(__dirname, '..', '..', 'src')
38 | var reduxNodeModules = path.join(__dirname, '..', '..', 'node_modules')
39 | var fs = require('fs')
40 | if (fs.existsSync(reduxSrc) && fs.existsSync(reduxNodeModules)) {
41 | // Resolve Redux to source
42 | module.exports.resolve = { alias: { 'redux': reduxSrc } }
43 | // Our root .babelrc needs this flag for CommonJS output
44 | process.env.BABEL_ENV = 'commonjs'
45 | // Compile Redux from source
46 | module.exports.module.loaders.push({
47 | test: /\.js$/,
48 | loaders: [ 'babel' ],
49 | include: reduxSrc,
50 | })
51 | }
52 |
--------------------------------------------------------------------------------
/src/applyMiddleware.js:
--------------------------------------------------------------------------------
1 | import compose from './compose'
2 |
3 | /**
4 | * Creates a store enhancer that applies middleware to the dispatch method
5 | * of the Redux store. This is handy for a variety of tasks, such as expressing
6 | * asynchronous actions in a concise manner, or logging every action payload.
7 | *
8 | * See `redux-thunk` package as an example of the Redux middleware.
9 | *
10 | * Because middleware is potentially asynchronous, this should be the first
11 | * store enhancer in the composition chain.
12 | *
13 | * Note that each middleware will be given the `dispatch` and `getState` functions
14 | * as named arguments.
15 | *
16 | * @param {...Function} middlewares The middleware chain to be applied.
17 | * @returns {Function} A store enhancer applying the middleware.
18 | */
19 | export default function applyMiddleware(...middlewares) {
20 | return (createStore) => (reducer, initialState, enhancer) => {
21 | var store = createStore(reducer, initialState, enhancer)
22 | var dispatch = store.dispatch
23 | var chain = []
24 |
25 | var middlewareAPI = {
26 | getState: store.getState,
27 | dispatch: (action) => dispatch(action)
28 | }
29 | chain = middlewares.map(middleware => middleware(middlewareAPI))
30 | dispatch = compose(...chain)(store.dispatch)
31 |
32 | return {
33 | ...store,
34 | dispatch
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/bindActionCreators.js:
--------------------------------------------------------------------------------
1 | function bindActionCreator(actionCreator, dispatch) {
2 | return (...args) => dispatch(actionCreator(...args))
3 | }
4 |
5 | /**
6 | * Turns an object whose values are action creators, into an object with the
7 | * same keys, but with every function wrapped into a `dispatch` call so they
8 | * may be invoked directly. This is just a convenience method, as you can call
9 | * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
10 | *
11 | * For convenience, you can also pass a single function as the first argument,
12 | * and get a function in return.
13 | *
14 | * @param {Function|Object} actionCreators An object whose values are action
15 | * creator functions. One handy way to obtain it is to use ES6 `import * as`
16 | * syntax. You may also pass a single function.
17 | *
18 | * @param {Function} dispatch The `dispatch` function available on your Redux
19 | * store.
20 | *
21 | * @returns {Function|Object} The object mimicking the original object, but with
22 | * every action creator wrapped into the `dispatch` call. If you passed a
23 | * function as `actionCreators`, the return value will also be a single
24 | * function.
25 | */
26 | export default function bindActionCreators(actionCreators, dispatch) {
27 | if (typeof actionCreators === 'function') {
28 | return bindActionCreator(actionCreators, dispatch)
29 | }
30 |
31 | if (typeof actionCreators !== 'object' || actionCreators === null) {
32 | throw new Error(
33 | `bindActionCreators expected an object or a function, instead received ${actionCreators === null ? 'null' : typeof actionCreators}. ` +
34 | `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
35 | )
36 | }
37 |
38 | var keys = Object.keys(actionCreators)
39 | var boundActionCreators = {}
40 | for (var i = 0; i < keys.length; i++) {
41 | var key = keys[i]
42 | var actionCreator = actionCreators[key]
43 | if (typeof actionCreator === 'function') {
44 | boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
45 | }
46 | }
47 | return boundActionCreators
48 | }
49 |
--------------------------------------------------------------------------------
/src/compose.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Composes single-argument functions from right to left. The rightmost
3 | * function can take multiple arguments as it provides the signature for
4 | * the resulting composite function.
5 | *
6 | * @param {...Function} funcs The functions to compose.
7 | * @returns {Function} A function obtained by composing the argument functions
8 | * from right to left. For example, compose(f, g, h) is identical to doing
9 | * (...args) => f(g(h(...args))).
10 | */
11 |
12 | export default function compose(...funcs) {
13 | return (...args) => {
14 | if (funcs.length === 0) {
15 | return args[0]
16 | }
17 |
18 | const last = funcs[funcs.length - 1]
19 | const rest = funcs.slice(0, -1)
20 |
21 | return rest.reduceRight((composed, f) => f(composed), last(...args))
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import createStore from './createStore'
2 | import combineReducers from './combineReducers'
3 | import bindActionCreators from './bindActionCreators'
4 | import applyMiddleware from './applyMiddleware'
5 | import compose from './compose'
6 | import warning from './utils/warning'
7 |
8 | /*
9 | * This is a dummy function to check if the function name has been altered by minification.
10 | * If the function has been minified and NODE_ENV !== 'production', warn the user.
11 | */
12 | function isCrushed() {}
13 |
14 | if (
15 | process.env.NODE_ENV !== 'production' &&
16 | typeof isCrushed.name === 'string' &&
17 | isCrushed.name !== 'isCrushed'
18 | ) {
19 | warning(
20 | 'You are currently using minified code outside of NODE_ENV === \'production\'. ' +
21 | 'This means that you are running a slower development build of Redux. ' +
22 | 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
23 | 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' +
24 | 'to ensure you have the correct code for your production build.'
25 | )
26 | }
27 |
28 | export {
29 | createStore,
30 | combineReducers,
31 | bindActionCreators,
32 | applyMiddleware,
33 | compose
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/warning.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Prints a warning in the console if it exists.
3 | *
4 | * @param {String} message The warning message.
5 | * @returns {void}
6 | */
7 | export default function warning(message) {
8 | /* eslint-disable no-console */
9 | if (typeof console !== 'undefined' && typeof console.error === 'function') {
10 | console.error(message)
11 | }
12 | /* eslint-enable no-console */
13 | try {
14 | // This error was thrown as a convenience so that you can use this stack
15 | // to find the callsite that caused this warning to fire.
16 | throw new Error(message)
17 | /* eslint-disable no-empty */
18 | } catch (e) { }
19 | /* eslint-enable no-empty */
20 | }
21 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/applyMiddleware.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import { createStore, applyMiddleware } from '../src/index'
3 | import * as reducers from './helpers/reducers'
4 | import { addTodo, addTodoAsync, addTodoIfEmpty } from './helpers/actionCreators'
5 | import { thunk } from './helpers/middleware'
6 |
7 | describe('applyMiddleware', () => {
8 | it('wraps dispatch method with middleware once', () => {
9 | function test(spyOnMethods) {
10 | return methods => {
11 | spyOnMethods(methods)
12 | return next => action => next(action)
13 | }
14 | }
15 |
16 | const spy = expect.createSpy(() => {})
17 | const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos)
18 |
19 | store.dispatch(addTodo('Use Redux'))
20 | store.dispatch(addTodo('Flux FTW!'))
21 |
22 | expect(spy.calls.length).toEqual(1)
23 |
24 | expect(Object.keys(spy.calls[0].arguments[0])).toEqual([
25 | 'getState',
26 | 'dispatch'
27 | ])
28 |
29 | expect(store.getState()).toEqual([ { id: 1, text: 'Use Redux' }, { id: 2, text: 'Flux FTW!' } ])
30 | })
31 |
32 | it('passes recursive dispatches through the middleware chain', () => {
33 | function test(spyOnMethods) {
34 | return () => next => action => {
35 | spyOnMethods(action)
36 | return next(action)
37 | }
38 | }
39 |
40 | const spy = expect.createSpy(() => {})
41 | const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos)
42 |
43 | return store.dispatch(addTodoAsync('Use Redux')).then(() => {
44 | expect(spy.calls.length).toEqual(2)
45 | })
46 | })
47 |
48 | it('works with thunk middleware', done => {
49 | const store = applyMiddleware(thunk)(createStore)(reducers.todos)
50 |
51 | store.dispatch(addTodoIfEmpty('Hello'))
52 | expect(store.getState()).toEqual([
53 | {
54 | id: 1,
55 | text: 'Hello'
56 | }
57 | ])
58 |
59 | store.dispatch(addTodoIfEmpty('Hello'))
60 | expect(store.getState()).toEqual([
61 | {
62 | id: 1,
63 | text: 'Hello'
64 | }
65 | ])
66 |
67 | store.dispatch(addTodo('World'))
68 | expect(store.getState()).toEqual([
69 | {
70 | id: 1,
71 | text: 'Hello'
72 | },
73 | {
74 | id: 2,
75 | text: 'World'
76 | }
77 | ])
78 |
79 | store.dispatch(addTodoAsync('Maybe')).then(() => {
80 | expect(store.getState()).toEqual([
81 | {
82 | id: 1,
83 | text: 'Hello'
84 | },
85 | {
86 | id: 2,
87 | text: 'World'
88 | },
89 | {
90 | id: 3,
91 | text: 'Maybe'
92 | }
93 | ])
94 | done()
95 | })
96 | })
97 | })
98 |
--------------------------------------------------------------------------------
/test/bindActionCreators.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import { bindActionCreators, createStore } from '../src'
3 | import { todos } from './helpers/reducers'
4 | import * as actionCreators from './helpers/actionCreators'
5 |
6 | describe('bindActionCreators', () => {
7 | let store
8 | let actionCreatorFunctions
9 |
10 | beforeEach(() => {
11 | store = createStore(todos)
12 | actionCreatorFunctions = { ...actionCreators }
13 | Object.keys(actionCreatorFunctions).forEach(key => {
14 | if (typeof actionCreatorFunctions[key] !== 'function') {
15 | delete actionCreatorFunctions[key]
16 | }
17 | })
18 | })
19 |
20 | it('wraps the action creators with the dispatch function', () => {
21 | const boundActionCreators = bindActionCreators(actionCreators, store.dispatch)
22 | expect(
23 | Object.keys(boundActionCreators)
24 | ).toEqual(
25 | Object.keys(actionCreatorFunctions)
26 | )
27 |
28 | const action = boundActionCreators.addTodo('Hello')
29 | expect(action).toEqual(
30 | actionCreators.addTodo('Hello')
31 | )
32 | expect(store.getState()).toEqual([
33 | { id: 1, text: 'Hello' }
34 | ])
35 | })
36 |
37 | it('skips non-function values in the passed object', () => {
38 | const boundActionCreators = bindActionCreators({
39 | ...actionCreators,
40 | foo: 42,
41 | bar: 'baz',
42 | wow: undefined,
43 | much: {},
44 | test: null
45 | }, store.dispatch)
46 | expect(
47 | Object.keys(boundActionCreators)
48 | ).toEqual(
49 | Object.keys(actionCreatorFunctions)
50 | )
51 | })
52 |
53 | it('supports wrapping a single function only', () => {
54 | const actionCreator = actionCreators.addTodo
55 | const boundActionCreator = bindActionCreators(actionCreator, store.dispatch)
56 |
57 | const action = boundActionCreator('Hello')
58 | expect(action).toEqual(actionCreator('Hello'))
59 | expect(store.getState()).toEqual([
60 | { id: 1, text: 'Hello' }
61 | ])
62 | })
63 |
64 | it('throws for an undefined actionCreator', () => {
65 | expect(() => {
66 | bindActionCreators(undefined, store.dispatch)
67 | }).toThrow(
68 | 'bindActionCreators expected an object or a function, instead received undefined. ' +
69 | 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?'
70 | )
71 | })
72 |
73 | it('throws for a null actionCreator', () => {
74 | expect(() => {
75 | bindActionCreators(null, store.dispatch)
76 | }).toThrow(
77 | 'bindActionCreators expected an object or a function, instead received null. ' +
78 | 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?'
79 | )
80 | })
81 |
82 | it('throws for a primitive actionCreator', () => {
83 | expect(() => {
84 | bindActionCreators('string', store.dispatch)
85 | }).toThrow(
86 | 'bindActionCreators expected an object or a function, instead received string. ' +
87 | 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?'
88 | )
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/test/compose.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import { compose } from '../src'
3 |
4 | describe('Utils', () => {
5 | describe('compose', () => {
6 | it('composes from right to left', () => {
7 | const double = x => x * 2
8 | const square = x => x * x
9 | expect(compose(square)(5)).toBe(25)
10 | expect(compose(square, double)(5)).toBe(100)
11 | expect(compose(double, square, double)(5)).toBe(200)
12 | })
13 |
14 | it('composes functions from right to left', () => {
15 | const a = next => x => next(x + 'a')
16 | const b = next => x => next(x + 'b')
17 | const c = next => x => next(x + 'c')
18 | const final = x => x
19 |
20 | expect(compose(a, b, c)(final)('')).toBe('abc')
21 | expect(compose(b, c, a)(final)('')).toBe('bca')
22 | expect(compose(c, a, b)(final)('')).toBe('cab')
23 | })
24 |
25 | it('can be seeded with multiple arguments', () => {
26 | const square = x => x * x
27 | const add = (x, y) => x + y
28 | expect(compose(square, add)(1, 2)).toBe(9)
29 | })
30 |
31 | it('returns the first given argument if given no functions', () => {
32 | expect(compose()(1, 2)).toBe(1)
33 | expect(compose()(3)).toBe(3)
34 | expect(compose()()).toBe(undefined)
35 | })
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/test/helpers/actionCreators.js:
--------------------------------------------------------------------------------
1 | import { ADD_TODO, DISPATCH_IN_MIDDLE, THROW_ERROR, UNKNOWN_ACTION } from './actionTypes'
2 |
3 | export function addTodo(text) {
4 | return { type: ADD_TODO, text }
5 | }
6 |
7 | export function addTodoAsync(text) {
8 | return dispatch => new Promise(resolve => setImmediate(() => {
9 | dispatch(addTodo(text))
10 | resolve()
11 | }))
12 | }
13 |
14 | export function addTodoIfEmpty(text) {
15 | return (dispatch, getState) => {
16 | if (!getState().length) {
17 | dispatch(addTodo(text))
18 | }
19 | }
20 | }
21 |
22 | export function dispatchInMiddle(boundDispatchFn) {
23 | return {
24 | type: DISPATCH_IN_MIDDLE,
25 | boundDispatchFn
26 | }
27 | }
28 |
29 | export function throwError() {
30 | return {
31 | type: THROW_ERROR
32 | }
33 | }
34 |
35 | export function unknownAction() {
36 | return {
37 | type: UNKNOWN_ACTION
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/test/helpers/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO'
2 | export const DISPATCH_IN_MIDDLE = 'DISPATCH_IN_MIDDLE'
3 | export const THROW_ERROR = 'THROW_ERROR'
4 | export const UNKNOWN_ACTION = 'UNKNOWN_ACTION'
5 |
--------------------------------------------------------------------------------
/test/helpers/middleware.js:
--------------------------------------------------------------------------------
1 | export function thunk({ dispatch, getState }) {
2 | return next => action =>
3 | typeof action === 'function' ?
4 | action(dispatch, getState) :
5 | next(action)
6 | }
7 |
--------------------------------------------------------------------------------
/test/helpers/reducers.js:
--------------------------------------------------------------------------------
1 | import { ADD_TODO, DISPATCH_IN_MIDDLE, THROW_ERROR } from './actionTypes'
2 |
3 |
4 | function id(state = []) {
5 | return state.reduce((result, item) => (
6 | item.id > result ? item.id : result
7 | ), 0) + 1
8 | }
9 |
10 | export function todos(state = [], action) {
11 | switch (action.type) {
12 | case ADD_TODO:
13 | return [
14 | ...state,
15 | {
16 | id: id(state),
17 | text: action.text
18 | }
19 | ]
20 | default:
21 | return state
22 | }
23 | }
24 |
25 | export function todosReverse(state = [], action) {
26 | switch (action.type) {
27 | case ADD_TODO:
28 | return [
29 | {
30 | id: id(state),
31 | text: action.text
32 | }, ...state
33 | ]
34 | default:
35 | return state
36 | }
37 | }
38 |
39 | export function dispatchInTheMiddleOfReducer(state = [], action) {
40 | switch (action.type) {
41 | case DISPATCH_IN_MIDDLE:
42 | action.boundDispatchFn()
43 | return state
44 | default:
45 | return state
46 | }
47 | }
48 |
49 | export function errorThrowingReducer(state = [], action) {
50 | switch (action.type) {
51 | case THROW_ERROR:
52 | throw new Error()
53 | default:
54 | return state
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/test/utils/warning.spec.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect'
2 | import warning from '../../src/utils/warning'
3 |
4 | describe('Utils', () => {
5 | describe('warning', () => {
6 | it('calls console.error when available', () => {
7 | const spy = expect.spyOn(console, 'error')
8 | try {
9 | warning('Test')
10 | expect(spy.calls[0].arguments[0]).toBe('Test')
11 | } finally {
12 | spy.restore()
13 | }
14 | })
15 |
16 | it('does not throw when console.error is not available', () => {
17 | const realConsole = global.console
18 | Object.defineProperty(global, 'console', { value: {} })
19 | try {
20 | expect(() => warning('Test')).toNotThrow()
21 | } finally {
22 | Object.defineProperty(global, 'console', { value: realConsole })
23 | }
24 | })
25 |
26 | it('does not throw when console is not available', () => {
27 | const realConsole = global.console
28 | Object.defineProperty(global, 'console', { value: undefined })
29 | try {
30 | expect(() => warning('Test')).toNotThrow()
31 | } finally {
32 | Object.defineProperty(global, 'console', { value: realConsole })
33 | }
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var webpack = require('webpack')
4 |
5 | var env = process.env.NODE_ENV
6 | var config = {
7 | module: {
8 | loaders: [
9 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ }
10 | ]
11 | },
12 | output: {
13 | library: 'Redux',
14 | libraryTarget: 'umd'
15 | },
16 | plugins: [
17 | new webpack.optimize.OccurenceOrderPlugin(),
18 | new webpack.DefinePlugin({
19 | 'process.env.NODE_ENV': JSON.stringify(env)
20 | })
21 | ]
22 | };
23 |
24 | if (env === 'production') {
25 | config.plugins.push(
26 | new webpack.optimize.UglifyJsPlugin({
27 | compressor: {
28 | pure_getters: true,
29 | unsafe: true,
30 | unsafe_comps: true,
31 | screw_ie8: true,
32 | warnings: false
33 | }
34 | })
35 | )
36 | }
37 |
38 | module.exports = config
39 |
--------------------------------------------------------------------------------