├── .babelrc ├── .eslintrc ├── .gitattributes ├── .gitbook.yml ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── help.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── README.md ├── guides │ ├── async-await.md │ ├── chaining-actions.md │ ├── comparison.md │ ├── custom-delimiters.md │ ├── custom-suffixes.md │ ├── design-principles.md │ ├── dispatching-promises.md │ ├── null-values.md │ ├── optimistic-updates.md │ ├── passing-extra-values-to-reducer.md │ ├── reducers.md │ ├── redux-actions.md │ ├── redux-promise-middleware-actions.md │ └── rejected-promises.md ├── introduction.md └── upgrading │ ├── v4.md │ ├── v5.md │ └── v6.md ├── examples ├── catching-errors-with-middleware │ ├── .gitignore │ ├── README.md │ ├── actions.js │ ├── index.html │ ├── index.js │ ├── middleware.js │ ├── package-lock.json │ ├── package.json │ └── store.js ├── catching-errors │ ├── .gitignore │ ├── README.md │ ├── actions.js │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── store.js ├── using-promise-actions │ ├── .gitignore │ ├── README.md │ ├── actions.js │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── store.js ├── using-promise-all │ ├── .gitignore │ ├── actions.js │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── store.js ├── using-promise-middleware │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── store.js └── using-typescript │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ ├── store.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── index.d.ts ├── index.js └── isPromise.js ├── test ├── .eslintrc ├── actions.spec.js ├── async-functions.spec.js ├── bluebird.spec.js ├── delimiter.spec.js ├── function.spec.js ├── module-versions.spec.js ├── module.spec.js ├── optimistic-updates.spec.js ├── promise-fulfilled.spec.js ├── promise-pending.spec.js ├── promise-rejected.spec.js ├── resolve-boolean.spec.js ├── resolve-null.spec.js ├── resolve-number.spec.js └── utils │ ├── createStore.js │ ├── defaults.js │ ├── modifierMiddleware.js │ └── spyMiddleware.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/env"], "@babel/react"], 3 | "env": { 4 | "es": { 5 | "presets": [["@babel/env", { "modules": false }]] 6 | }, 7 | "test": { 8 | "presets": [ 9 | [ 10 | "@babel/env", 11 | { 12 | "targets": { 13 | "node": "current" 14 | } 15 | } 16 | ] 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true, 8 | "mocha": true 9 | }, 10 | "parserOptions": { 11 | "ecmaVersion": 2018, 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "rules": { 18 | "react/jsx-uses-react": 2, 19 | "react/jsx-uses-vars": 2, 20 | "react/react-in-jsx-scope": 2, 21 | "react/jsx-filename-extension": 0, 22 | "padded-blocks": 0, 23 | "block-scoped-var": 0, 24 | "comma-dangle": 0, 25 | "arrow-parens": 0, 26 | "no-extra-boolean-cast": 0, 27 | "import/no-extraneous-dependencies": 0, 28 | "import/first": 0, 29 | "import/no-unresolved": 0, 30 | "import/extensions": 0, 31 | "brace-style": 0 32 | }, 33 | "plugins": [ 34 | "react" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text=auto 2 | -------------------------------------------------------------------------------- /.gitbook.yml: -------------------------------------------------------------------------------- 1 | structure: 2 | readme: README.md 3 | summary: docs/README.md -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 5 | 6 | ## Our Standards 7 | Examples of behavior that contributes to creating a positive environment include: 8 | * Using welcoming and inclusive language 9 | * Being respectful of differing viewpoints and experiences 10 | * Gracefully accepting constructive criticism 11 | * Focusing on what is best for the community 12 | * Showing empathy towards other community members 13 | 14 | Examples of unacceptable behavior by participants include: 15 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 16 | * Trolling, insulting/derogatory comments, and personal or political attacks 17 | * Public or private harassment 18 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 19 | * Other conduct which could reasonably be considered inappropriate in a professional setting 20 | 21 | ## Our Responsibilities 22 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 23 | 24 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 25 | 26 | ## Scope 27 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 28 | 29 | ## Enforcement 30 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting Patrick Burtchaell, the project owner, at [patrick@pburtchaell.com](mailto:patrick@pburtchaell.com). The project owner will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 31 | 32 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 33 | 34 | ## Attribution 35 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]. 36 | 37 | [homepage]: http://contributor-covenant.org 38 | [version]: http://contributor-covenant.org/version/1/4/ 39 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Please familize yourself with the [GitHub Community Guidelines](https://help.github.com/articles/github-community-guidelines/) before contributing. 3 | 4 | ## Getting Started 5 | 6 | 1. Clone the repository 7 | 2. Install dependencies: `npm install` 8 | 3. Run tests: `npm test` 9 | 4. Run examples: `npm start` 10 | 11 | ### File Organization 12 | All code is written in JavaScript and transpiled using Babel. 13 | 14 | Source files are located in the `src` directory. Test files are located in the `test` directory. 15 | 16 | ### Git 17 | Git commit messages [should be written in the imperative](http://chris.beams.io/posts/git-commit/). 18 | 19 | A pre-commit hook will run tests and lint code when you make a commit. If needed, you can force a commit with `--no-verify`. 20 | 21 | ### Tests 22 | Tests are written in Mocha and code style is maintained with ESLint. 23 | 24 | ### Dependencies 25 | Please use Yarn instead of npm to upgrade dependencies. 26 | 27 | You can interactively upgrade dependencies: 28 | 29 | ``` 30 | yarn upgrade-intractive 31 | ``` 32 | 33 | Or you can upgrade dependencies one by one. For example, to upgrade Redux, you would run a command like this: 34 | 35 | ``` 36 | yarn add redux@4.0.0 -D 37 | ``` 38 | 39 | Install [synp](https://github.com/imsnif/synp): 40 | 41 | ``` 42 | npm install -g synp 43 | ``` 44 | 45 | Sync package-lock.json with yark.lock with: 46 | 47 | ``` 48 | yarn generate-lockfile 49 | ``` 50 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: pburtchaell 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | Hello! :wave: 6 | 7 | This template enables you to open an issue that is valuable for you, the project, and the GitHub community. :two_hearts: 8 | 9 | Before submitting an issue, please: 10 | 1. Search for related issues. 11 | 2. Agree the [code of conduct](/.github/CODE_OF_CONDUCT.md). 12 | 3. Read the [contributing guide](/.github/CONTRIBUTING.md). 13 | 14 | If you are new to open source, check out this 38 minute course on [how to contribute to open source on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). It's free! :smile: 15 | 16 | --- 17 | 18 | ## Version Number 19 | The Redux version number and the Redux Promise Middleware version number. 20 | 21 | If your issue involves other libraries, please include the names and versions of those libraries 22 | 23 | ## Test Case 24 | Some code or a fork to help us reproduce the behavior. 25 | 26 | Consider creating a [JSBin](https://jsbin.com/?html,output) for the code example. 27 | 28 | ## Steps to Reproduce 29 | Steps to reproduce the behavior: 30 | 1. Go to '...' 31 | 2. Click on '....' 32 | 3. Scroll down to '....' 33 | 4. See error 34 | 35 | ## Expected Behavior 36 | A clear and concise description of what the bug is. 37 | 38 | ## Actual Behavior 39 | A clear and concise description of what you expected to happen. 40 | 41 | ## Screenshots 42 | If applicable, screenshots to help explain your problem. 43 | 44 | ## Additional Context 45 | Add any other context about the problem here. 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | Hello! :wave: 6 | 7 | This template enables you to open an issue that is valuable for you, the project, and the GitHub community. :two_hearts: 8 | 9 | Before submitting an issue, please: 10 | 1. Agree the [Code of Conduct](/.github/CODE_OF_CONDUCT.md). 11 | 2. Read the [Contributing Guide](/.github/CONTRIBUTING.md). 12 | 13 | If you are new to open source, check out this 38 minute course on [how to contribute to open source on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). It's free! :smile: 14 | 15 | --- 16 | 17 | ## Problem 18 | A clear and concise description of what the problem is and how it relates to the project. 19 | 20 | ## Solution(s) 21 | A clear and concise description of what you want to happen. 22 | 23 | ## Alternatives 24 | A clear and concise description of any alternative solutions or features you've considered. 25 | 26 | ## Additional Context 27 | Add any other context or screenshots about the feature request here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Help 3 | about: Ask for help on a problem 4 | --- 5 | Issues should primarily be used for bug reports and feature requests. 6 | 7 | Before you ask for help on a problem, please answer the following questions: 8 | 9 | - Am I asking for help on a problem that is unreleated to or outside the scope of this project? (yes/no) 10 | - Can I ask for help on [StackOverflow](https://stackoverflow.com/questions/tagged/redux-promise-middleware) instead of GitHub? (yes/no) 11 | 12 | If you answered "yes" to one of those questions, please do not create an issue asking for help. I 13 | 14 | Instead, ask a question on StackOverflow with the "redux-promise-middleware" tag. We actively watch [StackOverflow for questions about the middleware](https://stackoverflow.com/questions/tagged/redux-promise-middleware) and will help you through that channel. 15 | 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Hello! :wave: 2 | 3 | This template enables you to open an issue that is valuable for you, the project, and the GitHub community. :two_hearts: 4 | 5 | Before submitting an issue, please: 6 | 1. Agree the [Code of Conduct](/.github/CODE_OF_CONDUCT.md). 7 | 2. Read the [Contributing Guide](/.github/CONTRIBUTING.md). 8 | 9 | If you are new to open source, check out this 38 minute course on [how to contribute to open source on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). It's free! :smile: 10 | 11 | --- 12 | 13 | Before submitting a pull request, please make sure the following is done: 14 | 15 | 1. Add tests for new functionality or bug fixes, following the [red/green/refactor method](https://en.wikipedia.org/wiki/Test-driven_development#Development_style) 16 | 2. Ensure all tests pass: `npm test` 17 | 3. Update and/or add documentation 18 | 19 | **In order to merge your Pull Request, tests and documentation are required.** -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | example/build/ 3 | node_modules/ 4 | .idea/ 5 | .coverage 6 | .coveralls.yml 7 | .DS_Store 8 | *.log 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | bin/ 3 | docs/ 4 | examples/ 5 | jspm_packages/ 6 | node_modules/ 7 | src/ 8 | test/ 9 | .babelrc 10 | .eslintrc 11 | .npmignore 12 | .travis.yml 13 | webpack.config.js 14 | !src/index.d.ts 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" # latest stable release 4 | - "v8.10.0" 5 | cache: yarn 6 | script: 7 | - yarn test 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Patrick Burtchaell 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux Promise Middleware 2 | 3 | [![npm downloads](https://img.shields.io/npm/dm/redux-promise-middleware.svg?style=flat)](https://www.npmjs.com/package/redux-promise-middleware) 4 | 5 | Redux Promise Middleware enables simple, yet robust handling of async action creators in [Redux](http://redux.js.org). 6 | 7 | ```js 8 | const asyncAction = () => ({ 9 | type: 'PROMISE', 10 | payload: new Promise(...), 11 | }) 12 | ``` 13 | 14 | Given a single action with an async payload, the middleware transforms the action to a separate pending action and a separate fulfilled/rejected action, representing the states of the async action. 15 | 16 | The middleware can be combined with [Redux Thunk](https://github.com/gaearon/redux-thunk) to chain action creators. 17 | 18 | ```js 19 | const secondAction = (data) => ({ 20 | type: 'SECOND', 21 | payload: {...}, 22 | }) 23 | 24 | const firstAction = () => { 25 | return (dispatch) => { 26 | const response = dispatch({ 27 | type: 'FIRST', 28 | payload: new Promise(...), 29 | }) 30 | 31 | response.then((data) => { 32 | dispatch(secondAction(data)) 33 | }) 34 | } 35 | } 36 | ``` 37 | 38 | ## Documentation and Help 39 | - [Introduction](/docs/introduction.md) 40 | - [Guides](/docs/guides/) 41 | - [Examples](/examples) 42 | 43 | **Heads Up:** Version 6 includes some breaking changes. Check the [upgrading guide](docs/upgrading/v6.md) for help. 44 | 45 | ## Issues 46 | For bug reports and feature requests, [file an issue on GitHub](https://github.com/pburtchaell/redux-promise-middleware/issues/new). 47 | 48 | For help, [ask a question on StackOverflow](https://stackoverflow.com/questions/tagged/redux-promise-middleware). 49 | 50 | ## Releases 51 | - [Release History](https://github.com/pburtchaell/redux-promise-middleware/releases) 52 | - [Upgrade from 5.x to 6.0.0](docs/upgrading/v6.md) 53 | - [Upgrade from 4.x to 5.0.0](docs/upgrading/v5.md) 54 | - [Upgrade from 3.x to 4.0.0](docs/upgrading/v4.md) 55 | 56 | For older versions: 57 | - [5.x](https://github.com/pburtchaell/redux-promise-middleware/tree/5.1.1) 58 | - [4.x](https://github.com/pburtchaell/redux-promise-middleware/tree/4.4.0) 59 | - [3.x](https://github.com/pburtchaell/redux-promise-middleware/tree/3.3.0) 60 | - [2.x](https://github.com/pburtchaell/redux-promise-middleware/tree/2.4.0) 61 | - [1.x](https://github.com/pburtchaell/redux-promise-middleware/tree/1.0.0) 62 | 63 | ## Maintainers 64 | Please reach out to us if you have any questions or comments. 65 | 66 | Patrick Burtchaell (pburtchaell): 67 | - [GitHub](https://github.com/pburtchaell) 68 | 69 | Thomas Hudspith-Tatham (tomatau): 70 | - [GitHub](https://github.com/tomatau) 71 | 72 | ## License 73 | 74 | [Code licensed with the MIT License (MIT)](/LICENSE). 75 | 76 | [Documentation licensed with the CC BY-NC License](https://creativecommons.org/licenses/by-nc/4.0/). 77 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | ## Getting Started 4 | - [Introduction](introduction.md) 5 | - [Design Principles](guides/design-principles.md) 6 | 7 | ## Guides 8 | - [Catching Errors Thrown by Rejected Promises](guides/rejected-promises.md) 9 | - [Chaining Actions](guides/chaining-actions.md) 10 | - [Passing extra values to reducer](guides/passing-extra-values-to-reducer.md) 11 | - [Comparison to other promise middleware](guides/comparison.md) 12 | - [Custom Type Delimiters](guides/custom-delimiters.md) 13 | - [Custom Types](guides/custom-suffixes.md) 14 | - [Optimistic Updates](guides/optimistic-updates.md) 15 | - [Use with Async/Await](guides/async-await.md) 16 | - [Use with Reducers](guides/reducers.md) 17 | - [Use with Redux Actions](guides/redux-actions.md) 18 | - [Use with Redux Promise Actions](guides/redux-promise-middleware-actions.md) 19 | - [Use with Promises Resolved with Null Values](guides/null-values.md) 20 | 21 | ## Upgrade Guides 22 | - [Upgrade from 5.x to 6.0.0](upgrading/v6.md) 23 | - [Upgrade from 4.x to 5.0.0](upgrading/v5.md) 24 | - [Upgrade from 3.x to 4.0.0](upgrading/v4.md) 25 | - [Release History](https://github.com/pburtchaell/redux-promise-middleware/releases) 26 | -------------------------------------------------------------------------------- /docs/guides/async-await.md: -------------------------------------------------------------------------------- 1 | # Use with Async/Await 2 | 3 | Instead of chaining your async code with `.then().then().then()`, you can use async/await. 4 | 5 | Consider this example. First, request `fooData`, then request `barData` and exit the function (also resolving the promise). 6 | 7 | ```js 8 | { 9 | type: 'TYPE', 10 | async payload () { 11 | const fooData = await getFooData(); 12 | const barData = await getBarData(fooData); 13 | 14 | return barData; 15 | } 16 | } 17 | ``` 18 | 19 | Async/await can be combined with data for [optimistic updates](optimistic-updates.md): 20 | 21 | ```js 22 | { 23 | type: 'OPTIMISTIC_TYPE', 24 | payload: { 25 | data: { 26 | ... 27 | }, 28 | async promise () { 29 | ... 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | Please note there is no need to `return await` in an async function. [See this ESLint rule for more details](https://eslint.org/docs/rules/no-return-await). 36 | -------------------------------------------------------------------------------- /docs/guides/chaining-actions.md: -------------------------------------------------------------------------------- 1 | # Chaining Actions 2 | 3 | When a promise is resolved, one might want to dispatch additional actions in response. One example could be changing the route after a user is successfully signed in. Another could be showing an error message after a request fails. 4 | 5 | First, note this behavior uses thunks. You will need to include [Redux Thunk](https://github.com/gaearon/redux-thunk) in your middleware stack. 6 | 7 | *Note: Redux Thunk is a middleware that enables action creators to return a function instead of an object ([hence the name "thunk"](https://en.wikipedia.org/wiki/Thunk)). The returned function is called with a `dispatch` argument, which is what you can use to chain actions.* 8 | 9 | 10 | ```js 11 | const foo = () => { 12 | return dispatch => { 13 | 14 | return dispatch({ 15 | type: 'TYPE', 16 | payload: new Promise() 17 | }).then(() => dispatch(bar())); 18 | }; 19 | } 20 | ``` 21 | 22 | If you need to chain several actions, using `Promise.all` is suggested. 23 | 24 | ```js 25 | const foo = () => { 26 | return dispatch => { 27 | 28 | return dispatch({ 29 | type: 'TYPE', 30 | payload: Promise.all([ 31 | dispatch(bar()), 32 | dispatch(baz()) 33 | ]) 34 | }); 35 | }; 36 | } 37 | ``` 38 | 39 | When handling a promise with `then`, the parameter is an object with two properties: (1) the "value" (if the promise is fulfilled) or the "reason" (if the promise is rejected) and (2) the object of the dispatched action. 40 | 41 | ```js 42 | // fulfilled promise 43 | const foo = () => { 44 | return dispatch => { 45 | 46 | return dispatch({ 47 | type: 'FOO', 48 | payload: new Promise(resolve => { 49 | resolve('foo'); // resolve the promise with the value 'foo' 50 | }) 51 | }).then(({ value, action }) => { 52 | console.log(value); // => 'foo' 53 | console.log(action.type); // => 'FOO_FULFILLED' 54 | }); 55 | }; 56 | } 57 | 58 | // rejected promise 59 | const bar = () => { 60 | return dispatch => { 61 | 62 | return dispatch({ 63 | type: 'BAR', 64 | payload: new Promise(() => { 65 | throw new Error('foo'); // reject the promise for the reason 'bar' 66 | }) 67 | }).then(() => null, error => { 68 | console.log(error instanceof Error) // => true 69 | console.log(error.message); // => 'foo' 70 | }); 71 | }; 72 | } 73 | ``` 74 | 75 | Rejected promises can also be handled with `.catch()`. 76 | 77 | ```js 78 | // rejected promise with throw 79 | const baz = () => { 80 | return dispatch => { 81 | 82 | return dispatch({ 83 | type: 'BAZ', 84 | payload: new Promise(() => { 85 | throw new Error(); // throw an error 86 | }) 87 | }).catch((error) => { 88 | console.log(error instanceof Error) // => true 89 | }); 90 | }; 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/guides/comparison.md: -------------------------------------------------------------------------------- 1 | # What is the difference between this and other promise middleware? 2 | 3 | For context, this question was originally asked in issue [#27](https://github.com/pburtchaell/redux-promise-middleware/issues/27). 4 | 5 | ## [acdlite/redux-promise](https://github.com/acdlite/redux-promise) 6 | 7 | Both middleware solve the same problem, but the implementation is different. Promise middleware dispatches a pending action in addition to a rejected or fulfilled action. This is a feature acdlite/redux-promise has not implemented at time of writing (November 2015). The pending action enables optimistic updates and describes an unsettled promise. 8 | 9 | Both middleware use the [Flux Standard Action](https://github.com/acdlite/flux-standard-action) specification. 10 | 11 | One could also argue the API for promise middleware is more transparent and easier to integrate. For example, you do not need to use [redux-actions](https://github.com/acdlite/redux-actions). 12 | -------------------------------------------------------------------------------- /docs/guides/custom-delimiters.md: -------------------------------------------------------------------------------- 1 | # Type Delimiter Configuration 2 | 3 | In the case you need to use different type delimiters, you can configure this globally for all actions. By default, the middleware uses a underscore `_` delimiter. 4 | 5 | For example, given `FOO` async action, `PENDING` type will be appended with a underscore `_` delimiter. 6 | 7 | ```js 8 | { 9 | type: 'FOO_PENDING' 10 | } 11 | ``` 12 | 13 | To change the default, supply an optional configuration object to the middleware with the `promiseTypeDelimiter` property. This property accepts a new string to use as the delimiter. 14 | 15 | ```js 16 | import { createPromise } from 'redux-promise-middleware'; 17 | 18 | applyMiddleware( 19 | createPromise({ 20 | promiseTypeDelimiter: '/' 21 | }) 22 | ) 23 | ``` 24 | 25 | With this configuration, given `FOO` async action, the type will be appended with a forward slash `/` delimiter. 26 | 27 | ```js 28 | { 29 | type: 'FOO/PENDING' 30 | } 31 | ``` 32 | 33 | Finally, if you are using a library like [type-to-reducer](https://github.com/tomatau/type-to-reducer), you'll also need to [configure it to handle the custom delimiter]](https://github.com/tomatau/type-to-reducer#custom-type-delimiter). 34 | -------------------------------------------------------------------------------- /docs/guides/custom-suffixes.md: -------------------------------------------------------------------------------- 1 | # Type Suffix Configuration 2 | 3 | In the case you need to use different type suffixes, you can configure this globally for all actions or locally (action-by-action). 4 | 5 | To change suffixes, you can supply an optional configuration object to the middleware. This object accepts an array of suffix strings that can be used instead of the default with a key of `promiseTypeSuffixes`. 6 | 7 | ```js 8 | import { createPromise } from 'redux-promise-middleware'; 9 | 10 | applyMiddleware( 11 | createPromise({ 12 | promiseTypeSuffixes: ['LOADING', 'SUCCESS', 'ERROR'] 13 | }) 14 | ) 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/guides/design-principles.md: -------------------------------------------------------------------------------- 1 | # Design Principles 2 | 3 | ## Promise Objects Are State Machines 4 | 5 | A promise object is a "machine" holding one of two states: 6 | 7 | 1. Pending 8 | 2. Settled 9 | 10 | A settled state is the deffered result of the promise. This state will be either (a) rejected or (b) resolved. The rejected state throws an error and the fulfilled state returns either null or a value. 11 | 12 | See more: [ECMAScript 25.4 Spec: Promise Objects](https://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects). 13 | 14 | ## Action Objects Describe State Changes to the Store 15 | 16 | An action object describes changes to the store. Actions are the only source of information for the store. 17 | 18 | See more: [Redux Documentation](http://redux.js.org/docs/basics/Actions.html). 19 | 20 | ## Asynchronous Action Objects Describe the Promise Object State 21 | 22 | Promise middleware dispatches "asynchronous" action objects describing the state of the promise: 23 | 24 | 1. Pending action 25 | 2. Fullfilled or rejcted action (settled) 26 | 27 | This affords asynchronous updates to the store. 28 | 29 | Another way of thinking of this is promise middleware abstracts the two states of an promise object to two action objects. 30 | 31 | ## Use Flux Standard Action (FSA) 32 | 33 | Promise middleware dispatches actions in compliance with [the Flux Standard Action](https://github.com/acdlite/flux-standard-action) reccommendations. 34 | -------------------------------------------------------------------------------- /docs/guides/dispatching-promises.md: -------------------------------------------------------------------------------- 1 | # Dispatching Promises 2 | 3 | ## Implicitly 4 | 5 | ```js 6 | const foo = () => ({ 7 | type: 'FOO', 8 | payload: new Promise() 9 | }); 10 | ``` 11 | 12 | ## Explicitly 13 | 14 | ```js 15 | const foo = () => ({ 16 | type: 'FOO', 17 | payload: { 18 | promise: new Promise() 19 | } 20 | }); 21 | ``` 22 | 23 | ## Async/Await 24 | 25 | For more on using async/await, [see the guide](async-await.md). 26 | 27 | ```js 28 | const foo = () => ({ 29 | type: 'FOO', 30 | async payload() { 31 | const data = await getDataFromApi(): 32 | 33 | return data; 34 | } 35 | }); 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/guides/null-values.md: -------------------------------------------------------------------------------- 1 | # Promises Resolved with Null Values 2 | 3 | If a promise is resolved with a `null` or `undefined` value, the fullfilled action will not include a payload property. This is because actions describe changes in state. Consider the following two actions: 4 | 5 | ``` 6 | // A 7 | { 8 | type: 'ACTION`, 9 | meta: ... 10 | } 11 | 12 | // B 13 | { 14 | type: 'ACTION' 15 | payload: null, 16 | meta: ... 17 | } 18 | ``` 19 | 20 | Both actions describe the same change in state. This is why, when you resolve with `null` or `undefined`, the payload property is not included. It would be redundant to include it. 21 | -------------------------------------------------------------------------------- /docs/guides/optimistic-updates.md: -------------------------------------------------------------------------------- 1 | # Optimistic Updates 2 | 3 | ## What are optimistic updates? 4 | 5 | > Optimistic [updates to a UI] don't wait for an operation to finish to update to the final state. They immediately switch to the final state, showing fake data for the time while the real operation is still in-progress. 6 | > - Igor Mandrigin, UX Planet 7 | 8 | ["Optimistic UI,"](https://uxplanet.org/optimistic-1000-34d9eefe4c05#.twmtjnmaw) a short article by UX Planet, is a great summary if you are unfamiliar with the practice. In short, it's the practice of updating the UI when a request is pending. This makes the experience more fluid for users. 9 | 10 | Because promise middleware dispatches a pending action, it is easy to optimistically update the Redux store. 11 | 12 | ## Code 13 | 14 | You may pass an optional `data` object. This is dispatched from the pending action and is useful for optimistic updates. 15 | 16 | ```js 17 | const foo = data => ({ 18 | type: 'FOO', 19 | payload: { 20 | promise: new Promise(), 21 | data: data 22 | } 23 | }); 24 | ``` 25 | 26 | Considering the action creator above, the pending action would be described as: 27 | 28 | ```js 29 | // pending action 30 | { 31 | type: 'FOO_PENDING', 32 | payload: data 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/guides/passing-extra-values-to-reducer.md: -------------------------------------------------------------------------------- 1 | # Passing extra values to reducer 2 | 3 | When returning an object from an action, only the payload and type attributes would be passed to the reducer. 4 | 5 | Consider this example: 6 | 7 | ``` 8 | { 9 | type: 'ACTION`, 10 | payload: new Promise(), 11 | extraValue: 123 12 | } 13 | ``` 14 | 15 | Here, reducer would not receive the `extraValue` property. 16 | In order to pass extra attributes, you need to include it as part of the `meta` attribute. 17 | 18 | 19 | ``` 20 | { 21 | type: 'ACTION`, 22 | payload: new Promise(), 23 | meta: {extraValue: 123} 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/guides/reducers.md: -------------------------------------------------------------------------------- 1 | # Use with Reducers 2 | 3 | Handling actions dispatched by Redux promise middleware is simple by default. 4 | 5 | ```js 6 | const FOO_TYPE = 'FOO'; 7 | 8 | // Dispatch the action 9 | const fooActionCreator = () => ({ 10 | type: FOO_TYPE 11 | payload: Promise.resolve('foo') 12 | }); 13 | 14 | // Handle the action 15 | const fooReducer = (state = {}, action) => { 16 | switch(action.type) { 17 | case `${FOO_TYPE}_PENDING`: 18 | return; 19 | 20 | case `${FOO_TYPE}_FULFILLED`: 21 | return { 22 | isFulfilled: true, 23 | data: action.payload 24 | }; 25 | 26 | case `${FOO_TYPE}_REJECTED`: 27 | return { 28 | isRejected: true, 29 | error: action.payload 30 | }; 31 | 32 | default: return state; 33 | } 34 | } 35 | ``` 36 | 37 | ### Action Types 38 | 39 | Optionally, the default promise suffixes can be imported from this module. 40 | 41 | ```js 42 | import { ActionType } from 'redux-promise-middleware'; 43 | ``` 44 | 45 | This can be useful in your reducers to ensure types are more robust. 46 | 47 | ```js 48 | const FOO_PENDING = `FOO_${ActionType.Pending}`; 49 | const FOO_FULFILLED = `FOO_${ActionType.Fulfilled}`; 50 | const FOO_REJECTED = `FOO_${ActionType.Rejected}`; 51 | ``` 52 | 53 | ## Large Applications 54 | 55 | In a large application with many async actions, having many reducers with this same structure can grow redundant. 56 | 57 | To keep your reducers [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), you might see value in using a solution like [type-to-reducer](https://github.com/tomatau/type-to-reducer). 58 | 59 | ```js 60 | import typeToReducer from 'type-to-reducer'; 61 | 62 | const BAR_TYPE = 'BAR'; 63 | 64 | // Dispatch the action 65 | const barActionCreator = () => ({ 66 | type: BAR_TYPE 67 | payload: Promise.resolve('bar') 68 | }); 69 | 70 | // Handle the action 71 | const barReducer = typeToReducer({ 72 | [BAR_TYPE]: { 73 | PENDING: () => ({ 74 | // ... 75 | }), 76 | REJECTED: (state, action) => ({ 77 | isRejected: true, 78 | error: action.payload 79 | }), 80 | FULFILLED: (state, action) => ({ 81 | isFulfilled: true, 82 | data: action.payload 83 | }) 84 | } 85 | }, {}); 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/guides/redux-actions.md: -------------------------------------------------------------------------------- 1 | # Use with Redux Actions 2 | 3 | To use this middleware with Redux Actions, return a promise from the payload creator. Note this example is experimental and not tested; it may not work as expected. 4 | 5 | ```js 6 | // Create an async action 7 | const fooAction = createAction('FOO', async () => { 8 | const { response } = await asyncFoo(); 9 | return response; 10 | }); 11 | 12 | // Use async action 13 | fooAction('123') 14 | ``` 15 | 16 | This would dispatch `FOO_PENDING` and `FOO_FULFILLED` with the data as the payload. 17 | -------------------------------------------------------------------------------- /docs/guides/redux-promise-middleware-actions.md: -------------------------------------------------------------------------------- 1 | # Use with Redux Promise Middleware Actions 2 | 3 | To use this middleware with [redux-promise-middleware-actions](https://github.com/omichelsen/redux-promise-middleware-actions), invoke `createAsyncAction` and return a promise from the payload creator. 4 | 5 | ```js 6 | import { createAsyncAction } from 'redux-promise-middleware-actions'; 7 | 8 | // Create an async action 9 | const fooAction = createAsyncAction('FOO', async (url) => { 10 | const response = await fetch(url); 11 | return response.json(); 12 | }); 13 | 14 | // Use async action 15 | dispatch(fooAction('https://some.url')); 16 | ``` 17 | 18 | This would dispatch `FOO_PENDING` and `FOO_FULFILLED` with the data as the payload. `fooAction` has a reference to these action creators on the object itself, e.g. `fooAction.pending()`. You can listen for these in the reducer like this: 19 | 20 | ```js 21 | const reducer = (state, action) => { 22 | switch (action.type) { 23 | case String(fooAction.pending): 24 | return { 25 | ...state, 26 | pending: true, 27 | }; 28 | case String(fooAction.fulfilled): 29 | return { 30 | ...state, 31 | data: action.payload, 32 | error: undefined, 33 | pending: false, 34 | }; 35 | case String(fooAction.rejected): 36 | return { 37 | ...state, 38 | error: action.payload, 39 | pending: false, 40 | }; 41 | default: 42 | return state; 43 | } 44 | }; 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/guides/rejected-promises.md: -------------------------------------------------------------------------------- 1 | # Catching Errors Thrown by Rejected Promises 2 | 3 | ## The Principle 4 | 5 | Redux promise middleware dispatches an action for a rejected promise, but does not catch the error thrown. **This is an expected behavior.** Because the error is not caught, you will (in most cases) get an "uncaught" warning in the developer console. Again, this is an expected behavior. 6 | 7 | By principle, it's your application's responsibility to catch the error thrown by the rejected promise. It's not the responsibility of the middleware. 8 | 9 | ## How to Catch Promises 10 | 11 | However, you probably want to catch the error. Here's some suggested approaches/solutions to this. 12 | 13 | 1. Catch/handle the error "globally" in error handling middleware 14 | 2. Catch/handle the error "locally" at the action creator 15 | 16 | ## Catching Errors Locally 17 | 18 | Generally, it'll make sense to use local error handling to directly control the "side effect(s)" of an error. 19 | 20 | This can be done by dispatching some specific action. Here's an example of handling an error locally at the action creator. 21 | 22 | ```js 23 | export function foo() { 24 | return dispatch => ({ 25 | type: 'FOO_ACTION', 26 | 27 | // Throw an error 28 | payload: new Promise(() => { 29 | throw new Error('foo'); 30 | }) 31 | 32 | // Catch the error locally 33 | }).catch(error => { 34 | console.log(error.message); // 'foo' 35 | 36 | // Dispatch a second action in response to the error 37 | dispatch(bar()); 38 | }); 39 | } 40 | ``` 41 | 42 | Please note this example requires [Redux Thunk](https://github.com/gaearon/redux-thunk). 43 | 44 | ## Catching Errors Globally 45 | 46 | In some cases, it might make sense to "globally" catch all errors or all errors of a certain action type. To give an example, you might want to show a alert modal whenever an error is thrown. 47 | 48 | [There is an example of how this middleware would work](https://github.com/pburtchaell/redux-promise-middleware/tree/master/examples/catching-errors-with-middleware/middleware.js). Note that any middleware you write will see all rejected promises before they're passed up to action creators for handling. 49 | 50 | ## The unhandledrejection Event 51 | 52 | A third option is to handle all rejected promises (not just promises used with Redux promise middleware) using an [`unhandledrejection`](https://developer.mozilla.org/en-US/docs/Web/Events/unhandledrejection) event. I wouldn't reccommend this because it assumes too much and could be difficult to debug, but there might be a case where it is useful for your program. 53 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Installation 4 | 5 | First, install the middleware. 6 | 7 | ``` 8 | npm i redux-promise-middleware -s 9 | ``` 10 | 11 | ## Setup 12 | 13 | Import the middleware and include it in `applyMiddleware` when creating the Redux store: 14 | 15 | ```js 16 | import promise from 'redux-promise-middleware' 17 | 18 | composeStoreWithMiddleware = applyMiddleware( 19 | promise, 20 | )(createStore) 21 | ``` 22 | 23 | ## Use 24 | 25 | Dispatch a promise as the value of the `payload` property of the action. 26 | 27 | ```js 28 | const foo = () => ({ 29 | type: 'FOO', 30 | payload: new Promise() 31 | }); 32 | ``` 33 | 34 | A pending action is immediately dispatched. 35 | 36 | ```js 37 | { 38 | type: 'FOO_PENDING' 39 | } 40 | ``` 41 | 42 | Once the promise is settled, a second action will be dispatched. If the promise is resolved a fulfilled action is dispatched. 43 | 44 | ```js 45 | { 46 | type: 'FOO_FULFILLED' 47 | payload: { 48 | ... 49 | } 50 | } 51 | ``` 52 | 53 | On the other hand, if the promise is rejected, a rejected action is dispatched. 54 | 55 | ```js 56 | { 57 | type: 'FOO_REJECTED' 58 | error: true, 59 | payload: { 60 | ... 61 | } 62 | } 63 | ``` 64 | 65 | That's it! 66 | 67 | ## Further Reading 68 | 69 | - [Catching Errors Thrown by Rejected Promises](guides/rejected-promises.md) 70 | - [Use with Reducers](guides/reducers.md) 71 | - [Optimistic Updates](guides/optimistic-updates.md) 72 | - [Design Principles](guides/design-principles.md) 73 | 74 | --- 75 | Copyright (c) 2017 Patrick Burtchaell. [Code licensed with the MIT License (MIT)](/LICENSE). [Documentation licensed with the CC BY-NC License](LICENSE). 76 | -------------------------------------------------------------------------------- /docs/upgrading/v4.md: -------------------------------------------------------------------------------- 1 | ## Upgrade from 3.x to 4.0.0 2 | 3 | This release introduces changes to error handling. 4 | 5 | Previously, the parameter of the rejected promise callback was both the dispatched action and an error object. The middleware also *always* constructed a new error object, which caused unexpected mutation and circular references. 6 | 7 | **Now, the parameter of the rejected promise callback is the value of `reject`.** The middleware does not construct a new error; it is your responsibility to make sure the promise is rejected with an Error object. 8 | 9 | ```js 10 | // before 11 | const bar = () => ({ 12 | type: 'FOO', 13 | payload: new Promise(() => { 14 | reject('foo'); 15 | }) 16 | });.then(() => null, ({ reason, action }) => { 17 | console.log(action.type): // => 'FOO' 18 | console.log(reason.message); // => 'foo' 19 | }); 20 | 21 | // after 22 | const bar = () => ({ 23 | type: 'FOO', 24 | payload: new Promise(() => { 25 | 26 | /** 27 | * Make sure the promise is rejected with an error. You 28 | * can also use `reject(new Error('foo'));`. It's a best 29 | * practice to reject a promise with an Error object. 30 | */ 31 | throw new Error('foo'); 32 | }) 33 | });.then(() => null, error => { 34 | console.log(error instanceof Error); // => true 35 | console.log(error.message); // => 'foo' 36 | }); 37 | ``` 38 | 39 | # 2.x to 3.0.0 40 | 41 | This release introduces some major changes to the functionality of the middleware: 42 | 43 | **First, the middleware returns a promise instead of the action.** 44 | 45 | ```js 46 | // before 47 | const foo = () => ({ 48 | type: 'FOO', 49 | payload: { 50 | promise: Promise.resolve('foo') 51 | } 52 | }); 53 | 54 | foo().action.promise.then(value => { 55 | console.log(value); // => 'foo' 56 | }); 57 | 58 | // after 59 | const bar = () => ({ 60 | type: 'BAR', 61 | payload: Promise.resolve('bar') 62 | }); 63 | 64 | bar().then(({ value }) => { 65 | console.log(value); // => 'bar' 66 | }); 67 | ``` 68 | 69 | **Second, a new promise is created** so `.then()` and `.catch()` work as expected. 70 | 71 | ``` js 72 | // before 73 | const foo = () => ({ 74 | type: 'FOO', 75 | payload: { 76 | promise: Promise.reject('foo') 77 | } 78 | }); 79 | 80 | foo().action.promise.then( 81 | value => { 82 | console.log(value); // => 'foo' 83 | }, 84 | reason => { 85 | // nothing happens 86 | } 87 | ); 88 | 89 | // after 90 | const bar = () => ({ 91 | type: 'BAR', 92 | payload: Promise.reject('bar') 93 | }); 94 | 95 | bar().then( 96 | ({ value }) => { 97 | // ... 98 | }, 99 | ({ reason }) => { 100 | console.log(reason); // => 'bar' 101 | } 102 | ); 103 | 104 | const baz = () => ({ 105 | type: 'BAZ', 106 | payload: new Promise((resolve, reject) => { 107 | throw 'baz' 108 | }) 109 | }); 110 | 111 | bar().catch(({ reason }) => { 112 | console.log(reason) // => 'baz' 113 | }); 114 | ``` 115 | 116 | **Third, promises can be explicitly or implicitly in the action object.** 117 | 118 | ```js 119 | // before 120 | const foo = () => ({ 121 | type: 'FOO', 122 | payload: { 123 | promise: Promise.resolve() 124 | } 125 | }); 126 | 127 | // after, with implicit promise as the value of the 'payload' property 128 | const bar = () => ({ 129 | type: 'BAR', 130 | payload: Promise.resolve() 131 | }); 132 | ``` 133 | 134 | Of course, if you prefer the explicit syntax, this still works. This syntax is also required for optimistic updates. 135 | 136 | ```js 137 | // after, but with explicit 'promise' property and 'data' property 138 | const bar = () => ({ 139 | type: 'BAZ', 140 | payload: { 141 | promise: Promise.resolve(), 142 | data: ... 143 | } 144 | }); 145 | ``` 146 | 147 | **Fourth, thunks are no longer bound to the promise.** If you are chaining actions with Redux Thunk, this is critical change. 148 | 149 | ```js 150 | // before, with Redux Thunk 151 | const foo = () => ({ 152 | type: 'FOO', 153 | payload: { 154 | promise: new Promise((resolve, reject) => { 155 | ... 156 | }).then( 157 | value => (action, dispatch) => { 158 | // handle fulfilled 159 | dispatch(someSuccessHandlerActionCreator()); 160 | }, 161 | reason => (action, dispatch) => { 162 | // handle rejected 163 | dispatch(someErrorHandlerActionCreator()); 164 | } 165 | ) 166 | } 167 | }); 168 | 169 | // after, with Redux Thunk 170 | const bar = () => { 171 | return (dispatch, getState) => { 172 | 173 | return dispatch({ 174 | type: 'FOO', 175 | payload: Promise.resolve('foo') 176 | }).then( 177 | ({ value, action }) => { 178 | console.log(value); // => 'foo' 179 | console.log(action.type); // => 'FOO_FULFILLED' 180 | dispatch(someSuccessHandlerActionCreator()); 181 | }, 182 | ({ reason, action }) => { 183 | // handle rejected 184 | dispatch(someErrorHandlerActionCreator()); 185 | } 186 | ); 187 | }; 188 | }; 189 | ``` -------------------------------------------------------------------------------- /docs/upgrading/v5.md: -------------------------------------------------------------------------------- 1 | # Upgrade from 4.x to 5.0.0 2 | 3 | Previously, the `promiseTypeSeparator` config property was used to change the character used to join type strings. 4 | 5 | Now, the `promiseTypeDelimiter` config property is used. Why? Because [delimiters](https://en.wikipedia.org/wiki/Delimiter) are one or more characters used to specify the boundaries in strings. It’s s delimiter, not a separator! 6 | 7 | ```js 8 | applyMiddleware( 9 | promiseMiddleware({ 10 | promiseTypeDelimiter: '/' 11 | }) 12 | ) 13 | ``` 14 | 15 | With this configuration, given `FOO` async action, the type will be appended with a forward slash `/` delimiter. 16 | 17 | ```js 18 | { 19 | type: 'FOO/PENDING' 20 | } 21 | ``` -------------------------------------------------------------------------------- /docs/upgrading/v6.md: -------------------------------------------------------------------------------- 1 | # Upgrade from 5.x to 6.0.0 2 | 3 | This version includes changes to the public API, making it easier to import and use the middleware. 4 | 5 | ## New: Preconfigured by Default 6 | 7 | ### Before 8 | 9 | Previously, the middleware need to be instantiated with an optional configuration. 10 | 11 | ```js 12 | import promiseMiddleware from 'redux-promise-middleware' 13 | 14 | applyMiddleware( 15 | promiseMiddleware({ 16 | // Optional configuration 17 | }), 18 | )(createStore) 19 | ``` 20 | 21 | This implementation enabled custom configuration, but, for most implementations, it is uncessary overhead. 22 | 23 | ### After 24 | 25 | Now, the default export is preconfigured and ready to go. 26 | 27 | ```js 28 | import promise from 'redux-promise-middleware' 29 | 30 | applyMiddleware( 31 | promise, 32 | )(createStore) 33 | ``` 34 | 35 | The middleware still supports custom configuration: import `createPromise` and pass in the configuration object. 36 | 37 | ```js 38 | import { createPromise } from 'redux-promise-middleware' 39 | 40 | applyMiddleware( 41 | createPromise({ 42 | // Custom configuration 43 | typeDelimiter: '/', 44 | }), 45 | )(createStore) 46 | ``` 47 | 48 | ## New: `ActionType` Export 49 | 50 | ### Before 51 | 52 | 53 | ```js 54 | import { PENDING, FULFILLED, REJECTED } from 'redux-promise-middleware' 55 | ``` 56 | 57 | Previously, the middleware exported three string constants. One each for the pending, fulfilled and rejected action types. This is useful for reducers, for example: 58 | 59 | ```js 60 | const reducer = (state = {}, action) => { 61 | switch(action.type) { 62 | case `FOO_${PENDING}`: 63 | // .. 64 | 65 | case `FOO_${FULFILLED}`: 66 | // ... 67 | 68 | default: return state; 69 | } 70 | } 71 | ``` 72 | 73 | This is a nice affordance, it could be better design if the action types were exported as an enum (E.g., an object, since this is just JavaScript), as opposed three individual strings. 74 | 75 | ### After 76 | 77 | ```js 78 | import { ActionType } from 'redux-promise-middleware' 79 | ``` 80 | 81 | Now, the action types are exported as one enum. 82 | 83 | ```js 84 | const reducer = (state = {}, action) => { 85 | switch(action.type) { 86 | case `FOO_${ActionType.Pending}`: 87 | // .. 88 | 89 | case `FOO_${ActionType.Fulfilled}`: 90 | // ... 91 | 92 | case `FOO_${ActionType.Rejected}`: 93 | // ... 94 | 95 | default: return state; 96 | } 97 | } 98 | ``` 99 | 100 | Using action types is entirely optional. One one hand, code benefits from more robust types and is less prone to static errors, but, on another hand, you and your team spends more time and effort writing the code. 101 | 102 | At the end of the day, you can also just use regular strings like any other action. 103 | 104 | 105 | ```js 106 | const reducer = (state = {}, action) => { 107 | switch(action.type) { 108 | case `FOO_PENDING`: 109 | // .. 110 | 111 | case `FOO_FULFILLED`: 112 | // ... 113 | 114 | case `FOO_REJECTED`: 115 | // ... 116 | 117 | default: return state; 118 | } 119 | } 120 | ``` 121 | -------------------------------------------------------------------------------- /examples/catching-errors-with-middleware/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | node_modules -------------------------------------------------------------------------------- /examples/catching-errors-with-middleware/README.md: -------------------------------------------------------------------------------- 1 | # Catching Errors with Middleware 2 | 3 | This example demonstrates how to catch a rejected promise. 4 | 5 | ## Getting Started 6 | 7 | - Clone this repository to your computer 8 | - Open the folder for this example 9 | - Run `npm i` to install dependencies 10 | - Run `npm start` to start the example 11 | - Open `http://localhost:1234` in a web browser 12 | -------------------------------------------------------------------------------- /examples/catching-errors-with-middleware/actions.js: -------------------------------------------------------------------------------- 1 | export const foo = () => ({ 2 | type: 'FOO', 3 | // When you throw an error, always instantiate a new Error object with `new Error()` 4 | payload: Promise.reject(new Error('foo')), 5 | }); 6 | 7 | export const bar = () => ({ 8 | type: 'BAR', 9 | payload: Promise.reject(new Error('bar')), 10 | }); 11 | -------------------------------------------------------------------------------- /examples/catching-errors-with-middleware/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Catching Errors with Middleware 5 |
6 | 7 | -------------------------------------------------------------------------------- /examples/catching-errors-with-middleware/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import * as actions from './actions'; 4 | import store from './store'; 5 | 6 | const App = () => ( 7 | <> 8 |
9 |

Catching Errors with Middleware

10 |

This example demonstrates how to catch a rejected promise with an error middleware.

11 |

Open the Developer Console to see logs for the dispatched actions.

12 |
13 |
14 |
15 | <> 16 |

Example 1: Foo

17 |

The "foo" action throws an error and the error is caught at the middleware.

18 |

You'll see a normal error message in the console.

19 | 22 | 23 | <> 24 |

Example 2: Bar

25 |

The "bar " action throws an error and is uncaught.

26 |

You'll see an "Uncaught (in promise) Error" message in the console.

27 | 30 | 31 |
32 | 33 | ); 34 | 35 | render(, document.querySelector('#mount')); 36 | -------------------------------------------------------------------------------- /examples/catching-errors-with-middleware/middleware.js: -------------------------------------------------------------------------------- 1 | import isPromise from 'is-promise'; 2 | import _ from 'underscore'; 3 | 4 | export default function errorMiddleware() { 5 | return next => action => { 6 | const types = { 7 | FOO: true, 8 | }; 9 | 10 | // If not a promise, continue on 11 | if (!isPromise(action.payload)) { 12 | return next(action); 13 | } 14 | 15 | /* 16 | * Another solution would would be to include a property in `meta` 17 | * and evaulate that property. 18 | * 19 | * if (action.meta.globalError === true) { 20 | * // handle error 21 | * } 22 | * 23 | * The error middleware serves to dispatch the initial pending promise to 24 | * the promise middleware, but adds a `catch`. 25 | */ 26 | if (_.has(types, action.type)) { 27 | 28 | // Dispatch initial pending promise, but catch any errors 29 | return next(action).catch(error => { 30 | console.warn(error); 31 | 32 | return error; 33 | }); 34 | } 35 | 36 | return next(action); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /examples/catching-errors-with-middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js", 3 | "scripts": { 4 | "start": "`npm bin`/parcel index.html", 5 | "test": "" 6 | }, 7 | "dependencies": { 8 | "is-promise": "^2.1.0", 9 | "react": "^16.4.1", 10 | "react-dom": "^16.4.1", 11 | "react-redux": "^5.0.7", 12 | "redux": "^4.0.0", 13 | "redux-logger": "^3.0.6", 14 | "underscore": "^1.9.1" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.7.4", 18 | "parcel-bundler": "^1.12.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/catching-errors-with-middleware/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import promise from '../../src'; 3 | import errorMiddleware from './middleware'; 4 | import { createLogger } from 'redux-logger'; 5 | 6 | const reducer = (state) => state; 7 | 8 | // Custom error middleware should go before the promise middleware 9 | const store = createStore(reducer, null, applyMiddleware( 10 | errorMiddleware, 11 | promise, 12 | createLogger({ collapsed: true }), 13 | )); 14 | 15 | export default store; 16 | -------------------------------------------------------------------------------- /examples/catching-errors/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | node_modules -------------------------------------------------------------------------------- /examples/catching-errors/README.md: -------------------------------------------------------------------------------- 1 | # Catching Errors 2 | 3 | This example demonstrates how to catch a rejected promise. 4 | 5 | ## Getting Started 6 | 7 | - Clone this repository to your computer 8 | - Open the folder for this example 9 | - Run `npm i` to install dependencies 10 | - Run `npm start` to start the example 11 | - Open `http://localhost:1234` in a web browser 12 | -------------------------------------------------------------------------------- /examples/catching-errors/actions.js: -------------------------------------------------------------------------------- 1 | export const foo = () => dispatch => dispatch({ 2 | type: 'FOO', 3 | // When you throw an error, always instantiate a new Error object with `new Error()` 4 | payload: Promise.reject(new Error('foo')), 5 | }); 6 | 7 | export const bar = () => dispatch => dispatch({ 8 | type: 'BAR', 9 | payload: Promise.reject(new Error('bar')), 10 | }); 11 | -------------------------------------------------------------------------------- /examples/catching-errors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Catching Errors 4 |
5 | 6 | -------------------------------------------------------------------------------- /examples/catching-errors/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import * as actions from './actions'; 4 | import store from './store'; 5 | 6 | class App extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | isPending: false, 12 | error: null, 13 | }; 14 | 15 | this.throwError = this.throwError.bind(this); 16 | } 17 | 18 | throwError() { 19 | const action = store.dispatch(actions.foo()); 20 | 21 | this.setState({ isPending: true }); 22 | 23 | action.catch(error => { 24 | this.setState({ 25 | isPending: false, 26 | error: error.message, 27 | }); 28 | }); 29 | } 30 | 31 | render() { 32 | const { isPending, error } = this.state; 33 | 34 | return ( 35 | <> 36 |
37 |

Catching Errors

38 |

This example demonstrates how to catch a rejected promise.

39 |
40 |
41 | 44 |
45 |
46 | Error caught: {`${typeof error === 'string'}`} 47 |
48 |
49 | Error Message: {`${error}`} 50 |
51 |
52 | 53 | ); 54 | } 55 | } 56 | 57 | render(, document.querySelector('#mount')); 58 | -------------------------------------------------------------------------------- /examples/catching-errors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js", 3 | "scripts": { 4 | "start": "`npm bin`/parcel index.html", 5 | "test": "" 6 | }, 7 | "dependencies": { 8 | "is-promise": "^2.1.0", 9 | "react": "^16.4.1", 10 | "react-dom": "^16.4.1", 11 | "react-redux": "^5.0.7", 12 | "redux": "^4.0.0", 13 | "redux-logger": "^3.0.6", 14 | "redux-thunk": "^2.3.0" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.7.4", 18 | "parcel-bundler": "^1.12.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/catching-errors/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import promise from '../../src'; 3 | import thunk from 'redux-thunk'; 4 | import { createLogger } from 'redux-logger'; 5 | 6 | const reducer = (state) => state; 7 | 8 | // Custom error middleware should go before the promise middleware 9 | const store = createStore(reducer, null, applyMiddleware( 10 | thunk, 11 | promise, 12 | createLogger({ collapsed: true }), 13 | )); 14 | 15 | export default store; 16 | -------------------------------------------------------------------------------- /examples/using-promise-actions/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | node_modules -------------------------------------------------------------------------------- /examples/using-promise-actions/README.md: -------------------------------------------------------------------------------- 1 | # Using Promise Middleware with redux-promise-middleware-actions 2 | 3 | This example demonstrates how to use the promise middleware with [redux-promise-middleware-actions](https://www.npmjs.com/package/redux-promise-middleware-actions). This provides a shorthand for creating asynchronous actions with a single function definition. 4 | 5 | Using the `createAsyncAction` action creator function you supply a function that returns a promise, and the middleware will take care of dispatching "pending", "fulfilled" and "rejected" actions: 6 | 7 | ```js 8 | import { createAsyncAction } from 'redux-promise-middleware-actions'; 9 | 10 | export const getDog = createAsyncAction('GET_DOG', () => ( 11 | fetch('https://dog.ceo/api/breeds/image/random') 12 | .then((response) => response.json()) 13 | )); 14 | 15 | dispatch(getDog()); // { type: 'GET_DOG_PENDING' } 16 | ``` 17 | 18 | The action creator function has the added benefit of strongly typed references to the action types so you don't have to maintain an enum list: 19 | 20 | ```js 21 | getDog.pending.toString(): // 'GET_DOG_PENDING' 22 | getDog.fulfilled.toString(): // 'GET_DOG_FULFILLED' 23 | getDog.rejected.toString(): // 'GET_DOG_REJECTED' 24 | ``` 25 | 26 | In the example, the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) is used to request an image of a dog from a JSON API (called the [Dog API](https://dog.ceo/dog-api/), woof woof!). When the request is pending, a loading message is rendered. When the request is fulfilled, the image is rendered. 27 | 28 | ## Getting Started 29 | 30 | - Clone this repository to your computer 31 | - Open the folder for this example 32 | - Run `npm i` to install dependencies 33 | - Run `npm start` to start the example 34 | - Open `http://localhost:1234` in a web browser 35 | -------------------------------------------------------------------------------- /examples/using-promise-actions/actions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import { createAsyncAction } from 'redux-promise-middleware-actions'; 3 | 4 | /* 5 | * Function: getDog 6 | * Description: Fetch an image of a dog from the [Dog API](https://dog.ceo/dog-api/) 7 | */ 8 | export const getDog = createAsyncAction('GET_DOG', () => ( 9 | fetch('https://dog.ceo/api/breeds/image/random') 10 | .then((response) => response.json()) 11 | )); 12 | -------------------------------------------------------------------------------- /examples/using-promise-actions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Using Promise Middleware with redux-promise-middleware-actions 5 |

