├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package-scripts.js ├── package.json ├── rollup.config.js └── src ├── MakeAsyncFunction.js ├── MakeAsyncFunction.test.js ├── index.js └── index.js.flow /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "8" 8 | } 9 | } 10 | ], 11 | "react", 12 | "stage-2" 13 | ], 14 | "env": { 15 | "test": { 16 | "plugins": ["transform-react-jsx-source", "istanbul"] 17 | } 18 | }, 19 | "plugins": ["transform-flow-strip-types"] 20 | } 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "jsx-a11y/href-no-hash": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | dist 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [options] 9 | esproposal.decorators=ignore 10 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rasmussenerik@gmail.com. The project 59 | team will review and investigate all complaints, and will respond in a way that 60 | it deems appropriate to the circumstances. The project team is obligated to 61 | maintain confidentiality with regard to the reporter of an incident. Further 62 | details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 71 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to React Redux Promise Listener! Please take a 4 | moment to review this document **before submitting a pull request**. 5 | 6 | We are open to, and grateful for, any contributions made by the community. 7 | 8 | ## Reporting issues and asking questions 9 | 10 | Before opening an issue, please search 11 | the[issue tracker](https://github.com/erikras/react-redux-promise-listener/issues) to 12 | make sure your issue hasn’t already been reported. 13 | 14 | **We use the issue tracker to keep track of bugs and improvements** to React 15 | Redux Promise Listener itself, its examples, and the documentation. We encourage you to open 16 | issues to discuss improvements, architecture, internal implementation, etc. If a 17 | topic has been discussed before, we will ask you to join the previous 18 | discussion. 19 | 20 | For support or usage questions, please search and ask on 21 | [StackOverflow with a `react-redux-promise-listener` tag](https://stackoverflow.com/questions/tagged/react-redux-promise-listener). 22 | We ask you to do this because StackOverflow has a much better job at keeping 23 | popular questions visible. Unfortunately good answers get lost and outdated on 24 | GitHub. 25 | 26 | **If you already asked at StackOverflow and still got no answers, post an issue 27 | with the question link, so we can either answer it or evolve into a bug/feature 28 | request.** 29 | 30 | ## Sending a pull request 31 | 32 | **Please ask first before starting work on any significant new features.** 33 | 34 | It's never a fun experience to have your pull request declined after investing a 35 | lot of time and effort into a new feature. To avoid this from happening, we 36 | request that contributors create 37 | [an issue](https://github.com/erikras/react-redux-promise-listener/issues) to first 38 | discuss any significant new features. 39 | 40 | Please try to keep your pull request focused in scope and avoid including 41 | unrelated commits. 42 | 43 | After you have submitted your pull request, we’ll try to get back to you as soon 44 | as possible. We may suggest some changes or improvements. 45 | 46 | Please format the code before submitting your pull request by running: 47 | 48 | ```sh 49 | npm run precommit 50 | ``` 51 | 52 | ## Coding standards 53 | 54 | Our code formatting rules are defined in 55 | [.eslintrc](https://github.com/erikras/react-redux-promise-listener/blob/master/.eslintrc). 56 | You can check your code against these standards by running: 57 | 58 | ```sh 59 | npm start lint 60 | ``` 61 | 62 | To automatically fix any style violations in your code, you can run: 63 | 64 | ```sh 65 | npm run precommit 66 | ``` 67 | 68 | ## Running tests 69 | 70 | You can run the test suite using the following commands: 71 | 72 | ```sh 73 | npm test 74 | ``` 75 | 76 | Please ensure that the tests are passing when submitting a pull request. If 77 | you're adding new features to React Redux Promise Listener, please include tests. 78 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### Are you submitting a **bug report** or a **feature request**? 8 | 9 | 10 | 11 | ### What is the current behavior? 12 | 13 | 14 | 15 | ### What is the expected behavior? 16 | 17 | ### Sandbox Link 18 | 19 | 20 | 21 | ### What's your environment? 22 | 23 | 24 | 25 | ### Other information 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.iml 3 | .nyc_output 4 | coverage 5 | flow-coverage 6 | node_modules 7 | dist 8 | lib 9 | es 10 | npm-debug.log 11 | .DS_Store 12 | yarn.lock 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | trailingComma: none 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '8' 10 | script: 11 | - npm start validate 12 | after_success: 13 | - npx codecov 14 | - npm install --global semantic-release 15 | # - semantic-release pre && npm publish && semantic-release post 16 | branches: 17 | only: 18 | - master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Erik Rasmussen 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 | # React Redux Promise Listener 2 | 3 | [![NPM Downloads](https://img.shields.io/npm/dm/react-redux-promise-listener.svg?style=flat)](https://www.npmjs.com/package/react-redux-promise-listener) 4 | [![Build Status](https://travis-ci.org/erikras/react-redux-promise-listener.svg?branch=master)](https://travis-ci.org/erikras/react-redux-promise-listener) 5 | [![codecov.io](https://codecov.io/gh/erikras/react-redux-promise-listener/branch/master/graph/badge.svg)](https://codecov.io/gh/erikras/react-redux-promise-listener) 6 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 7 | 8 | ## Why? 9 | 10 | Most of the popular React form libraries accept an `onSubmit` function that is expected to return a `Promise` that resolves when the submission is complete, or rejects when the submission fails. This mechanism is fundamentally incompatible with action management libraries like [`redux-saga`](https://redux-saga.js.org), which perform side-effects (e.g. ajax requests) in a way that does not let the submission function easily return a promise. React Redux Promise Listener is a potential solution. 11 | 12 | ### Example 13 | 14 | [![Edit 🏁 React Final Form - Async Redux Submission](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/x71mx66z8w) 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | * [Usage](#usage) 23 | * [Step 1](#step-1) 24 | * [Step 2](#step-2) 25 | * [API](#api) 26 | * [`MakeAsyncFunction: React.Component`](#makeasyncfunction-reactcomponentprops) 27 | * [Types](#types) 28 | * [`Props`](#props) 29 | * [`start: string`](#start-string) 30 | * [`resolve: string`](#resolve-string) 31 | * [`reject: string`](#reject-string) 32 | * [`setPayload?: (action: Object, payload: any) => Object`](#setpayload-action-object-payload-any--object) 33 | * [`getPayload?: (action: Object) => any`](#getpayload-action-object--any) 34 | * [`getError?: (action: Object) => any`](#geterror-action-object--any) 35 | 36 | 37 | 38 | ## Usage 39 | 40 | ### Step 1 41 | 42 | Step 1 involves installing Redux middleware and is detailed [here in the docs of Redux Promise Listener](https://github.com/erikras/redux-promise-listener#step-1). 43 | 44 | ### Step 2 45 | 46 | Welcome back! You may now create an async function in your React code like so: 47 | 48 | ```jsx 49 | import MakeAsyncFunction from 'react-redux-promise-listener' 50 | import { promiseListener } from './store' 51 | 52 | ... 53 | 54 | {asyncFunc => ( 60 | 61 | 62 | ... 63 | 64 | 65 | 66 | )} 67 | ``` 68 | 69 | ## API 70 | 71 | ### `MakeAsyncFunction: React.Component` 72 | 73 | A react component that passes an async function to its child render prop. 74 | 75 | ## Types 76 | 77 | ### `Props` 78 | 79 | #### `start: string` 80 | 81 | The `type` of action to dispatch when the function is called. 82 | 83 | #### `resolve: string` 84 | 85 | The `type` of action that will cause the promise to be resolved. 86 | 87 | #### `reject: string` 88 | 89 | The `type` of action that will cause the promise to be rejected. 90 | 91 | #### `setPayload?: (action: Object, payload: any) => Object` 92 | 93 | A function to set the payload (the parameter passed to the async function). Defaults to `(action, payload) => ({ ...action, payload })`. 94 | 95 | #### `getPayload?: (action: Object) => any` 96 | 97 | A function to get the payload out of the resolve action to pass to resolve the promise with. Defaults to `(action) => action.payload`. 98 | 99 | #### `getError?: (action: Object) => any` 100 | 101 | A function to get the error out of the reject action to pass to reject the promise with. Defaults to `(action) => action.payload`. 102 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | browser: true, 4 | testURL: 'http://localhost/' 5 | } 6 | -------------------------------------------------------------------------------- /package-scripts.js: -------------------------------------------------------------------------------- 1 | const npsUtils = require('nps-utils') 2 | 3 | const series = npsUtils.series 4 | const concurrent = npsUtils.concurrent 5 | const rimraf = npsUtils.rimraf 6 | const crossEnv = npsUtils.crossEnv 7 | 8 | module.exports = { 9 | scripts: { 10 | test: { 11 | default: crossEnv('NODE_ENV=test jest --coverage'), 12 | update: crossEnv('NODE_ENV=test jest --coverage --updateSnapshot'), 13 | watch: crossEnv('NODE_ENV=test jest --watch'), 14 | codeCov: crossEnv( 15 | 'cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js' 16 | ), 17 | size: { 18 | description: 'check the size of the bundle', 19 | script: 'bundlesize' 20 | } 21 | }, 22 | build: { 23 | description: 'delete the dist directory and run all builds', 24 | default: series( 25 | rimraf('dist'), 26 | concurrent.nps( 27 | 'build.es', 28 | 'build.cjs', 29 | 'build.umd.main', 30 | 'build.umd.min', 31 | 'copyTypes' 32 | ) 33 | ), 34 | es: { 35 | description: 'run the build with rollup (uses rollup.config.js)', 36 | script: 'rollup --config --environment FORMAT:es' 37 | }, 38 | cjs: { 39 | description: 'run rollup build with CommonJS format', 40 | script: 'rollup --config --environment FORMAT:cjs' 41 | }, 42 | umd: { 43 | min: { 44 | description: 'run the rollup build with sourcemaps', 45 | script: 'rollup --config --sourcemap --environment MINIFY,FORMAT:umd' 46 | }, 47 | main: { 48 | description: 'builds the cjs and umd files', 49 | script: 'rollup --config --sourcemap --environment FORMAT:umd' 50 | } 51 | }, 52 | andTest: series.nps('build', 'test.size') 53 | }, 54 | copyTypes: series( 55 | npsUtils.copy('src/*.js.flow dist'), 56 | npsUtils.copy( 57 | 'dist/index.js.flow dist --rename="react-redux-promise-listener.cjs.js.flow"' 58 | ), 59 | npsUtils.copy( 60 | 'dist/index.js.flow dist --rename="react-redux-promise-listener.es.js.flow"' 61 | ) 62 | ), 63 | docs: { 64 | description: 'Generates table of contents in README', 65 | script: 'doctoc README.md' 66 | }, 67 | lint: { 68 | description: 'lint the entire project', 69 | script: 'eslint .' 70 | }, 71 | flow: { 72 | description: 'flow check the entire project', 73 | script: 'flow check' 74 | }, 75 | validate: { 76 | description: 77 | 'This runs several scripts to make sure things look good before committing or on clean install', 78 | default: concurrent.nps('lint', 'flow', 'build.andTest', 'test') 79 | } 80 | }, 81 | options: { 82 | silent: false 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-promise-listener", 3 | "version": "1.0.0", 4 | "description": "A React component and Redux middleware that allows actions to be converted into Promises", 5 | "main": "dist/react-redux-promise-listener.cjs.js", 6 | "jsnext:main": "dist/react-redux-promise-listener.es.js", 7 | "module": "dist/react-redux-promise-listener.es.js", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "start": "nps", 13 | "test": "nps test", 14 | "precommit": "lint-staged && npm start validate", 15 | "prepublish": "npm start validate" 16 | }, 17 | "author": "Erik Rasmussen (http://github.com/erikras)", 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/erikras/react-redux-promise-listener.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/erikras/react-redux-promise-listener/issues" 25 | }, 26 | "homepage": "https://github.com/erikras/react-redux-promise-listener#readme", 27 | "testEnvironment": "node", 28 | "devDependencies": { 29 | "@types/react": "^16.4.7", 30 | "babel-eslint": "^8.2.6", 31 | "babel-jest": "^23.4.2", 32 | "babel-plugin-external-helpers": "^6.22.0", 33 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 34 | "babel-preset-env": "^1.7.0", 35 | "babel-preset-react": "^6.24.1", 36 | "babel-preset-stage-2": "^6.24.1", 37 | "bundlesize": "^0.17.0", 38 | "doctoc": "^1.3.0", 39 | "eslint": "^5.2.0", 40 | "eslint-config-react-app": "^2.1.0", 41 | "eslint-plugin-babel": "^5.1.0", 42 | "eslint-plugin-flowtype": "^2.50.0", 43 | "eslint-plugin-import": "^2.13.0", 44 | "eslint-plugin-jsx-a11y": "^6.1.1", 45 | "eslint-plugin-react": "^7.10.0", 46 | "flow-bin": "^0.77.0", 47 | "glow": "^1.2.2", 48 | "husky": "^0.14.3", 49 | "jest": "^23.4.2", 50 | "lint-staged": "^7.2.0", 51 | "nps": "^5.9.2", 52 | "nps-utils": "^1.6.0", 53 | "prettier": "^1.14.0", 54 | "prettier-eslint-cli": "^4.7.1", 55 | "prop-types": "^15.6.2", 56 | "react": "^16.4.1", 57 | "react-dom": "^16.4.1", 58 | "react-redux": "^5.0.7", 59 | "redux": "^4.0.0", 60 | "redux-promise-listener": "^1.0.0", 61 | "rollup": "^0.63.5", 62 | "rollup-plugin-babel": "^3.0.7", 63 | "rollup-plugin-commonjs": "^9.1.4", 64 | "rollup-plugin-flow": "^1.1.1", 65 | "rollup-plugin-node-resolve": "^3.3.0", 66 | "rollup-plugin-replace": "^2.0.0", 67 | "rollup-plugin-uglify": "^4.0.0" 68 | }, 69 | "peerDependencies": { 70 | "redux": ">=3.0.0", 71 | "redux-promise-listener": ">=1.0.0", 72 | "prop-types": "^15.6.0", 73 | "react": "^15.3.0 || ^16.0.0", 74 | "react-redux": ">=5.0.0" 75 | }, 76 | "lint-staged": { 77 | "*.{js*,json,md,css}": [ 78 | "prettier --write", 79 | "git add" 80 | ] 81 | }, 82 | "bundlesize": [ 83 | { 84 | "path": "dist/react-redux-promise-listener.umd.min.js", 85 | "threshold": "700B" 86 | }, 87 | { 88 | "path": "dist/react-redux-promise-listener.es.js", 89 | "threshold": "1kB" 90 | }, 91 | { 92 | "path": "dist/react-redux-promise-listener.cjs.js", 93 | "threshold": "1kB" 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | import flow from 'rollup-plugin-flow' 4 | import commonjs from 'rollup-plugin-commonjs' 5 | import { uglify } from 'rollup-plugin-uglify' 6 | import replace from 'rollup-plugin-replace' 7 | 8 | const minify = process.env.MINIFY 9 | const format = process.env.FORMAT 10 | const es = format === 'es' 11 | const umd = format === 'umd' 12 | const cjs = format === 'cjs' 13 | 14 | let output 15 | 16 | if (es) { 17 | output = { file: `dist/react-redux-promise-listener.es.js`, format: 'es' } 18 | } else if (umd) { 19 | if (minify) { 20 | output = { 21 | file: `dist/react-redux-promise-listener.umd.min.js`, 22 | format: 'umd' 23 | } 24 | } else { 25 | output = { file: `dist/react-redux-promise-listener.umd.js`, format: 'umd' } 26 | } 27 | } else if (cjs) { 28 | output = { file: `dist/react-redux-promise-listener.cjs.js`, format: 'cjs' } 29 | } else if (format) { 30 | throw new Error(`invalid format specified: "${format}".`) 31 | } else { 32 | throw new Error('no format specified. --environment FORMAT:xxx') 33 | } 34 | 35 | export default { 36 | input: 'src/index.js', 37 | output: Object.assign( 38 | { 39 | name: 'react-redux-promise-listener', 40 | exports: 'named', 41 | globals: { 42 | react: 'React', 43 | 'react-redux': 'ReactRedux', 44 | 'prop-types': 'PropTypes', 45 | 'redux-promise-listener': 'ReduxPromiseListener' 46 | } 47 | }, 48 | output 49 | ), 50 | external: ['react', 'prop-types', 'redux-promise-listener'], 51 | plugins: [ 52 | resolve({ jsnext: true, main: true }), 53 | flow(), 54 | commonjs({ include: 'node_modules/**' }), 55 | babel({ 56 | exclude: 'node_modules/**', 57 | babelrc: false, 58 | presets: [['env', { modules: false }], 'stage-2'], 59 | plugins: ['external-helpers'] 60 | }), 61 | umd 62 | ? replace({ 63 | 'process.env.NODE_ENV': JSON.stringify( 64 | minify ? 'production' : 'development' 65 | ) 66 | }) 67 | : null, 68 | minify ? uglify() : null 69 | ].filter(Boolean) 70 | } 71 | -------------------------------------------------------------------------------- /src/MakeAsyncFunction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import PropTypes from 'prop-types' 4 | import type { 5 | AsyncFunction, 6 | Config, 7 | PromiseListener 8 | } from 'redux-promise-listener' 9 | 10 | type ChildrenAndListener = { 11 | children: ((any) => Promise) => void, 12 | listener: PromiseListener 13 | } 14 | 15 | type Props = Config & ChildrenAndListener 16 | type State = { asyncFunction?: AsyncFunction } 17 | 18 | export default class MakeAsyncFunction extends React.Component { 19 | props: Props 20 | state: State 21 | 22 | static propTypes = { 23 | children: PropTypes.func.isRequired, 24 | listener: PropTypes.object.isRequired, 25 | start: PropTypes.string.isRequired, 26 | resolve: PropTypes.string.isRequired, 27 | reject: PropTypes.string.isRequired, 28 | setPayload: PropTypes.func, 29 | getPayload: PropTypes.func, 30 | getError: PropTypes.func 31 | } 32 | 33 | constructor(props: Props) { 34 | super(props) 35 | if ( 36 | process.env.NODE_ENV !== 'production' && 37 | typeof props.children !== 'function' 38 | ) { 39 | console.error('Warning: Must provide a render function as children') 40 | } 41 | const { 42 | listener, 43 | start, 44 | resolve, 45 | reject, 46 | setPayload, 47 | getPayload, 48 | getError 49 | } = props 50 | this.state = { 51 | asyncFunction: listener.createAsyncFunction({ 52 | start, 53 | resolve, 54 | reject, 55 | setPayload, 56 | getPayload, 57 | getError 58 | }) 59 | } 60 | } 61 | 62 | unsubscribe = () => { 63 | if (this.state.asyncFunction) { 64 | this.state.asyncFunction.unsubscribe() 65 | } 66 | } 67 | 68 | createAsyncFunction = () => { 69 | const { 70 | listener, 71 | start, 72 | resolve, 73 | reject, 74 | setPayload, 75 | getPayload, 76 | getError 77 | } = this.props 78 | this.unsubscribe() 79 | this.setState({ 80 | asyncFunction: listener.createAsyncFunction({ 81 | start, 82 | resolve, 83 | reject, 84 | setPayload, 85 | getPayload, 86 | getError 87 | }) 88 | }) 89 | } 90 | 91 | componentDidMount() { 92 | this.createAsyncFunction() 93 | } 94 | 95 | componentDidUpdate(prevProps: Props) { 96 | if ( 97 | prevProps.start !== this.props.start || 98 | prevProps.resolve !== this.props.resolve || 99 | prevProps.reject !== this.props.reject 100 | ) { 101 | this.createAsyncFunction() 102 | } 103 | } 104 | 105 | componentWillUnmount() { 106 | this.unsubscribe() 107 | } 108 | 109 | render() { 110 | return this.props.children && this.state.asyncFunction 111 | ? this.props.children(this.state.asyncFunction.asyncFunction) 112 | : null 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/MakeAsyncFunction.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TestUtils from 'react-dom/test-utils' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import createListener from 'redux-promise-listener' 6 | import MakeAsyncFunction from './MakeAsyncFunction' 7 | 8 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) 9 | const minimalMockProps = { 10 | listener: { 11 | createAsyncFunction: () => {} 12 | }, 13 | start: 'mock', 14 | resolve: 'mock', 15 | reject: 'mock' 16 | } 17 | 18 | describe('MakeAsyncFunction', () => { 19 | it('should print a warning with no children render function specified', () => { 20 | const spy = jest.spyOn(global.console, 'error').mockImplementation(() => {}) 21 | TestUtils.renderIntoDocument() 22 | expect(spy).toHaveBeenCalled() 23 | expect(spy).toHaveBeenCalledWith( 24 | 'Warning: Must provide a render function as children' 25 | ) 26 | spy.mockRestore() 27 | }) 28 | 29 | it('should dispatch start action, and resolve on resolve action', async () => { 30 | const reducer = jest.fn((state, action) => state) 31 | const resolve = jest.fn() 32 | const reject = jest.fn() 33 | const initialState = {} 34 | const listener = createListener() 35 | 36 | const store = createStore( 37 | reducer, 38 | initialState, 39 | applyMiddleware(listener.middleware) 40 | ) 41 | expect(reducer).toHaveBeenCalled() 42 | expect(reducer).toHaveBeenCalledTimes(1) 43 | expect(reducer.mock.calls[0][0]).toBe(initialState) 44 | 45 | const dom = TestUtils.renderIntoDocument( 46 | 47 | 53 | {save => { 54 | expect(save).toBeDefined() 55 | expect(typeof save).toBe('function') 56 | return ( 57 |
58 |
65 | ) 66 | }} 67 |
68 |
69 | ) 70 | const button = TestUtils.findRenderedDOMComponentWithTag(dom, 'button') 71 | expect(button).toBeDefined() 72 | 73 | TestUtils.Simulate.click(button) 74 | 75 | expect(reducer).toHaveBeenCalledTimes(2) 76 | expect(reducer.mock.calls[1][1]).toEqual({ type: 'SAVE' }) 77 | 78 | expect(resolve).not.toHaveBeenCalled() 79 | expect(reject).not.toHaveBeenCalled() 80 | 81 | await sleep(1) 82 | 83 | store.dispatch({ type: 'SAVE_SUCCESS', payload: 'Awesome!' }) 84 | 85 | await sleep(1) 86 | 87 | expect(resolve).toHaveBeenCalled() 88 | expect(resolve).toHaveBeenCalledTimes(1) 89 | expect(resolve.mock.calls[0][0]).toBe('Awesome!') 90 | expect(reject).not.toHaveBeenCalled() 91 | }) 92 | 93 | it('should dispatch start action, and reject on reject action', async () => { 94 | const reducer = jest.fn((state, action) => state) 95 | const resolve = jest.fn() 96 | const reject = jest.fn() 97 | const initialState = {} 98 | const listener = createListener() 99 | 100 | const store = createStore( 101 | reducer, 102 | initialState, 103 | applyMiddleware(listener.middleware) 104 | ) 105 | expect(reducer).toHaveBeenCalled() 106 | expect(reducer).toHaveBeenCalledTimes(1) 107 | expect(reducer.mock.calls[0][0]).toBe(initialState) 108 | 109 | const dom = TestUtils.renderIntoDocument( 110 | 111 | 117 | {save => { 118 | expect(save).toBeDefined() 119 | expect(typeof save).toBe('function') 120 | return ( 121 |
122 |
129 | ) 130 | }} 131 |
132 |
133 | ) 134 | const button = TestUtils.findRenderedDOMComponentWithTag(dom, 'button') 135 | expect(button).toBeDefined() 136 | 137 | TestUtils.Simulate.click(button) 138 | 139 | expect(reducer).toHaveBeenCalledTimes(2) 140 | expect(reducer.mock.calls[1][1]).toEqual({ type: 'SAVE' }) 141 | 142 | expect(resolve).not.toHaveBeenCalled() 143 | expect(reject).not.toHaveBeenCalled() 144 | 145 | await sleep(1) 146 | 147 | store.dispatch({ type: 'SAVE_ERROR', payload: 'Bummer!' }) 148 | 149 | await sleep(1) 150 | 151 | expect(resolve).not.toHaveBeenCalled() 152 | expect(reject).toHaveBeenCalled() 153 | expect(reject).toHaveBeenCalledTimes(1) 154 | expect(reject.mock.calls[0][0]).toBe('Bummer!') 155 | }) 156 | 157 | it('should accept changes to action props', async () => { 158 | const reducer = jest.fn((state, action) => state) 159 | const resolve = jest.fn() 160 | const reject = jest.fn() 161 | const initialState = {} 162 | const listener = createListener() 163 | 164 | const store = createStore( 165 | reducer, 166 | initialState, 167 | applyMiddleware(listener.middleware) 168 | ) 169 | expect(reducer).toHaveBeenCalled() 170 | expect(reducer).toHaveBeenCalledTimes(1) 171 | expect(reducer.mock.calls[0][0]).toBe(initialState) 172 | 173 | class Container extends React.Component { 174 | state = { 175 | resolveAction: 'SAVE_SUCCESS' 176 | } 177 | 178 | render() { 179 | return ( 180 |
181 | 187 | {save => { 188 | expect(save).toBeDefined() 189 | expect(typeof save).toBe('function') 190 | return ( 191 |
192 |
199 | ) 200 | }} 201 |
202 | 204 | this.setState({ resolveAction: 'OTHER_SAVE_SUCCESS' }) 205 | } 206 | > 207 | Change Action 208 | 209 |
210 | ) 211 | } 212 | } 213 | 214 | const dom = TestUtils.renderIntoDocument( 215 | 216 | 217 | 218 | ) 219 | 220 | const button = TestUtils.findRenderedDOMComponentWithTag(dom, 'button') 221 | const changeAction = TestUtils.findRenderedDOMComponentWithTag(dom, 'span') 222 | expect(button).toBeDefined() 223 | expect(changeAction).toBeDefined() 224 | 225 | TestUtils.Simulate.click(button) 226 | 227 | expect(reducer).toHaveBeenCalledTimes(2) 228 | expect(reducer.mock.calls[1][1]).toEqual({ type: 'SAVE' }) 229 | 230 | expect(resolve).not.toHaveBeenCalled() 231 | expect(reject).not.toHaveBeenCalled() 232 | 233 | await sleep(1) 234 | 235 | store.dispatch({ type: 'SAVE_SUCCESS', payload: 'Great!' }) 236 | 237 | await sleep(1) 238 | 239 | expect(resolve).toHaveBeenCalled() 240 | expect(resolve).toHaveBeenCalledTimes(1) 241 | expect(resolve.mock.calls[0][0]).toBe('Great!') 242 | expect(reject).not.toHaveBeenCalled() 243 | 244 | TestUtils.Simulate.click(changeAction) 245 | 246 | // Click save again 247 | 248 | TestUtils.Simulate.click(button) 249 | 250 | expect(reducer).toHaveBeenCalledTimes(4) 251 | expect(reducer.mock.calls[3][1]).toEqual({ type: 'SAVE' }) 252 | 253 | await sleep(1) 254 | 255 | // old save action should not trigger resolve 256 | expect(resolve).toHaveBeenCalledTimes(1) 257 | store.dispatch({ type: 'SAVE_SUCCESS', payload: 'Great again!' }) 258 | await sleep(1) 259 | expect(resolve).toHaveBeenCalledTimes(1) 260 | 261 | // new save action should 262 | store.dispatch({ type: 'OTHER_SAVE_SUCCESS', payload: 'Also great!' }) 263 | 264 | await sleep(1) 265 | expect(resolve).toHaveBeenCalledTimes(2) 266 | expect(resolve.mock.calls[1][0]).toBe('Also great!') 267 | expect(reject).not.toHaveBeenCalled() 268 | }) 269 | 270 | it('should unsubscribe on unmount', async () => { 271 | const reducer = jest.fn((state, action) => state) 272 | const initialState = {} 273 | const listener = createListener() 274 | 275 | const store = createStore( 276 | reducer, 277 | initialState, 278 | applyMiddleware(listener.middleware) 279 | ) 280 | 281 | class Container extends React.Component { 282 | state = { 283 | showComponent: true 284 | } 285 | 286 | render() { 287 | return ( 288 |
289 | {this.state.showComponent && ( 290 | 296 | {save =>
} 297 | 298 | )} 299 | 302 |
303 | ) 304 | } 305 | } 306 | 307 | const dom = TestUtils.renderIntoDocument( 308 | 309 | 310 | 311 | ) 312 | 313 | const button = TestUtils.findRenderedDOMComponentWithTag(dom, 'button') 314 | expect(button).toBeDefined() 315 | 316 | TestUtils.Simulate.click(button) 317 | }) 318 | }) 319 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './MakeAsyncFunction' 3 | -------------------------------------------------------------------------------- /src/index.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from 'react' 3 | import type { SetPayload, GetPayload } from 'redux-promise-listener' 4 | 5 | type Props = { 6 | start: string, 7 | resolve: string, 8 | reject: string, 9 | setPayload?: SetPayload, 10 | getPayload?: GetPayload, 11 | getError?: GetPayload 12 | } 13 | 14 | declare export default React.ComponentType 15 | --------------------------------------------------------------------------------