Using Redux Promise Middleware

6 |

This example demonstrates how to use the middleware to fetch data a JSON API.

7 |
8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /examples/using-promise-actions/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import store from './store'; 3 | import { getDog } from './actions'; 4 | 5 | /* 6 | * Function: render 7 | * Description: Renders the given state to the given HTML DOM node 8 | */ 9 | const render = (mount, state) => { 10 | if (state.pending) { 11 | mount.innerHTML = 'Loading...'; 12 | } else if (state.data) { 13 | mount.innerHTML = ``; 14 | } 15 | }; 16 | 17 | /* 18 | * Function: initializes 19 | * Description: Renders the initial state of the example 20 | */ 21 | const initialize = () => { 22 | const mount = document.querySelector('#mount'); 23 | 24 | // Load the post when button is clicked 25 | const button = document.querySelector('#load'); 26 | button.addEventListener('click', () => store.dispatch(getDog())); 27 | 28 | render(mount, {}); 29 | store.subscribe(() => render(mount, store.getState())); 30 | }; 31 | 32 | initialize(); 33 | -------------------------------------------------------------------------------- /examples/using-promise-actions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js", 3 | "scripts": { 4 | "start": "`npm bin`/parcel index.html" 5 | }, 6 | "dependencies": { 7 | "redux": "^4.0.0", 8 | "redux-logger": "^3.0.6", 9 | "redux-promise-middleware-actions": "^1.0.0" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.7.4", 13 | "parcel-bundler": "^1.12.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/using-promise-actions/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { createLogger } from 'redux-logger'; 3 | import { asyncReducer } from 'redux-promise-middleware-actions'; 4 | import promise from '../../src/index'; 5 | import { getDog } from './actions'; 6 | 7 | // Use the built-in reducer which will create a state 8 | // for the response of `getDog()` with this shape: 9 | // { 10 | // data: , 11 | // pending: true | false, 12 | // error: 13 | // } 14 | const reducer = asyncReducer(getDog); 15 | 16 | const store = createStore(reducer, {}, applyMiddleware( 17 | promise, 18 | createLogger({ collapsed: true }), 19 | )); 20 | 21 | export default store; 22 | -------------------------------------------------------------------------------- /examples/using-promise-all/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | node_modules -------------------------------------------------------------------------------- /examples/using-promise-all/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Function: getDog 3 | * Description: Fetch an image of a dog from the [Dog API](https://dog.ceo/dog-api/) 4 | */ 5 | export const getFirstDog = () => ({ 6 | type: 'GET_FIRST_DOG', 7 | payload: fetch('https://dog.ceo/api/breeds/image/random') 8 | .then(response => response.json()) 9 | .then(json => ({ image: json.message, ...json })), 10 | }); 11 | 12 | /* 13 | * Function: getAnotherDog 14 | * Description: Fetch another dog 15 | */ 16 | export const getAnotherDog = () => ({ 17 | type: 'GET_ANOTHER_DOG', 18 | payload: fetch('https://dog.ceo/api/breeds/image/random') 19 | .then(response => response.json()) 20 | .then(json => ({ image: json.message, ...json })), 21 | }); 22 | 23 | /* 24 | * Function: getFinalDog 25 | * Description: Fetch a final dog 26 | */ 27 | export const getFinalDog = () => ({ 28 | type: 'GET_FINAL_DOG', 29 | payload: fetch('https://dog.ceo/api/breeds/image/random') 30 | .then(response => response.json()) 31 | .then(json => ({ image: json.message, ...json })), 32 | }); 33 | 34 | /* 35 | * Function: getAnimals 36 | * Description: Fetch all the animal images 37 | */ 38 | export const getAnimals = (actions) => dispatch => dispatch({ 39 | type: 'GET_ANIMALS', 40 | 41 | /* 42 | * Promise.all accepts an array of promises. Here, we remap the given array of actions 43 | * to an array of promises by dispatching each action. 44 | */ 45 | payload: Promise.all(actions.map((action) => dispatch(action()))), 46 | }); 47 | -------------------------------------------------------------------------------- /examples/using-promise-all/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Using Promise#all 5 |

Using Promise#all

6 |

This example demonstrates how to use the Promise#all method to resolve multiple async actions.

7 |
8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /examples/using-promise-all/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import * as actions from './actions'; 3 | import store from './store'; 4 | 5 | const render = (mount, state) => { 6 | if (state.images) { 7 | mount.innerHTML = state.images.reduce((html, image) => `${html} `, ''); 8 | } 9 | }; 10 | 11 | const initialize = () => { 12 | const mount = document.querySelector('#mount'); 13 | 14 | // Load the post when button is clicked 15 | const button = document.querySelector('#load'); 16 | button.addEventListener('click', () => { 17 | store.dispatch(actions.getAnimals([ 18 | actions.getFirstDog, 19 | actions.getAnotherDog, 20 | actions.getFinalDog, 21 | ])); 22 | }); 23 | 24 | render(mount, {}); 25 | store.subscribe(() => render(mount, store.getState())); 26 | }; 27 | 28 | initialize(); 29 | -------------------------------------------------------------------------------- /examples/using-promise-all/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js", 3 | "scripts": { 4 | "start": "`npm bin`/parcel index.html" 5 | }, 6 | "dependencies": { 7 | "redux": "^4.0.0", 8 | "redux-logger": "^3.0.6", 9 | "redux-thunk": "^2.3.0" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.7.4", 13 | "parcel-bundler": "^1.12.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/using-promise-all/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { createLogger } from 'redux-logger'; 4 | import promise from '../../src/index'; 5 | 6 | const defaultState = { 7 | images: [], 8 | }; 9 | 10 | const reducer = (state = defaultState, action) => { 11 | if (action.payload && action.payload.image) { 12 | const { image } = action.payload; 13 | 14 | return { 15 | images: Array.isArray(state.images) ? [...state.images, image] : [image], 16 | }; 17 | } 18 | 19 | return state; 20 | }; 21 | 22 | const store = createStore(reducer, {}, applyMiddleware( 23 | thunk, 24 | promise, 25 | createLogger({ collapsed: true }), 26 | )); 27 | 28 | export default store; 29 | -------------------------------------------------------------------------------- /examples/using-promise-middleware/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | node_modules -------------------------------------------------------------------------------- /examples/using-promise-middleware/README.md: -------------------------------------------------------------------------------- 1 | # Using Promise Middleware 2 | 3 | This example demonstrates how to use the middleware. 4 | 5 | In the example, the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) is used to request an image of a dog from a JSON API (called the [Dog API](https://dog.ceo/dog-api/), woof woof!). When the request is pending, a loading message is rendered. When the request is fulfilled, the image is rendered. 6 | 7 | ## Getting Started 8 | 9 | - Clone this repository to your computer 10 | - Open the folder for this example 11 | - Run `npm i` to install dependencies 12 | - Run `npm start` to start the example 13 | - Open `http://localhost:1234` in a web browser 14 | -------------------------------------------------------------------------------- /examples/using-promise-middleware/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Using Redux Promise Middleware 5 |

Using Redux Promise Middleware

6 |

This example demonstrates how to use the middleware to fetch data a JSON API.

7 |
8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /examples/using-promise-middleware/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import store from './store'; 3 | 4 | /* 5 | * Function: getDog 6 | * Description: Fetch an image of a dog from the [Dog API](https://dog.ceo/dog-api/) 7 | */ 8 | const getDog = () => ({ 9 | type: 'GET_DOG', 10 | payload: fetch('https://dog.ceo/api/breeds/image/random') 11 | .then(response => response.json()), 12 | }); 13 | 14 | /* 15 | * Function: render 16 | * Description: Renders the given state to the given HTML DOM node 17 | */ 18 | const render = (mount, state) => { 19 | if (state.isPending) { 20 | mount.innerHTML = 'Loading...'; 21 | } else if (state.image) { 22 | mount.innerHTML = ``; 23 | } 24 | }; 25 | 26 | /* 27 | * Function: initializes 28 | * Description: Renders the initial state of the example 29 | */ 30 | const initialize = () => { 31 | const mount = document.querySelector('#mount'); 32 | 33 | // Load the post when button is clicked 34 | const button = document.querySelector('#load'); 35 | button.addEventListener('click', () => store.dispatch(getDog())); 36 | 37 | render(mount, {}); 38 | store.subscribe(() => render(mount, store.getState())); 39 | }; 40 | 41 | initialize(); 42 | -------------------------------------------------------------------------------- /examples/using-promise-middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js", 3 | "scripts": { 4 | "start": "`npm bin`/parcel index.html" 5 | }, 6 | "dependencies": { 7 | "redux": "^4.0.0", 8 | "redux-logger": "^3.0.6" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "^7.7.4", 12 | "parcel-bundler": "^1.12.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/using-promise-middleware/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import promise from '../../src/index'; 3 | import { createLogger } from 'redux-logger'; 4 | 5 | const defaultState = { 6 | isPending: true, 7 | image: null, 8 | }; 9 | 10 | const reducer = (state = defaultState, action) => { 11 | switch (action.type) { 12 | case 'GET_DOG_PENDING': return defaultState; 13 | 14 | case 'GET_DOG_FULFILLED': 15 | return { 16 | isPending: false, 17 | image: action.payload.message, 18 | }; 19 | 20 | default: return state; 21 | } 22 | }; 23 | 24 | const store = createStore(reducer, {}, applyMiddleware( 25 | promise, 26 | createLogger({ collapsed: true }), 27 | )); 28 | 29 | export default store; 30 | -------------------------------------------------------------------------------- /examples/using-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | node_modules -------------------------------------------------------------------------------- /examples/using-typescript/README.md: -------------------------------------------------------------------------------- 1 | # Using Promise Middleware with TypeScript 2 | 3 | This example demonstrates how to use the middleware with TypeScript. 4 | 5 | ## Getting Started 6 | 7 | - Clone this repository to your computer 8 | - Open the folder for this example 9 | - Run `npm i` to install dependencies 10 | - Run `npm start` to start the example 11 | - Open `http://localhost:1234` in a web browser 12 | -------------------------------------------------------------------------------- /examples/using-typescript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Using Redux Promise Middleware 5 |

Using Redux Promise Middleware

6 |

This example demonstrates how to use the middleware to fetch data a JSON API.

7 |
8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /examples/using-typescript/index.ts: -------------------------------------------------------------------------------- 1 | import { AsyncAction } from 'redux-promise-middleware'; 2 | import store, { State } from './store'; 3 | 4 | /** 5 | * @private 6 | * Fetch an image of a dog from the [Dog API](https://dog.ceo/dog-api/) 7 | */ 8 | const getDog = (): AsyncAction => ({ 9 | type: 'GET_DOG', 10 | payload: fetch('https://dog.ceo/api/breeds/image/random') 11 | .then(response => response.json()), 12 | }); 13 | 14 | /** 15 | * @private 16 | * Renders the given state to the given HTML DOM node 17 | */ 18 | const render = (mount: HTMLElement | null, state: State) => { 19 | if (mount && state.isPending) { 20 | mount.innerHTML = 'Loading...'; 21 | } else if (mount && state.image) { 22 | mount.innerHTML = ``; 23 | } 24 | }; 25 | 26 | /** 27 | * @private 28 | * Renders the initial state of the example 29 | */ 30 | const initialize = () => { 31 | const mount: HTMLElement | null = document.querySelector('#mount'); 32 | 33 | // Load the post when button is clicked 34 | const button: HTMLElement | null = document.querySelector('#load'); 35 | if (button) button.addEventListener('click', () => store.dispatch(getDog())); 36 | 37 | render(mount, store.getState()); 38 | store.subscribe(() => render(mount, store.getState())); 39 | }; 40 | 41 | initialize(); 42 | -------------------------------------------------------------------------------- /examples/using-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js", 3 | "scripts": { 4 | "start": "`npm bin`/parcel index.html" 5 | }, 6 | "dependencies": { 7 | "redux": "^4.0.1", 8 | "redux-logger": "^3.0.6", 9 | "redux-promise-middleware": "^5.1.1" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.7.4", 13 | "parcel-bundler": "^1.12.4", 14 | "typescript": "^3.2.2" 15 | }, 16 | "alias": { 17 | "redux-promise-middleware": "../.." 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/using-typescript/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore, Reducer, Store, applyMiddleware } from 'redux'; 2 | import promise, { FluxStandardAction } from 'redux-promise-middleware'; 3 | 4 | export interface State { 5 | isPending: boolean; 6 | image?: string; 7 | } 8 | 9 | const defaultState = { 10 | isPending: true, 11 | }; 12 | 13 | const defaultReducer: Reducer = (state: State = defaultState, action: FluxStandardAction) => { 14 | switch (action.type) { 15 | case 'GET_DOG_FULFILLED': 16 | return { 17 | isPending: false, 18 | image: action.payload.message, 19 | }; 20 | 21 | default: return state; 22 | } 23 | }; 24 | 25 | const store: Store = createStore(defaultReducer, {}, applyMiddleware(promise)); 26 | 27 | export default store; 28 | -------------------------------------------------------------------------------- /examples/using-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "newLine": "LF", 6 | "outDir": "./lib/", 7 | "target": "es5", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "jsx": "preserve", 11 | "lib": [ 12 | "es2017", 13 | "dom" 14 | ], 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": [ 22 | "index.ts", 23 | "store.ts" 24 | ], 25 | "exclude": [ 26 | ".git", 27 | "node_modules" 28 | ] 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-promise-middleware", 3 | "description": "Enables simple, yet robust handling of async action creators in Redux", 4 | "version": "6.2.0", 5 | "main": "dist/index.js", 6 | "types": "src/index.d.ts", 7 | "module": "dist/es/index.js", 8 | "scripts": { 9 | "prepare": "npm run build", 10 | "start": "babel-node examples/server.js", 11 | "build-es": "BABEL_ENV=es babel src -d dist/es", 12 | "build-commonjs": "babel src -d dist", 13 | "build-umd": "webpack --output-filename umd/redux-promise-middleware.js", 14 | "build-umd-min": "NODE_ENV=production webpack --output-filename umd/redux-promise-middleware.min.js", 15 | "build": "rm -rf dist & npm run build-commonjs & npm run build-es & npm run build-umd & npm run build-umd-min", 16 | "prebuild": "npm run test", 17 | "test": "jest", 18 | "lint": "eslint src/**/*.js test/**/*.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/pburtchaell/redux-promise-middleware.git" 23 | }, 24 | "keywords": [ 25 | "redux", 26 | "middleware", 27 | "middlewares", 28 | "promise", 29 | "promises", 30 | "optimistic update", 31 | "optimistic updates", 32 | "async", 33 | "async functions" 34 | ], 35 | "author": "Patrick Burtchaell (pburtchaell.com)", 36 | "contributors": [ 37 | "Thomas (tomatao.co.uk)" 38 | ], 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/pburtchaell/redux-promise-middleware/issues" 42 | }, 43 | "homepage": "https://github.com/pburtchaell/redux-promise-middleware", 44 | "devDependencies": { 45 | "@babel/cli": "^7.7.0", 46 | "@babel/core": "^7.7.4", 47 | "@babel/preset-env": "^7.7.1", 48 | "@babel/preset-react": "^7.7.0", 49 | "babel-eslint": "^10.0.3", 50 | "babel-loader": "^8.0.6", 51 | "bluebird": "^3.5.0", 52 | "coveralls": "^3.0.8", 53 | "eslint": "^6.7.0", 54 | "eslint-config-airbnb": "^18.0.1", 55 | "eslint-plugin-import": "^2.18.2", 56 | "eslint-plugin-jsx-a11y": "^6.2.3", 57 | "eslint-plugin-react": "^7.14.3", 58 | "eslint-plugin-react-hooks": "^1.7.0", 59 | "istanbul": "^1.0.0-alpha.2", 60 | "jest": "^24.9.0", 61 | "redux": "5.0.0", 62 | "webpack": "^4.41.2", 63 | "webpack-cli": "^3.3.10" 64 | }, 65 | "peerDependencies": { 66 | "redux": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" 67 | }, 68 | "jest": { 69 | "verbose": true, 70 | "moduleNameMapper": { 71 | "redux-promise-middleware": "/src/index.js" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Redux Promise Middleware version 6.0.0 2 | // Project: Redux Promise Middleware 3 | // Definitions by: Patrick Burtchaell 4 | import { Middleware, Action as CoreReduxAction } from 'redux'; 5 | 6 | export declare enum ActionType { 7 | Pending = 'PENDING', 8 | Fulfilled = 'FULFILLED', 9 | Rejected = 'REJECTED', 10 | } 11 | 12 | // Action type types 13 | declare type PendingActionType = ActionType.Pending | string; 14 | declare type FulfilledActionType = ActionType.Fulfilled | string; 15 | declare type RejectedActionType = ActionType.Rejected | string; 16 | 17 | // Action payload types 18 | declare type AsyncFunction<> = () => Promise; 19 | declare type AsyncPayload = Promise | AsyncFunction | { 20 | promise: Promise | AsyncFunction; 21 | data?: any; 22 | }; 23 | 24 | // Flux standard action type 25 | export declare interface FluxStandardAction extends CoreReduxAction { 26 | payload?: any; 27 | meta?: any; 28 | error?: boolean; 29 | } 30 | 31 | // Async action type 32 | export declare interface AsyncAction extends FluxStandardAction { 33 | payload?: AsyncPayload; 34 | } 35 | 36 | export declare interface Config { 37 | promiseTypeSuffixes?: [PendingActionType, FulfilledActionType, RejectedActionType]; 38 | promiseTypeDelimiter?: string; 39 | } 40 | 41 | export declare function createPromise(config?: Config): Middleware; 42 | 43 | declare const promise: Middleware; 44 | export default promise; 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import isPromise from './isPromise.js'; 2 | 3 | /** 4 | * For TypeScript support: Remember to check and make sure 5 | * that `index.d.ts` is also up to date with the implementation. 6 | */ 7 | export const ActionType = { 8 | Pending: 'PENDING', 9 | Fulfilled: 'FULFILLED', 10 | Rejected: 'REJECTED', 11 | }; 12 | 13 | /** 14 | * Function: createPromise 15 | * Description: The main createPromise accepts a configuration 16 | * object and returns the middleware. 17 | */ 18 | export function createPromise(config = {}) { 19 | const defaultTypes = [ActionType.Pending, ActionType.Fulfilled, ActionType.Rejected]; 20 | const PROMISE_TYPE_SUFFIXES = config.promiseTypeSuffixes || defaultTypes; 21 | const PROMISE_TYPE_DELIMITER = ( 22 | config.promiseTypeDelimiter === undefined ? '_' : config.promiseTypeDelimiter 23 | ); 24 | 25 | return ref => { 26 | const { dispatch } = ref; 27 | 28 | return next => action => { 29 | 30 | /** 31 | * Instantiate variables to hold: 32 | * (1) the promise 33 | * (2) the data for optimistic updates 34 | */ 35 | let promise; 36 | let data; 37 | 38 | /** 39 | * There are multiple ways to dispatch a promise. The first step is to 40 | * determine if the promise is defined: 41 | * (a) explicitly (action.payload.promise is the promise) 42 | * (b) implicitly (action.payload is the promise) 43 | * (c) as an async function (returns a promise when called) 44 | * 45 | * If the promise is not defined in one of these three ways, we don't do 46 | * anything and move on to the next middleware in the middleware chain. 47 | */ 48 | 49 | // Step 1a: Is there a payload? 50 | if (action.payload) { 51 | const PAYLOAD = action.payload; 52 | 53 | // Step 1.1: Is the promise implicitly defined? 54 | if (isPromise(PAYLOAD)) { 55 | promise = PAYLOAD; 56 | } 57 | 58 | // Step 1.2: Is the promise explicitly defined? 59 | else if (isPromise(PAYLOAD.promise)) { 60 | promise = PAYLOAD.promise; 61 | data = PAYLOAD.data; 62 | } 63 | 64 | // Step 1.3: Is the promise returned by an async function? 65 | else if ( 66 | typeof PAYLOAD === 'function' 67 | || typeof PAYLOAD.promise === 'function' 68 | ) { 69 | promise = PAYLOAD.promise ? PAYLOAD.promise() : PAYLOAD(); 70 | data = PAYLOAD.promise ? PAYLOAD.data : undefined; 71 | 72 | // Step 1.3.1: Is the return of action.payload a promise? 73 | if (!isPromise(promise)) { 74 | 75 | // If not, move on to the next middleware. 76 | return next({ 77 | ...action, 78 | payload: promise 79 | }); 80 | } 81 | } 82 | 83 | // Step 1.4: If there's no promise, move on to the next middleware. 84 | else { 85 | return next(action); 86 | } 87 | 88 | // Step 1b: If there's no payload, move on to the next middleware. 89 | } else { 90 | return next(action); 91 | } 92 | 93 | /** 94 | * Instantiate and define constants for: 95 | * (1) the action type 96 | * (2) the action meta 97 | */ 98 | const TYPE = action.type; 99 | const META = action.meta; 100 | 101 | /** 102 | * Instantiate and define constants for the action type suffixes. 103 | * These are appended to the end of the action type. 104 | */ 105 | const [ 106 | PENDING, 107 | FULFILLED, 108 | REJECTED 109 | ] = PROMISE_TYPE_SUFFIXES; 110 | 111 | /** 112 | * Function: getAction 113 | * Description: This function constructs and returns a rejected 114 | * or fulfilled action object. The action object is based off the Flux 115 | * Standard Action (FSA). 116 | * 117 | * Given an original action with the type FOO: 118 | * 119 | * The rejected object model will be: 120 | * { 121 | * error: true, 122 | * type: 'FOO_REJECTED', 123 | * payload: ..., 124 | * meta: ... (optional) 125 | * } 126 | * 127 | * The fulfilled object model will be: 128 | * { 129 | * type: 'FOO_FULFILLED', 130 | * payload: ..., 131 | * meta: ... (optional) 132 | * } 133 | */ 134 | const getAction = (newPayload, isRejected) => ({ 135 | // Concatenate the type string property. 136 | type: [ 137 | TYPE, 138 | isRejected ? REJECTED : FULFILLED 139 | ].join(PROMISE_TYPE_DELIMITER), 140 | 141 | // Include the payload property. 142 | ...((newPayload === null || typeof newPayload === 'undefined') ? {} : { 143 | payload: newPayload 144 | }), 145 | 146 | // If the original action includes a meta property, include it. 147 | ...(META !== undefined ? { meta: META } : {}), 148 | 149 | // If the action is rejected, include an error property. 150 | ...(isRejected ? { 151 | error: true 152 | } : {}) 153 | }); 154 | 155 | /** 156 | * Function: handleReject 157 | * Calls: getAction to construct the rejected action 158 | * Description: This function dispatches the rejected action and returns 159 | * the original Error object. Please note the developer is responsible 160 | * for constructing and throwing an Error object. The middleware does not 161 | * construct any Errors. 162 | */ 163 | const handleReject = reason => { 164 | const rejectedAction = getAction(reason, true); 165 | dispatch(rejectedAction); 166 | 167 | throw reason; 168 | }; 169 | 170 | /** 171 | * Function: handleFulfill 172 | * Calls: getAction to construct the fullfilled action 173 | * Description: This function dispatches the fulfilled action and 174 | * returns the success object. The success object should 175 | * contain the value and the dispatched action. 176 | */ 177 | const handleFulfill = (value = null) => { 178 | const resolvedAction = getAction(value, false); 179 | dispatch(resolvedAction); 180 | 181 | return { value, action: resolvedAction }; 182 | }; 183 | 184 | /** 185 | * First, dispatch the pending action: 186 | * This object describes the pending state of a promise and will include 187 | * any data (for optimistic updates) and/or meta from the original action. 188 | */ 189 | next({ 190 | // Concatenate the type string. 191 | type: [TYPE, PENDING].join(PROMISE_TYPE_DELIMITER), 192 | 193 | // Include payload (for optimistic updates) if it is defined. 194 | ...(data !== undefined ? { payload: data } : {}), 195 | 196 | // Include meta data if it is defined. 197 | ...(META !== undefined ? { meta: META } : {}) 198 | }); 199 | 200 | /** 201 | * Second, dispatch a rejected or fulfilled action and move on to the 202 | * next middleware. 203 | */ 204 | return promise.then(handleFulfill, handleReject); 205 | }; 206 | }; 207 | } 208 | 209 | export default function middleware({ dispatch } = {}) { 210 | if (typeof dispatch === 'function') { 211 | return createPromise()({ dispatch }); 212 | } 213 | 214 | if (process && process.env && process.env.NODE_ENV !== 'production') { 215 | // eslint-disable-next-line no-console 216 | console.warn(`Redux Promise Middleware: As of version 6.0.0, the \ 217 | middleware library supports both preconfigured and custom configured \ 218 | middleware. To use a custom configuration, use the "createPromise" export \ 219 | and call this function with your configuration property. To use a \ 220 | preconfiguration, use the default export. For more help, check the upgrading \ 221 | guide: https://docs.psb.design/redux-promise-middleware/upgrade-guides/v6 222 | 223 | For custom configuration: 224 | import { createPromise } from 'redux-promise-middleware'; 225 | const promise = createPromise({ promiseTypeSuffixes: ['LOADING', 'SUCCESS', 'ERROR'] }); 226 | applyMiddleware(promise); 227 | 228 | For preconfiguration: 229 | import promise from 'redux-promise-middleware'; 230 | applyMiddleware(promise); 231 | `); 232 | } 233 | 234 | return null; 235 | } 236 | -------------------------------------------------------------------------------- /src/isPromise.js: -------------------------------------------------------------------------------- 1 | export default function isPromise(value) { 2 | if (value !== null && typeof value === 'object') { 3 | return value && typeof value.then === 'function'; 4 | } 5 | 6 | return false; 7 | } 8 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "expect": true, 4 | "jest": true 5 | } 6 | } -------------------------------------------------------------------------------- /test/actions.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | beforeEach(() => { store = createStore(); }); 7 | 8 | test('dispatches sync actions with no mutations', () => { 9 | const { dispatch, lastSpy } = store; 10 | 11 | const dispatched = getActionCreator(types.DEFAULT)(); 12 | const expected = getActionCreator(types.DEFAULT)(); 13 | 14 | dispatch(dispatched); 15 | 16 | expect(lastSpy.mock.calls[0]).toEqual([expected]); 17 | expect(lastSpy.mock.calls.length).toEqual(1); 18 | }); 19 | -------------------------------------------------------------------------------- /test/async-functions.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | beforeEach(() => { store = createStore(); }); 7 | 8 | test('actions dispatched for given async function - payload', async () => { 9 | const { dispatch, lastSpy } = store; 10 | 11 | const dispatched = getActionCreator(types.ASYNC_FUNCTION_WILL_RESOLVE)(); 12 | 13 | const action = dispatch(dispatched); 14 | 15 | return action.then(() => { 16 | expect(lastSpy.mock.calls[0]).toEqual([getActionCreator(types.PENDING)()]); 17 | expect(lastSpy.mock.calls[1]).toEqual([getActionCreator(types.FULFILLED)()]); 18 | }); 19 | }); 20 | 21 | test('actions dispatched for given async function - payload.promise', () => { 22 | const { dispatch, lastSpy } = store; 23 | 24 | const dispatched = getActionCreator(types.ASYNC_FUNCTION_PROMISE_FIELD)(); 25 | 26 | const action = dispatch(dispatched); 27 | 28 | return action.then(() => { 29 | expect(lastSpy.mock.calls[0]).toEqual([getActionCreator(types.PENDING)()]); 30 | expect(lastSpy.mock.calls[1]).toEqual([getActionCreator(types.FULFILLED)()]); 31 | }); 32 | }); 33 | 34 | test('pending action contains given optimistic update', async () => { 35 | const { dispatch, lastSpy } = store; 36 | 37 | const dispatched = getActionCreator(types.ASYNC_FUNCTION_OPTIMISTIC_UPDATE)(); 38 | 39 | const action = dispatch(dispatched); 40 | 41 | return action.then(() => { 42 | expect(lastSpy.mock.calls[0]).toEqual([getActionCreator(types.PENDING_OPTIMISTIC_UPDATE)()]); 43 | expect(lastSpy.mock.calls[1]).toEqual([getActionCreator(types.FULFILLED)()]); 44 | }); 45 | }); 46 | 47 | test('rejected action dispatched for given rejected async function', async () => { 48 | const { dispatch, lastSpy } = store; 49 | 50 | const dispatched = getActionCreator(types.ASYNC_FUNCTION_WILL_REJECT)(); 51 | 52 | const promiseSpy = jest.fn(); 53 | const action = dispatch(dispatched); 54 | 55 | return action 56 | .then(promiseSpy) 57 | .catch(() => { 58 | expect(lastSpy.mock.calls[0]).toEqual([getActionCreator(types.PENDING)()]); 59 | expect(lastSpy.mock.calls[1]).toEqual([getActionCreator(types.REJECTED)()]); 60 | }) 61 | .then(() => { 62 | expect(promiseSpy.mock.calls.length).toBe(0); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/bluebird.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | /* 5 | * This test ensures the original promise object is not mutated. In the case 6 | * a promise library is used, adding methods to the promise class, the 7 | * middleware should not remove those methods. 8 | */ 9 | test('original promise is propagated', () => { 10 | const { dispatch, lastSpy } = createStore(); 11 | 12 | const dispatched = getActionCreator(types.BLUEBIRD)(); 13 | 14 | const action = dispatch(dispatched); 15 | 16 | // Expect the promise returned has orginal methods available 17 | expect(action.any).toBeDefined(); 18 | 19 | // Expect actions are dispatched like usual 20 | return action.catch(() => { 21 | expect(lastSpy.mock.calls[0]).toEqual([getActionCreator(types.PENDING)()]); 22 | expect(lastSpy.mock.calls[1]).toEqual([getActionCreator(types.FULFILLED)()]); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/delimiter.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | // The middleware should use '_' as delimiter by default 7 | test('actions dispatched with default delimiter', (done) => { 8 | store = createStore(); 9 | 10 | const dispatched = getActionCreator(types.WILL_RESOLVE)(); 11 | const expected = getActionCreator(types.FULFILLED)(); 12 | 13 | store.dispatch(dispatched).then(({ value, action }) => { 14 | expect(value).toEqual(expected.payload); 15 | expect(action).toEqual(expected); 16 | done(); 17 | }); 18 | }); 19 | 20 | // The middleware should allow global custom action type delimiter 21 | test('actions dispatched with custom delimiter', (done) => { 22 | store = createStore({ promiseTypeDelimiter: '/' }); 23 | 24 | const dispatched = getActionCreator(types.WILL_RESOLVE)(); 25 | 26 | const expected = { 27 | ...getActionCreator(types.FULFILLED)(), 28 | type: `${dispatched.type}/FULFILLED`, 29 | }; 30 | 31 | return store.dispatch(dispatched).then(({ value, action }) => { 32 | expect(value).toEqual(expected.payload); 33 | expect(action).toEqual(expected); 34 | done(); 35 | }); 36 | }); 37 | 38 | // The middleware should allow empty string delimiter 39 | test('actions dispatched with custom delimiter', (done) => { 40 | store = createStore({ promiseTypeDelimiter: '' }); 41 | 42 | const dispatched = getActionCreator(types.WILL_RESOLVE)(); 43 | 44 | const expected = { 45 | ...getActionCreator(types.FULFILLED)(), 46 | type: `${dispatched.type}FULFILLED`, 47 | }; 48 | 49 | return store.dispatch(dispatched).then(({ value, action }) => { 50 | expect(value).toEqual(expected.payload); 51 | expect(action).toEqual(expected); 52 | done(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/function.spec.js: -------------------------------------------------------------------------------- 1 | import createStore from './utils/createStore'; 2 | 3 | it('action dispatched for given synchronous function', () => { 4 | const store = createStore(); 5 | 6 | const dispatched = { 7 | type: 'ACTION', 8 | payload() { 9 | return 'foo'; 10 | } 11 | }; 12 | 13 | const expected = { 14 | type: 'ACTION', 15 | payload: 'foo', 16 | }; 17 | 18 | store.dispatch(dispatched); 19 | 20 | expect(store.lastSpy.mock.calls[0]).toEqual([expected]); 21 | }); 22 | -------------------------------------------------------------------------------- /test/module-versions.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: ["error", { allow: ["warn"] }] */ 2 | import promise from 'redux-promise-middleware'; 3 | 4 | // For migration from version 5.x to version 6.0.0 5 | test('module exports warns when incorrectly called', () => { 6 | // Replace the console.warn method with a mock/spy method 7 | global.console = { warn: jest.fn() }; 8 | 9 | // Try to instantiate a middleware, which should call console.warn 10 | promise(); 11 | 12 | // Make sure console.warn is called 13 | expect(console.warn).toHaveBeenCalled(); 14 | }); 15 | -------------------------------------------------------------------------------- /test/module.spec.js: -------------------------------------------------------------------------------- 1 | import promise, { createPromise, ActionType } from 'redux-promise-middleware'; 2 | 3 | test('module exports default `import promise from \'redux-promise-middleware\'', () => { 4 | expect(promise.length).toBe(0); 5 | }); 6 | 7 | test('module exports `import { createPromise } from \'redux-promise-middleware\'', () => { 8 | const middleware = createPromise(); 9 | expect(middleware.length).toBe(1); 10 | }); 11 | 12 | test('module exports `import { ActionType } from \'redux-promise-middleware\'', () => { 13 | expect(typeof ActionType).toBe('object'); 14 | expect(ActionType.Pending).toBe('PENDING'); 15 | expect(ActionType.Fulfilled).toBe('FULFILLED'); 16 | expect(ActionType.Rejected).toBe('REJECTED'); 17 | }); 18 | -------------------------------------------------------------------------------- /test/optimistic-updates.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | beforeEach(() => { store = createStore(); }); 7 | 8 | test('pending action contains given optimistic update of type object', () => { 9 | const { dispatch, firstSpy, lastSpy } = store; 10 | 11 | const dispatched = getActionCreator(types.OPTIMISTIC_UPDATE)(); 12 | const expected = getActionCreator(types.PENDING_OPTIMISTIC_UPDATE)(); 13 | 14 | dispatch(dispatched); 15 | 16 | expect(firstSpy.mock.calls[0]).toEqual([dispatched]); 17 | expect(lastSpy.mock.calls[0]).toEqual([expected]); 18 | }); 19 | 20 | test('pending action contains given optimistic update of type boolean', () => { 21 | const { dispatch, firstSpy, lastSpy } = store; 22 | 23 | const dispatched = { 24 | ...getActionCreator(types.OPTIMISTIC_UPDATE)(), 25 | payload: { 26 | promise: Promise.resolve(), 27 | data: true, 28 | }, 29 | }; 30 | 31 | const expected = { 32 | ...getActionCreator(types.PENDING_OPTIMISTIC_UPDATE)(), 33 | payload: true, 34 | }; 35 | 36 | dispatch(dispatched); 37 | 38 | expect(firstSpy.mock.calls[0]).toEqual([dispatched]); 39 | expect(lastSpy.mock.calls[0]).toEqual([expected]); 40 | }); 41 | -------------------------------------------------------------------------------- /test/promise-fulfilled.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | beforeEach(() => { store = createStore(); }); 7 | 8 | test('pending action dispatched with given payload', (done) => { 9 | const { dispatch, lastSpy } = store; 10 | 11 | const dispatched = getActionCreator(types.WILL_RESOLVE)(); 12 | const expected = getActionCreator(types.FULFILLED)(); 13 | 14 | return dispatch(dispatched).then(({ value, action: actionFromPromise }) => { 15 | expect(value).toEqual(expected.payload); 16 | expect(actionFromPromise.payload).toEqual(expected.payload); 17 | expect(lastSpy.mock.calls[1]).toEqual([expected]); 18 | done(); 19 | }); 20 | }); 21 | 22 | test('pending action dispatched with given meta', (done) => { 23 | const { dispatch, lastSpy } = store; 24 | 25 | const dispatched = getActionCreator(types.META_FIELD)(); 26 | const expected = getActionCreator(types.FULFILLED_META_FIELD)(); 27 | 28 | return dispatch(dispatched).then(({ value, action: actionFromPromise }) => { 29 | expect(value).toEqual(expected.payload); 30 | expect(actionFromPromise.meta).toEqual(expected.meta); 31 | expect(lastSpy.mock.calls[1]).toEqual([expected]); 32 | done(); 33 | }); 34 | }); 35 | 36 | test('fulfilled action dispatched with custom type', (done) => { 37 | const { dispatch, lastSpy } = createStore({ 38 | promiseTypeSuffixes: [undefined, 'SUCCESS', undefined] 39 | }); 40 | 41 | const dispatched = getActionCreator(types.WILL_RESOLVE)(); 42 | const expected = { 43 | ...getActionCreator(types.FULFILLED)(), 44 | type: 'ACTION_SUCCESS', 45 | }; 46 | 47 | return dispatch(dispatched).then(() => { 48 | expect(lastSpy.mock.calls[1]).toEqual([expected]); 49 | done(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/promise-pending.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | beforeEach(() => { store = createStore(); }); 7 | 8 | /* 9 | * Test if the middleware dispatches a pending action when the payload 10 | * property has a Promise object as the value. This is considered an "implicit" 11 | * promise payload. 12 | */ 13 | test('pending action dispatched for promise - payload', () => { 14 | const { dispatch, firstSpy, lastSpy } = store; 15 | 16 | const dispatched = getActionCreator(types.WILL_RESOLVE)(); 17 | const expected = getActionCreator(types.PENDING)(); 18 | 19 | dispatch(dispatched); 20 | 21 | expect(firstSpy.mock.calls[0]).toEqual([dispatched]); 22 | expect(lastSpy.mock.calls[0]).toEqual([expected]); 23 | }); 24 | 25 | /* 26 | * Tests if the middleware dispatches a pending action 27 | * when the payload has a `promise` property with a Promise object 28 | * as the value. This is considered an "explicit" promise payload because 29 | * the `promise` property explicitly describes the value. 30 | */ 31 | test('pending action dispatched for promise - payload.promise', () => { 32 | const { dispatch, firstSpy, lastSpy } = store; 33 | 34 | const dispatched = getActionCreator(types.PROMISE_FIELD)(); 35 | const expected = getActionCreator(types.PENDING)(); 36 | 37 | dispatch(dispatched); 38 | 39 | expect(firstSpy.mock.calls[0]).toEqual([dispatched]); 40 | expect(lastSpy.mock.calls[0]).toEqual([expected]); 41 | }); 42 | 43 | test('pending action contains given meta of type object', () => { 44 | const { dispatch, firstSpy, lastSpy } = store; 45 | 46 | const meta = { 47 | foo: 'foo', 48 | bar: 'bar', 49 | baz: 'baz', 50 | }; 51 | 52 | const dispatched = { 53 | ...getActionCreator(types.WILL_RESOLVE)(), 54 | meta, 55 | }; 56 | 57 | const expected = { 58 | ...getActionCreator(types.PENDING)(), 59 | meta, 60 | }; 61 | 62 | dispatch(dispatched); 63 | 64 | expect(firstSpy.mock.calls[0]).toEqual([dispatched]); 65 | expect(lastSpy.mock.calls[0]).toEqual([expected]); 66 | }); 67 | 68 | test('pending action contains given meta of type boolean', () => { 69 | const { dispatch, firstSpy, lastSpy } = store; 70 | 71 | const meta = true; 72 | 73 | const dispatched = { 74 | ...getActionCreator(types.WILL_RESOLVE)(), 75 | meta, 76 | }; 77 | 78 | const expected = { 79 | ...getActionCreator(types.PENDING)(), 80 | meta, 81 | }; 82 | 83 | dispatch(dispatched); 84 | 85 | expect(firstSpy.mock.calls[0]).toEqual([dispatched]); 86 | expect(lastSpy.mock.calls[0]).toEqual([expected]); 87 | }); 88 | 89 | test('pending action dispatched with custom type', (done) => { 90 | const { dispatch, lastSpy } = createStore({ 91 | promiseTypeSuffixes: ['LOADING', undefined, undefined] 92 | }); 93 | 94 | const dispatched = getActionCreator(types.WILL_RESOLVE)(); 95 | const expected = { 96 | ...getActionCreator(types.PENDING)(), 97 | type: 'ACTION_LOADING', 98 | }; 99 | 100 | return dispatch(dispatched).then(() => { 101 | expect(lastSpy.mock.calls[0]).toEqual([expected]); 102 | done(); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/promise-rejected.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator, defaultError } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | beforeEach(() => { store = createStore(); }); 7 | 8 | test('errors caught with Promise#catch method', (done) => { 9 | const { dispatch, lastSpy } = store; 10 | 11 | const dispatched = getActionCreator(types.WILL_REJECT)(); 12 | 13 | const promiseSpy = jest.fn(); 14 | const action = dispatch(dispatched); 15 | 16 | return action 17 | .then(promiseSpy) 18 | .catch((error) => { 19 | expect(error).toBeInstanceOf(Error); 20 | expect(lastSpy.mock.calls[0]).toEqual([getActionCreator(types.PENDING)()]); 21 | expect(lastSpy.mock.calls[1]).toEqual([getActionCreator(types.REJECTED)()]); 22 | }) 23 | .then(() => { 24 | expect(promiseSpy.mock.calls.length).toBe(0); 25 | done(); 26 | }); 27 | }); 28 | 29 | test('errors caught with Promise#then method', (done) => { 30 | const { dispatch, lastSpy } = store; 31 | 32 | const dispatched = getActionCreator(types.WILL_REJECT)(); 33 | 34 | const promiseSpy = jest.fn(); 35 | const action = dispatch(dispatched); 36 | 37 | return action 38 | .then(promiseSpy, (error) => { 39 | expect(error).toBeInstanceOf(Error); 40 | expect(lastSpy.mock.calls[0]).toEqual([getActionCreator(types.PENDING)()]); 41 | expect(lastSpy.mock.calls[1]).toEqual([getActionCreator(types.REJECTED)()]); 42 | }) 43 | .then(() => { 44 | expect(promiseSpy.mock.calls.length).toBe(0); 45 | done(); 46 | }); 47 | }); 48 | 49 | test('rejected action dispatched with truthy error property', () => { 50 | const { dispatch, lastSpy } = store; 51 | 52 | const action = dispatch(getActionCreator(types.WILL_REJECT)()); 53 | 54 | return action.catch(() => { 55 | expect(lastSpy.mock.calls[1][0].error).toBeTruthy(); 56 | }); 57 | }); 58 | 59 | test('promise returns original Error instance', () => { 60 | const { dispatch } = store; 61 | 62 | const dispatched = getActionCreator(types.WILL_REJECT)(); 63 | 64 | return dispatch(dispatched).catch((error) => { 65 | expect(error).toEqual(defaultError); 66 | expect(error.message).toEqual(defaultError.message); 67 | }); 68 | }); 69 | 70 | test('rejected action dispatched with custom type', () => { 71 | const { dispatch, lastSpy } = createStore({ 72 | promiseTypeSuffixes: [undefined, undefined, 'ERROR'] 73 | }); 74 | 75 | const dispatched = getActionCreator(types.WILL_REJECT)(); 76 | const expected = { 77 | ...getActionCreator(types.REJECTED)(), 78 | type: 'ACTION_ERROR', 79 | }; 80 | 81 | return dispatch(dispatched).catch(() => { 82 | expect(lastSpy.mock.calls[1]).toEqual([expected]); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/resolve-boolean.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | beforeEach(() => { store = createStore(); }); 7 | 8 | describe('given a promise resolved with a boolean value', () => { 9 | test('resolved action dispatched with boolean payload', (done) => { 10 | const { dispatch, firstSpy, lastSpy } = store; 11 | 12 | const dispatched = getActionCreator(types.BOOLEAN_PROMISE)(); 13 | const expected = getActionCreator(types.FULFILLED_BOOLEAN_PROMISE)(); 14 | 15 | const action = dispatch(dispatched); 16 | 17 | expect(firstSpy.mock.calls[0]).toEqual([dispatched]); 18 | 19 | action.then(() => { 20 | expect(lastSpy.mock.calls[1]).toEqual([expected]); 21 | done(); 22 | }); 23 | }); 24 | 25 | test('promise returns boolean value', (done) => { 26 | const { dispatch } = store; 27 | 28 | const dispatched = getActionCreator(types.BOOLEAN_PROMISE)(); 29 | const expected = getActionCreator(types.FULFILLED_BOOLEAN_PROMISE)(); 30 | 31 | const action = dispatch(dispatched); 32 | 33 | action.then(({ value, action: actionFromPromise }) => { 34 | expect(value).toEqual(expected.payload); 35 | expect(actionFromPromise.payload).toEqual(expected.payload); 36 | done(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/resolve-null.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | beforeEach(() => { store = createStore(); }); 7 | 8 | describe('given a promise resolved with a null value', () => { 9 | test('resolved action dispatched with ~undefined~ payload', (done) => { 10 | const { dispatch, firstSpy, lastSpy } = store; 11 | 12 | const dispatched = getActionCreator(types.NULL_PROMISE)(); 13 | const expected = getActionCreator(types.FULFILLED_NULL_PROMISE)(); 14 | 15 | const action = dispatch(dispatched); 16 | 17 | expect(firstSpy.mock.calls[0]).toEqual([dispatched]); 18 | 19 | action.then(() => { 20 | expect(lastSpy.mock.calls[1]).toEqual([expected]); 21 | done(); 22 | }); 23 | }); 24 | 25 | test('promise returns null value', (done) => { 26 | const { dispatch } = store; 27 | 28 | const dispatched = getActionCreator(types.NULL_PROMISE)(); 29 | 30 | const action = dispatch(dispatched); 31 | 32 | action.then(({ value, action: actionFromPromise }) => { 33 | expect(value).toEqual(null); 34 | expect(actionFromPromise.payload).toEqual(undefined); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/resolve-number.spec.js: -------------------------------------------------------------------------------- 1 | import { types, getActionCreator } from './utils/defaults'; 2 | import createStore from './utils/createStore'; 3 | 4 | let store; 5 | 6 | beforeEach(() => { store = createStore(); }); 7 | 8 | describe('given a promise resolved with a numeric value', () => { 9 | test('resolved action dispatched with numeric payload', (done) => { 10 | const { dispatch, firstSpy, lastSpy } = store; 11 | 12 | const dispatched = getActionCreator(types.NUMBER_PROMISE)(); 13 | const expected = getActionCreator(types.FULFILLED_NUMBER_PROMISE)(); 14 | 15 | const action = dispatch(dispatched); 16 | 17 | expect(firstSpy.mock.calls[0]).toEqual([dispatched]); 18 | 19 | action.then(() => { 20 | expect(lastSpy.mock.calls[1]).toEqual([expected]); 21 | done(); 22 | }); 23 | }); 24 | 25 | test('promise returns numeric value', (done) => { 26 | const { dispatch } = store; 27 | 28 | const dispatched = getActionCreator(types.NUMBER_PROMISE)(); 29 | const expected = getActionCreator(types.FULFILLED_NUMBER_PROMISE)(); 30 | 31 | const action = dispatch(dispatched); 32 | 33 | action.then(({ value, action: actionFromPromise }) => { 34 | expect(value).toEqual(expected.payload); 35 | expect(actionFromPromise.payload).toEqual(expected.payload); 36 | done(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/utils/createStore.js: -------------------------------------------------------------------------------- 1 | import { createStore as createReduxStore, applyMiddleware } from 'redux'; 2 | import { createPromise } from 'redux-promise-middleware'; 3 | import spyMiddleware from './spyMiddleware'; 4 | 5 | /* 6 | * Function: createStore 7 | * Description: Creates a store for testing that includes 8 | * spies/mock functions to see dispatched actions. 9 | */ 10 | const createStore = (config, restMiddlewares = []) => { 11 | const firstSpy = jest.fn(); 12 | const lastSpy = jest.fn(); 13 | 14 | const middlewares = [ 15 | spyMiddleware(firstSpy), 16 | createPromise(config), 17 | spyMiddleware(lastSpy), 18 | ...restMiddlewares, 19 | ]; 20 | 21 | const store = applyMiddleware(...middlewares)(createReduxStore)(() => null); 22 | 23 | return { 24 | dispatch: store.dispatch, 25 | getState: store.getState, 26 | firstSpy, 27 | lastSpy, 28 | }; 29 | }; 30 | 31 | export default createStore; 32 | -------------------------------------------------------------------------------- /test/utils/defaults.js: -------------------------------------------------------------------------------- 1 | import Bluebird from 'bluebird'; 2 | 3 | // The action types tested for 4 | export const types = { 5 | DEFAULT: 'DEFAULT', 6 | WILL_RESOLVE: 'WILL_RESOLVE', 7 | WILL_REJECT: 'WILL_REJECT', 8 | OPTIMISTIC_UPDATE: 'OPTIMISTIC_UPDATE', 9 | PROMISE_FIELD: 'PROMISE_FIELD', 10 | META_FIELD: 'META_FIELD', 11 | BLUEBIRD: 'BLUEBIRD', 12 | BOOLEAN_PROMISE: 'BOOLEAN_PROMISE', 13 | NULL_PROMISE: 'NULL_PROMISE', 14 | NUMBER_PROMISE: 'NUMBER_PROMISE', 15 | ASYNC_FUNCTION_WILL_RESOLVE: 'ASYNC_FUNCTION_WILL_RESOLVE', 16 | ASYNC_FUNCTION_WILL_REJECT: 'ASYNC_FUNCTION_WILL_REJECT', 17 | ASYNC_FUNCTION_PROMISE_FIELD: 'ASYNC_FUNC_PROMISE_FIELD', 18 | ASYNC_FUNCTION_OPTIMISTIC_UPDATE: 'ASYNC_FUNCTION_OPTIMISTIC_UPDATE', 19 | PENDING: 'PENDING', 20 | PENDING_META_FIELD: 'PENDING_META_FIELD', 21 | PENDING_OPTIMISTIC_UPDATE: 'PENDING_OPTIMISTIC_UPDATE', 22 | FULFILLED: 'FULFILLED', 23 | FULFILLED_META_FIELD: 'FULFILLED_META_FIELD', 24 | FULFILLED_BOOLEAN_PROMISE: 'FULFILLED_BOOLEAN_PROMISE', 25 | FULFILLED_NULL_PROMISE: 'FULFILLED_NULL_PROMISE', 26 | FULFILLED_NUMBER_PROMISE: 'FULFILLED_NUMBER_PROMISE', 27 | REJECTED: 'REJECTED', 28 | }; 29 | 30 | export const defaultMetadata = { foo: 'foo' }; 31 | export const defaultPayload = 'foo'; 32 | export const defaultError = new Error('foo'); 33 | 34 | // A simple action with no payload 35 | const defaultAction = { 36 | type: 'ACTION', 37 | }; 38 | 39 | // The action creators supported by the test suite 40 | const actions = { 41 | [types.DEFAULT]: () => (defaultAction), 42 | [types.WILL_RESOLVE]: () => ({ 43 | type: defaultAction.type, 44 | payload: Promise.resolve(defaultPayload), 45 | }), 46 | [types.WILL_REJECT]: () => ({ 47 | type: defaultAction.type, 48 | payload: new Promise((resolve, reject) => reject(defaultError)), 49 | }), 50 | [types.OPTIMISTIC_UPDATE]: () => ({ 51 | type: defaultAction.type, 52 | payload: { 53 | promise: Promise.resolve(defaultPayload), 54 | data: defaultMetadata, 55 | }, 56 | }), 57 | [types.PROMISE_FIELD]: () => ({ 58 | type: defaultAction.type, 59 | payload: { 60 | promise: Promise.resolve(defaultPayload), 61 | }, 62 | }), 63 | [types.META_FIELD]: () => ({ 64 | type: defaultAction.type, 65 | payload: Promise.resolve(defaultPayload), 66 | meta: defaultMetadata, 67 | }), 68 | [types.BLUEBIRD]: () => ({ 69 | type: defaultAction.type, 70 | payload: Bluebird.resolve(defaultPayload), 71 | }), 72 | [types.BOOLEAN_PROMISE]: () => ({ 73 | type: defaultAction.type, 74 | payload: Promise.resolve(true), 75 | }), 76 | [types.NULL_PROMISE]: () => ({ 77 | type: defaultAction.type, 78 | payload: Promise.resolve(null), 79 | }), 80 | [types.NUMBER_PROMISE]: () => ({ 81 | type: defaultAction.type, 82 | payload: Promise.resolve(21), 83 | }), 84 | [types.ASYNC_FUNCTION_WILL_RESOLVE]: () => ({ 85 | type: defaultAction.type, 86 | async payload() { 87 | return defaultPayload; 88 | }, 89 | }), 90 | [types.ASYNC_FUNCTION_WILL_REJECT]: () => ({ 91 | type: defaultAction.type, 92 | async payload() { 93 | throw new Error('foo'); 94 | }, 95 | }), 96 | [types.ASYNC_FUNCTION_PROMISE_FIELD]: () => ({ 97 | type: defaultAction.type, 98 | payload: { 99 | async promise() { 100 | return Promise.resolve(defaultPayload); 101 | }, 102 | }, 103 | }), 104 | [types.ASYNC_FUNCTION_OPTIMISTIC_UPDATE]: () => ({ 105 | type: defaultAction.type, 106 | payload: { 107 | async promise() { 108 | return Promise.resolve(defaultPayload); 109 | }, 110 | data: defaultMetadata, 111 | }, 112 | }), 113 | [types.PENDING]: () => ({ 114 | type: `${defaultAction.type}_PENDING`, 115 | }), 116 | [types.PENDING_META_FIELD]: () => ({ 117 | type: `${defaultAction.type}_PENDING`, 118 | meta: defaultMetadata, 119 | }), 120 | [types.PENDING_OPTIMISTIC_UPDATE]: () => ({ 121 | type: `${defaultAction.type}_PENDING`, 122 | payload: defaultMetadata, 123 | }), 124 | [types.FULFILLED]: () => ({ 125 | type: `${defaultAction.type}_FULFILLED`, 126 | payload: defaultPayload, 127 | }), 128 | [types.FULFILLED_META_FIELD]: () => ({ 129 | type: `${defaultAction.type}_FULFILLED`, 130 | payload: defaultPayload, 131 | meta: defaultMetadata, 132 | }), 133 | [types.FULFILLED_BOOLEAN_PROMISE]: () => ({ 134 | type: `${defaultAction.type}_FULFILLED`, 135 | payload: true, 136 | }), 137 | [types.FULFILLED_NULL_PROMISE]: () => ({ 138 | type: `${defaultAction.type}_FULFILLED`, 139 | payload: undefined, 140 | }), 141 | [types.FULFILLED_NUMBER_PROMISE]: () => ({ 142 | type: `${defaultAction.type}_FULFILLED`, 143 | payload: 21, 144 | }), 145 | [types.REJECTED]: () => ({ 146 | type: `${defaultAction.type}_REJECTED`, 147 | payload: defaultError, 148 | error: true, 149 | }), 150 | }; 151 | 152 | // Get action creators 153 | export const getActionCreator = (actionType) => { 154 | const action = actions[actionType]; 155 | 156 | if (action) { 157 | return action; 158 | } 159 | 160 | throw new Error(`Action with type ${actionType} is not found.`); 161 | }; 162 | -------------------------------------------------------------------------------- /test/utils/modifierMiddleware.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Function: modifierMiddleware 3 | * Description: Modifies actions with a given modifier object 4 | */ 5 | function modifierMiddleware(spy, modifier) { 6 | return () => next => action => { 7 | const modifiedAction = Object.assign(action, modifier); 8 | 9 | spy(modifiedAction); 10 | 11 | return next(modifiedAction); 12 | }; 13 | } 14 | 15 | export default modifierMiddleware; 16 | -------------------------------------------------------------------------------- /test/utils/spyMiddleware.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Function: spyMiddleware 3 | * Description: Tracks dispached actions with a given spy/mock function 4 | */ 5 | function spyMiddleware(spy) { 6 | return () => next => action => { 7 | spy(action); 8 | 9 | return next(action); 10 | }; 11 | } 12 | 13 | export default spyMiddleware; 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // ncapsulates a config for Webpack used to generate UMD builds 2 | 3 | const config = { 4 | mode: 'production', 5 | entry: './src/index', 6 | 7 | // Compile JS files with Babel 8 | module: { 9 | rules: [ 10 | { test: /\.js$/, use: { loader: 'babel-loader' }, exclude: /node_modules/ } 11 | ] 12 | }, 13 | 14 | output: { 15 | library: 'ReduxPromiseMiddleware', 16 | libraryTarget: 'umd' 17 | } 18 | }; 19 | 20 | 21 | // If the environment is set to production, compress the output file 22 | if (process.env.NODE_ENV !== 'production') { 23 | config.optimization = { 24 | minimize: false 25 | }; 26 | } 27 | 28 | module.exports = config; 29 | --------------------------------------------------------------------------------