redux-promise-middleware
Advanced tools
+4
| { | ||
| "stage": 0, | ||
| "loose": "all" | ||
| } |
| language: node_js | ||
| node_js: | ||
| - "4.1" |
| { | ||
| "stage": 0, | ||
| "loose": [ | ||
| "es6.modules" | ||
| ], | ||
| "env": { | ||
| "development": { | ||
| "plugins": [ | ||
| "react-transform" | ||
| ], | ||
| "extra": { | ||
| "react-transform": [ | ||
| { | ||
| "target": "react-transform-hmr", | ||
| "imports": ["react"], | ||
| "locals": ["module"] | ||
| }, | ||
| { | ||
| "target": "react-transform-catch-errors", | ||
| "imports": ["react", "redbox-react"] | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| "production": { | ||
| "plugins": [ | ||
| "react-display-name" | ||
| ] | ||
| } | ||
| } | ||
| } |
| import * as types from '../constants/post'; | ||
| import * as utils from '../utils/server/resources/post'; | ||
| export function getAllPosts() { | ||
| return { | ||
| type: types.GET_POSTS, | ||
| payload: { | ||
| promise: utils.getAllPosts(), | ||
| onSuccess: () => null, | ||
| onError: () => null | ||
| } | ||
| }; | ||
| } |
| import React, { Component } from 'react'; | ||
| import { render } from 'react-dom'; | ||
| import { Provider } from 'react-redux'; | ||
| import store from './store'; | ||
| class App extends Component { | ||
| render() { | ||
| return ( | ||
| <div className="app-container"> | ||
| <Provider store={store()}> | ||
| <h1>Hi</h1> | ||
| </Provider> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
| render(<App />, document.querySelector('#mount')); |
| export const GET_POSTS = 'GET_POSTS'; |
| <!doctype html><body><div id="mount"></div><script src="/app.js"></script></body> |
| { | ||
| "private": true, | ||
| "author": "Patrick Burtchaell <patrick@pburtchaell.com>", | ||
| "license": "MIT", | ||
| "scripts": { | ||
| "start": "NODE_ENV=development node server.js" | ||
| }, | ||
| "devDependencies": { | ||
| "babel-core": "^5.8.25", | ||
| "babel-loader": "^5.3.2", | ||
| "babel-plugin-react-display-name": "^2.0.0", | ||
| "babel-plugin-react-transform": "^1.1.1", | ||
| "eventsource-polyfill": "^0.9.6", | ||
| "express": "^4.13.3", | ||
| "morgan": "^1.6.1", | ||
| "react-transform-catch-errors": "^1.0.0", | ||
| "react-transform-hmr": "^1.0.1", | ||
| "redbox-react": "^1.1.1", | ||
| "webpack": "^1.12.2", | ||
| "webpack-dev-middleware": "^1.2.0", | ||
| "webpack-hot-middleware": "^2.2.1" | ||
| }, | ||
| "dependencies": { | ||
| "core-js": "^1.2.0", | ||
| "isomorphic-fetch": "^2.1.1", | ||
| "js-schema": "^1.0.1", | ||
| "react": "^0.14.0", | ||
| "react-dom": "^0.14.0", | ||
| "react-redux": "^4.0.0", | ||
| "redux": "^3.0.4", | ||
| "regenerator": "^0.8.40" | ||
| } | ||
| } |
| # Promise Middleware Example | ||
| This example demonstrates using the promise middleware to make async requests to a REST API that follows [the JSON API standard v1.0](http://jsonapi.org). | ||
| Uses: | ||
| - Redux 3.x | ||
| - React 14.x + React DOM | ||
| - Fetch API | ||
| - Babel + WebPack | ||
| ## Getting Started | ||
| 1. Install dependencies `npm install` | ||
| 2. Start the app: `npm start` |
| export default { | ||
| isPending: null, | ||
| isFulfilled: null, | ||
| isRejected: null, | ||
| error: null, | ||
| data: null | ||
| }; |
| import { combineReducers } from 'redux'; | ||
| export default combineReducers({ | ||
| posts: require('./posts') | ||
| }); |
| import * as types from '../constants/post'; | ||
| import defaultState from './defaultState'; | ||
| /* | ||
| * @function post | ||
| * @description This reducer holds the state of a post after it is fetched | ||
| * from the server. | ||
| * @param {object} state The previous state | ||
| * @param {object} action The dispatched action | ||
| * @returns {object} state The updated state | ||
| */ | ||
| export default function posts(state = defaultState, action) { | ||
| switch (action.type) { | ||
| case `${types.GET_POSTS}_PENDING`: | ||
| return { | ||
| ...defaultState, | ||
| isPending: true | ||
| }; | ||
| case `${types.GET_POSTS}_FULFILLED`: | ||
| return { | ||
| ...defaultState, | ||
| isFulfilled: true, | ||
| error: false, | ||
| data: action.payload | ||
| }; | ||
| case `${types.GET_POSTS}_REJECTED`: | ||
| return { | ||
| ...defaultState, | ||
| isRejected: true, | ||
| error: action.payload | ||
| }; | ||
| default: return state; | ||
| } | ||
| } |
| var path = require('path'); | ||
| var express = require('express'); | ||
| var webpack = require('webpack'); | ||
| var config = require('./webpack.local.config'); | ||
| var port = process.env.PORT || config.devPort; | ||
| var address = config.devAddress; | ||
| var app = express(); | ||
| var compiler = webpack(config); | ||
| // Logging | ||
| app.use(require('morgan')('short')); | ||
| app.use(require('webpack-dev-middleware')(compiler, { | ||
| noInfo: true, | ||
| publicPath: config.output.publicPath | ||
| })); | ||
| app.use(require('webpack-hot-middleware')(compiler, { | ||
| log: console.log, | ||
| path: '/__webpack_hmr', | ||
| heartbeat: 10 * 1000 | ||
| })); | ||
| app.get('*', function (req, res) { | ||
| res.sendFile(path.join(__dirname, 'index.html')); | ||
| }); | ||
| /** | ||
| * Mock a fake REST API for creating posts. | ||
| * | ||
| app.get('/api/v1/posts', function (req, res) { | ||
| res.json({ | ||
| data: [ | ||
| { | ||
| type: 'posts', | ||
| id: '1LOL234', | ||
| attributes: { | ||
| title: res.param.title, | ||
| author: res.param.author, | ||
| body: res.param.body | ||
| } | ||
| } | ||
| ] | ||
| }); | ||
| });*/ | ||
| /** | ||
| * Mock a fake REST API for getting posts. | ||
| * | ||
| app.post('/api/v1/posts/:id', function (req, res) { | ||
| var id = req.param.id; | ||
| res.json({ | ||
| data: [ | ||
| { | ||
| type: 'posts', | ||
| id: 'post-' + id, | ||
| attributes: { | ||
| title: 'This is teh post title for ' + id, | ||
| author: 'Yo', | ||
| body: 'This is the post text for ' + id | ||
| } | ||
| } | ||
| ] | ||
| }); | ||
| });*/ | ||
| app.listen(port, address, function (error) { | ||
| if (error) throw error; | ||
| console.log('server running at http://%s:%d', address, port); | ||
| }); |
| import { createStore, applyMiddleware } from 'redux'; | ||
| import reducers from './reducers'; | ||
| const createStoreWithMiddleware = applyMiddleware( | ||
| require('./utils/middleware/promise')() | ||
| )(createStore); | ||
| export default function store(initialState) { | ||
| const store = createStoreWithMiddleware(reducers, initialState); | ||
| if (module.hot) { | ||
| require('eventsource-polyfill'); | ||
| // Enable Webpack hot module replacement for reducers | ||
| module.hot.accept('./reducers', () => { | ||
| const nextRootReducer = require('./reducers'); | ||
| store.replaceReducer(reducers); | ||
| }); | ||
| } | ||
| if (NODE_ENV === 'development') { | ||
| window.store = store.getState(); | ||
| } | ||
| return store; | ||
| } |
| export default function buildURL(resource, id) { | ||
| const base = '/api/v1'; | ||
| return id ? `${base}/${resource}/${id}` : `${base}/${resource}`; | ||
| }; |
| export default function isPromise(value) { | ||
| if (value !== null && typeof value === 'object') { | ||
| return value.promise && typeof value.promise.then === 'function'; | ||
| } | ||
| } |
| import isPromise from './isPromise'; | ||
| const defaultTypes = ['PENDING', 'FULFILLED', 'REJECTED']; | ||
| export default function promiseMiddleware(config={}) { | ||
| const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypes; | ||
| return () => { | ||
| return next => action => { | ||
| if (!isPromise(action.payload)) { | ||
| return next(action); | ||
| } | ||
| const { type, payload, meta } = action; | ||
| const { promise, data } = payload; | ||
| const [ PENDING, FULFILLED, REJECTED ] = (meta || {}).promiseTypeSuffixes || promiseTypeSuffixes; | ||
| /** | ||
| * Dispatch the first async handler. This tells the | ||
| * reducer that an async action has been dispatched. | ||
| */ | ||
| next({ | ||
| type: `${type}_${PENDING}`, | ||
| payload: data, | ||
| ...meta ? { meta } : {} | ||
| }); | ||
| /** | ||
| * Return either the fulfilled action object or the rejected | ||
| * action object. | ||
| */ | ||
| return promise.then( | ||
| payload => next({ // eslint-disable-line no-shadow | ||
| payload, | ||
| type: `${type}_${FULFILLED}`, | ||
| ...meta ? { meta } : {} | ||
| }), | ||
| error => next({ | ||
| payload: error, | ||
| error: true, | ||
| type: `${type}_${REJECTED}`, | ||
| ...meta ? { meta } : {} | ||
| }) | ||
| ); | ||
| }; | ||
| }; | ||
| } |
| import 'core-js/shim'; | ||
| import 'regenerator/runtime'; | ||
| import 'isomorphic-fetch'; | ||
| /** | ||
| * @private | ||
| * @function request | ||
| * @description Make a request to the server and return a promise. | ||
| * @param {string} url | ||
| * @param {object} options | ||
| * @returns {promise} | ||
| */ | ||
| export default function request(url, options) { | ||
| return new Promise((resolve, reject) => { | ||
| if (!url) { | ||
| reject(new Error('There is no URL provided for the request.')); | ||
| } | ||
| if (!options) { | ||
| reject(new Error('There are no options provided for the request.')); | ||
| } | ||
| fetch(url, options).then(response => { | ||
| return response.json(); | ||
| }).then(response => { | ||
| if (response.status >= 200 && response.status < 300) { | ||
| return response.errors ? reject(response.errors) : reject(response); | ||
| } else { | ||
| return resolve(response) | ||
| } | ||
| }).catch(error => { | ||
| reject(error); | ||
| }); | ||
| }); | ||
| } |
| const Post = require('../server')('post'); | ||
| export function getAllPosts() { | ||
| return Post.get(); | ||
| } |
| import request from './request'; | ||
| import buildURL from './buildURL'; | ||
| /** | ||
| * @function Server | ||
| * @description Factory function to create a object that can send | ||
| * requests to a specific resource on the server. | ||
| * @param {string} resource The resource used for config | ||
| */ | ||
| export const Server = (resource) => { | ||
| // Default options used for every request | ||
| const defaultOptions = { | ||
| mode: 'cors', | ||
| headers: { | ||
| 'Accept': 'application/json', | ||
| 'Content-Type': 'application/json', | ||
| 'X-Parse-Application-Id': PARSE_APPLICATION_ID, | ||
| 'X-Parse-REST-API-Key': PARSE_REST_API_KEY | ||
| } | ||
| }; | ||
| return { | ||
| /** | ||
| * @function post | ||
| * @description Make a POST request. | ||
| * @param {string} path | ||
| * @param {object} body | ||
| * @param {object} options | ||
| * @returns {promise} | ||
| */ | ||
| post: (path, body, options = {}) => { | ||
| return request(buildURL(path), Object.assign( | ||
| options, | ||
| defaultOptions, | ||
| { | ||
| method: 'POST', | ||
| body: JSON.stringify(body) | ||
| } | ||
| )); | ||
| }, | ||
| /** | ||
| * @function post | ||
| * @description Make a GET request. | ||
| * @param {string} path | ||
| * @param {object} options | ||
| * @returns {promise} | ||
| */ | ||
| get: (path, options = {}) => { | ||
| return request(buildURL(path), Object.assign( | ||
| options, | ||
| defaultOptions, | ||
| { method: 'GET' } | ||
| )); | ||
| }, | ||
| /** | ||
| * @function edit | ||
| * @description Make a PUT request. | ||
| * @param {string} path | ||
| * @param {object} body | ||
| * @param {object} options | ||
| * @returns {promise} | ||
| */ | ||
| edit: (path, body, options = {}) => { | ||
| return request(buildURL(path), Object.assign( | ||
| options, | ||
| defaultOptions, | ||
| { method: 'PUT' } | ||
| )); | ||
| }, | ||
| /** | ||
| * @function delete | ||
| * @description Make a DELETE request. | ||
| * @param {string} path | ||
| * @param {object} options | ||
| * @returns {promise} | ||
| */ | ||
| delete: (path, options = {}) => { | ||
| return request(buildURL(path), Object.assign( | ||
| options, | ||
| defaultOptions, | ||
| { method: 'DELETE' } | ||
| )); | ||
| } | ||
| }; | ||
| }; |
| var path = require('path'); | ||
| var webpack = require('webpack'); | ||
| var pathToReact = '/node_modules/react/react.js'; | ||
| var pathToRedux = '/node_modules/redux/lib/index.js'; | ||
| var pathToReduxReact = '/node_modules/redux/react.js'; | ||
| // Local development server port and address | ||
| var port = 8000; | ||
| var address = '0.0.0.0'; | ||
| /** | ||
| * This is the Webpack configuration file for local development. It contains | ||
| * local-specific configuration such as the React Hot Loader, as well as: | ||
| * - The entry point of the application | ||
| * - Where the output file should be | ||
| * - Which loaders to use on what files to properly transpile the source | ||
| * For more information, see: http://webpack.github.io/docs/configuration.html | ||
| */ | ||
| module.exports = { | ||
| // Efficiently evaluate modules with source maps | ||
| devtool: 'eval', | ||
| // Configure local server | ||
| devPort: port, | ||
| devAddress: address, | ||
| // Cache the build | ||
| cache: true, | ||
| entry: { | ||
| app: [ | ||
| 'webpack-hot-middleware/client', | ||
| path.resolve(__dirname, './client') | ||
| ] | ||
| }, | ||
| /** | ||
| * Instead of making Webpack go through React and all its dependencies, | ||
| * you can override the behavior in development. | ||
| */ | ||
| resolve: { | ||
| extensions: ['', '.js', '.less', '.woff', '.woff2', '.png', '.jpg'], | ||
| modulesDirectories: ['node_modules', 'app'] | ||
| }, | ||
| /** | ||
| * This will not actually create a bundle.js file in ./dist. | ||
| * It is used by the dev server for dynamic hot loading. | ||
| */ | ||
| output: { | ||
| path: path.join(__dirname, 'dist'), | ||
| filename: '[name].js', | ||
| publicPath: '/' | ||
| }, | ||
| plugins: [ | ||
| new webpack.HotModuleReplacementPlugin(), | ||
| new webpack.NoErrorsPlugin(), | ||
| new webpack.DefinePlugin({ | ||
| NODE_ENV: JSON.stringify(process.env.NODE_ENV) | ||
| }) | ||
| ], | ||
| module: { | ||
| loaders: [ | ||
| { | ||
| test: /\.js$/, | ||
| exclude: /node_modules/, | ||
| loader: 'babel' | ||
| } | ||
| ], | ||
| noParse: [ | ||
| pathToReact, | ||
| pathToRedux, | ||
| pathToReduxReact | ||
| ] | ||
| } | ||
| }; |
+12
| BIN = `npm bin` | ||
| SRC_JS = $(shell find src -name "*.js") | ||
| DIST_JS = $(patsubst src/%.js, dist/%.js, $(SRC_JS)) | ||
| $(DIST_JS): dist/%.js: src/%.js | ||
| @mkdir -p $(dir $@) | ||
| @$(BIN)/babel $< -o $@ | ||
| # Task: js | ||
| # Builds distribution JS files for publishing to npm. | ||
| js: $(DIST_JS) |
| import chai, { expect } from 'chai'; | ||
| import sinon from 'sinon'; | ||
| import sinonChai from 'sinon-chai'; | ||
| import { createStore, applyMiddleware } from 'redux'; | ||
| import configureStore from 'redux-mock-store'; | ||
| import promiseMiddleware from '../src'; | ||
| chai.use(sinonChai); | ||
| describe('Promise Middleware:', () => { | ||
| const nextHandler = promiseMiddleware(); | ||
| it('must return a function to handle next', () => { | ||
| expect(nextHandler).to.be.a('function'); | ||
| expect(nextHandler.length).to.eql(1); | ||
| }); | ||
| /* | ||
| Make two fake middleware to surround promiseMiddleware in chain, | ||
| Give both of them a spy property to assert on their usage | ||
| */ | ||
| // first middleware mimics redux-thunk | ||
| function firstMiddlewareThunk(ref, next) { | ||
| this.spy = sinon.spy((action) => | ||
| typeof action === 'function' | ||
| ? action(ref.dispatch, ref.getState) | ||
| : next(action) | ||
| ); | ||
| return this.spy; | ||
| } | ||
| // final middleware returns the action merged with dummy data | ||
| const lastMiddlewareModfiesObject = { val: 'added-by-last-middleware' }; | ||
| function lastMiddlewareModfies(next) { | ||
| this.spy = sinon.spy((action) => { | ||
| next(action); | ||
| return { | ||
| ...action, | ||
| ...lastMiddlewareModfiesObject | ||
| }; | ||
| }); | ||
| return this.spy; | ||
| } | ||
| /* | ||
| Function for creating a dumb store using fake middleware stack | ||
| */ | ||
| const makeStore = (config) => applyMiddleware( | ||
| (ref) => (next) => firstMiddlewareThunk.call(firstMiddlewareThunk, ref, next), | ||
| promiseMiddleware(config), | ||
| () => (next) => lastMiddlewareModfies.call(lastMiddlewareModfies, next) | ||
| )(createStore)(()=>null); | ||
| let store; | ||
| beforeEach(()=> { | ||
| store = makeStore(); | ||
| }); | ||
| afterEach(()=> { | ||
| firstMiddlewareThunk.spy.reset(); | ||
| lastMiddlewareModfies.spy.reset(); | ||
| }); | ||
| context('When Action is Not a Promise:', () => { | ||
| const mockAction = { type: 'NOT_PROMISE' }; | ||
| it('invokes next with the action', () => { | ||
| store.dispatch(mockAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(mockAction); | ||
| }); | ||
| it('returns the return from next middleware', () => { | ||
| expect(store.dispatch(mockAction)).to.eql({ | ||
| ...mockAction, | ||
| ...lastMiddlewareModfiesObject | ||
| }); | ||
| }); | ||
| it('does not dispatch any other actions', () => { | ||
| const mockStore = configureStore([promiseMiddleware()]); | ||
| store = mockStore({}); | ||
| store.dispatch(mockAction); | ||
| expect(store.getActions()).to.eql([mockAction]); | ||
| }); | ||
| }); | ||
| context('When Action Has Promise Payload:', () => { | ||
| let promiseAction; | ||
| let pendingAction; | ||
| beforeEach(()=> { | ||
| promiseAction = { | ||
| type: 'HAS_PROMISE', | ||
| payload: { | ||
| promise: Promise.resolve() | ||
| } | ||
| }; | ||
| pendingAction = { | ||
| type: `${promiseAction.type}_PENDING` | ||
| }; | ||
| }); | ||
| it('dispatches a pending action', () => { | ||
| store.dispatch(promiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(pendingAction); | ||
| }); | ||
| it('optionally contains optimistic update payload from data property', () => { | ||
| const optimisticUpdate = { fake: 'data' }; | ||
| // data from promise becomes payload of pending | ||
| promiseAction.payload.data = optimisticUpdate; | ||
| pendingAction.payload = optimisticUpdate; | ||
| store.dispatch(promiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(pendingAction); | ||
| }); | ||
| it('optionally contains meta property', () => { | ||
| const meta = { fake: 'data' }; | ||
| promiseAction.meta = meta; | ||
| pendingAction.meta = meta; | ||
| store.dispatch(promiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(pendingAction); | ||
| }); | ||
| it('allows customisation of global pending action.type', () => { | ||
| const customPrefix = 'PENDIDDLE'; | ||
| store = makeStore({ | ||
| promiseTypeSuffixes: [customPrefix, '', ''] | ||
| }); | ||
| pendingAction.type = `${promiseAction.type}_${customPrefix}`; | ||
| store.dispatch(promiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(pendingAction); | ||
| }); | ||
| it('allows customisation of pending action.type per dispatch', () => { | ||
| const customPrefix = 'PENDOODDLE'; | ||
| const actionMeta = { promiseTypeSuffixes: [customPrefix, '', ''] }; | ||
| promiseAction.meta = actionMeta; | ||
| pendingAction.type = `${promiseAction.type}_${customPrefix}`; | ||
| // FIXME: Test leak, should the promiseTypeSuffixes be in other actions? | ||
| pendingAction.meta = actionMeta; | ||
| store.dispatch(promiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(pendingAction); | ||
| }); | ||
| it('returns the new promise object', () => { | ||
| expect(store.dispatch(promiseAction)).to.eql(promiseAction.payload.promise); | ||
| }); | ||
| context('When Promise Rejects:', ()=> { | ||
| let rejectingPromiseAction; | ||
| let rejectedAction; | ||
| let rejectValue; | ||
| beforeEach(()=> { | ||
| rejectValue = { test: 'rejected value' }; | ||
| rejectingPromiseAction = { | ||
| type: 'HAS_REJECTING_PROMISE', | ||
| payload: { | ||
| promise: Promise.reject(rejectValue) | ||
| } | ||
| }; | ||
| rejectedAction = { | ||
| type: `${rejectingPromiseAction.type}_REJECTED`, | ||
| error: true, | ||
| payload: rejectValue | ||
| }; | ||
| pendingAction = { | ||
| type: `${rejectingPromiseAction.type}_PENDING` | ||
| }; | ||
| }); | ||
| it('dispatches both pending and rejected', () => { | ||
| const mockStore = configureStore([promiseMiddleware()]); | ||
| store = mockStore({}); | ||
| return store.dispatch(rejectingPromiseAction).catch(() => { | ||
| expect(store.getActions()).to.eql([pendingAction, rejectedAction]); | ||
| }); | ||
| }); | ||
| it('re-dispatches rejected action with error and payload from error', () => { | ||
| return store.dispatch(rejectingPromiseAction).catch(() => | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(rejectedAction) | ||
| ); | ||
| }); | ||
| it('works when resolve is null', () => { | ||
| rejectingPromiseAction.payload.promise = Promise.reject(null); | ||
| rejectedAction = { | ||
| type: `${rejectingPromiseAction.type}_REJECTED`, | ||
| error: true | ||
| }; | ||
| return store.dispatch(rejectingPromiseAction).catch(() => | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(rejectedAction) | ||
| ); | ||
| }); | ||
| it('persists meta from original action', () => { | ||
| const metaData = { fake: 'data' }; | ||
| rejectingPromiseAction.meta = metaData; | ||
| rejectedAction.meta = metaData; | ||
| return store.dispatch(rejectingPromiseAction).catch(() => | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(rejectedAction) | ||
| ); | ||
| }); | ||
| it('allows promise to resolve thunk, pre-bound to rejected action', () => { | ||
| const thunkResolve = (action, dispatch, getState) => { | ||
| expect(action).to.eql({ | ||
| type: `${rejectingPromiseAction.type}_REJECTED`, | ||
| error: true | ||
| }); | ||
| expect(getState()).to.equal(store.getState()); | ||
| dispatch({ ...action, foo: 'bar' }); | ||
| }; | ||
| rejectingPromiseAction.payload.promise = Promise.reject(thunkResolve); | ||
| return store.dispatch(rejectingPromiseAction).catch(() => | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith({ | ||
| type: `${rejectingPromiseAction.type}_REJECTED`, | ||
| error: true, | ||
| foo: 'bar' | ||
| }) | ||
| ); | ||
| }); | ||
| it('allows customisation of global rejected action.type', () => { | ||
| const customPrefix = 'REJIGGLED'; | ||
| store = makeStore({ | ||
| promiseTypeSuffixes: ['', '', customPrefix] | ||
| }); | ||
| rejectedAction.type = `${rejectingPromiseAction.type}_${customPrefix}`; | ||
| return store.dispatch(rejectingPromiseAction).catch(() => | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(rejectedAction) | ||
| ); | ||
| }); | ||
| it('allows customisation of rejected action.type per dispatch', () => { | ||
| const customPrefix = 'REJOOGGLED'; | ||
| const actionMeta = { promiseTypeSuffixes: ['', '', customPrefix] }; | ||
| rejectingPromiseAction.meta = actionMeta; | ||
| rejectedAction.type = `${rejectingPromiseAction.type}_${customPrefix}`; | ||
| // FIXME: Test leak, should the promiseTypeSuffixes be in other actions? | ||
| rejectedAction.meta = actionMeta; | ||
| return store.dispatch(rejectingPromiseAction).catch(() => | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(rejectedAction) | ||
| ); | ||
| }); | ||
| }); | ||
| context('When Promise Fulfills', ()=> { | ||
| let fulfillingPromiseAction; | ||
| let fulfillingAction; | ||
| let fulfilledValue; | ||
| beforeEach(()=> { | ||
| fulfilledValue = { test: 'fulfilled value' }; | ||
| fulfillingPromiseAction = { | ||
| type: 'HAS_FULFILLING_PROMISE', | ||
| payload: { | ||
| promise: Promise.resolve(fulfilledValue) | ||
| } | ||
| }; | ||
| fulfillingAction = { | ||
| type: `${fulfillingPromiseAction.type}_FULFILLED`, | ||
| payload: fulfilledValue | ||
| }; | ||
| pendingAction = { | ||
| type: `${fulfillingPromiseAction.type}_PENDING` | ||
| }; | ||
| }); | ||
| it('dispatches both pending and fulfilled', () => { | ||
| const mockStore = configureStore([promiseMiddleware()]); | ||
| store = mockStore({}); | ||
| store.dispatch(fulfillingPromiseAction).then(() => { | ||
| expect(store.getActions()).to.eql([pendingAction, fulfillingAction]); | ||
| }); | ||
| }); | ||
| it('re-dispatches fulfilled action with payload from promise', async () => { | ||
| await store.dispatch(fulfillingPromiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(fulfillingAction); | ||
| }); | ||
| it('works when resolve is null', async () => { | ||
| fulfillingPromiseAction.payload.promise = Promise.resolve(null); | ||
| fulfillingAction = { | ||
| type: `${fulfillingPromiseAction.type}_FULFILLED` | ||
| }; | ||
| await store.dispatch(fulfillingPromiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(fulfillingAction); | ||
| }); | ||
| it('persists meta from original action', async () => { | ||
| const metaData = { fake: 'data' }; | ||
| fulfillingPromiseAction.meta = metaData; | ||
| fulfillingAction.meta = metaData; | ||
| await store.dispatch(fulfillingPromiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(fulfillingAction); | ||
| }); | ||
| it('allows promise to resolve thunk, pre-bound to fulfilled action', async () => { | ||
| const thunkResolve = (action, dispatch, getState) => { | ||
| expect(action).to.eql({ | ||
| type: `${fulfillingPromiseAction.type}_FULFILLED` | ||
| }); | ||
| expect(getState()).to.equal(store.getState()); | ||
| dispatch({ ...action, foo: 'bar' }); | ||
| }; | ||
| fulfillingPromiseAction.payload.promise = Promise.resolve(thunkResolve); | ||
| await store.dispatch(fulfillingPromiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith({ | ||
| type: `${fulfillingPromiseAction.type}_FULFILLED`, | ||
| foo: 'bar' | ||
| }); | ||
| }); | ||
| it('allows customisation of global fulfilled action.type', async () => { | ||
| const customPrefix = 'FULFIDDLED'; | ||
| store = makeStore({ | ||
| promiseTypeSuffixes: ['', customPrefix, ''] | ||
| }); | ||
| fulfillingAction.type = `${fulfillingPromiseAction.type}_${customPrefix}`; | ||
| await store.dispatch(fulfillingPromiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(fulfillingAction); | ||
| }); | ||
| it('allows customisation of fulfilled action.type per dispatch', async () => { | ||
| const customPrefix = 'FULFOODDLED'; | ||
| const actionMeta = { promiseTypeSuffixes: ['', customPrefix, ''] }; | ||
| fulfillingPromiseAction.meta = actionMeta; | ||
| fulfillingAction.type = `${fulfillingPromiseAction.type}_${customPrefix}`; | ||
| // FIXME: Test leak, should the promiseTypeSuffixes be in other actions? | ||
| fulfillingAction.meta = actionMeta; | ||
| await store.dispatch(fulfillingPromiseAction); | ||
| expect(lastMiddlewareModfies.spy).to.have.been.calledWith(fulfillingAction); | ||
| }); | ||
| it('should log errors made during dispatch within promise', async (done)=> { | ||
| const testError = new Error('test error'); | ||
| sinon.stub(console, 'error'); | ||
| store = applyMiddleware( | ||
| promiseMiddleware(), | ||
| )(createStore)((state, action)=> { | ||
| if (action.type === fulfillingAction.type) { | ||
| throw testError; | ||
| } | ||
| }); | ||
| await store.dispatch(fulfillingPromiseAction); | ||
| setTimeout(() => { | ||
| expect( | ||
| console.error // eslint-disable-line | ||
| ).to.have.been.calledWith(testError); | ||
| console.error.restore(); // eslint-disable-line | ||
| done(); | ||
| }, 0); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
+18
-15
@@ -16,2 +16,3 @@ 'use strict'; | ||
| var defaultTypes = ['PENDING', 'FULFILLED', 'REJECTED']; | ||
| var IS_ERROR = true; | ||
@@ -50,14 +51,11 @@ function promiseMiddleware() { | ||
| type: type + '_' + PENDING | ||
| }, !!data ? { payload: data } : {}, !!meta ? { meta: meta } : {})); | ||
| }, !data ? {} : { payload: data }, !meta ? {} : { meta: meta })); | ||
| var isAction = function isAction(resolved) { | ||
| return resolved && (resolved.meta || resolved.payload); | ||
| }; | ||
| var isThunk = function isThunk(resolved) { | ||
| return typeof resolved === 'function'; | ||
| }; | ||
| var getResolveAction = function getResolveAction(isError) { | ||
| var getPartialAction = function getPartialAction(isError) { | ||
| return _extends({ | ||
| type: type + '_' + (isError ? REJECTED : FULFILLED) | ||
| }, !!meta ? { meta: meta } : {}, !!isError ? { error: true } : {}); | ||
| }, !meta ? {} : { meta: meta }, !isError ? {} : { error: true }); | ||
| }; | ||
@@ -68,18 +66,23 @@ | ||
| * 1. a thunk, bound to a resolved/rejected object containing ?meta and type | ||
| * 2. the resolved/rejected object, if it looks like an action, merged into action | ||
| * 3. a resolve/rejected action with the resolve/rejected object as a payload | ||
| * 2. a resolve/rejected action with the resolve/rejected object as a payload | ||
| */ | ||
| action.payload.promise = promise.then(function () { | ||
| promise.then(function () { | ||
| var resolved = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
| var resolveAction = getResolveAction(); | ||
| return dispatch(isThunk(resolved) ? resolved.bind(null, resolveAction) : _extends({}, resolveAction, isAction(resolved) ? resolved : _extends({}, !!resolved && { payload: resolved }))); | ||
| var resolveAction = getPartialAction(); | ||
| dispatch(isThunk(resolved) ? resolved.bind(null, resolveAction) : _extends({}, resolveAction, !resolved ? {} : { payload: resolved })); | ||
| }, function () { | ||
| var rejected = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
| var resolveAction = getResolveAction(true); | ||
| return dispatch(isThunk(rejected) ? rejected.bind(null, resolveAction) : _extends({}, resolveAction, isAction(rejected) ? rejected : _extends({}, !!rejected && { payload: rejected }))); | ||
| }); | ||
| var rejectedAction = getPartialAction(IS_ERROR); | ||
| dispatch(isThunk(rejected) ? rejected.bind(null, rejectedAction) : _extends({}, rejectedAction, !rejected ? {} : { payload: rejected })); | ||
| })['catch'](function (error) { | ||
| return( | ||
| // log out any errors thrown as a result of the dispatch in this promise | ||
| console.error(error) | ||
| ); | ||
| } // eslint-disable-line | ||
| ); | ||
| return action; | ||
| return promise; | ||
| }; | ||
@@ -86,0 +89,0 @@ }; |
+6
-6
| { | ||
| "name": "redux-promise-middleware", | ||
| "version": "2.3.3", | ||
| "version": "2.4.0", | ||
| "description": "Redux middleware for handling promises", | ||
| "main": "dist/index.js", | ||
| "scripts": { | ||
| "pretest": "`npm bin`/eslint ./src/*.js", | ||
| "pretest": "eslint ./src/*.js", | ||
| "test": "mocha --compilers js:babel/register --reporter spec test/*.js", | ||
@@ -34,2 +34,3 @@ "prepublish": "npm test && make js" | ||
| "chai": "^3.4.0", | ||
| "escope": "^3.3.0", | ||
| "eslint": "^0.24.1", | ||
@@ -40,4 +41,4 @@ "eslint-config-airbnb": "0.0.6", | ||
| "redux": "^3.0.4", | ||
| "redux-mock-store": "0.0.2", | ||
| "sinon": "^1.17.2", | ||
| "redux-mock-store": "^1.0.2", | ||
| "sinon": "^1.17.3", | ||
| "sinon-chai": "^2.8.0" | ||
@@ -47,4 +48,3 @@ }, | ||
| "redux": "^2.0.0 || ^3.0.0" | ||
| }, | ||
| "dependencies": {} | ||
| } | ||
| } |
+4
-6
| # Redux Promise Middleware | ||
| [](https://www.npmjs.com/package/redux-promise-middleware) [](https://travis-ci.org/pburtchaell/redux-promise-middleware) [](https://coveralls.io/github/pburtchaell/redux-promise-middleware?branch=master) [](https://www.npmjs.com/package/redux-promise-middleware) | ||
| [](https://www.npmjs.com/package/redux-promise-middleware) [](https://travis-ci.org/pburtchaell/redux-promise-middleware) [](https://www.npmjs.com/package/redux-promise-middleware) | ||
@@ -38,3 +38,3 @@ # Getting Started | ||
| The middleware returns a [FSA compliant](https://github.com/acdlite/flux-standard-action) action for both rejected and resolved/fulfilled promises. In the case of a rejected promise, an `error` is returned. You can access the promise of the action with `payload.promise`. This is useful for chaining actions together or using `async...await` within an action creator. | ||
| The middleware returns a [FSA compliant](https://github.com/acdlite/flux-standard-action) action for both rejected and resolved/fulfilled promises. In the case of a rejected promise, an `error` is returned. | ||
@@ -60,3 +60,3 @@ ## What is the difference between this and other promise middleware? | ||
| promise: Promise.resolve({ | ||
| type: 'SECOND_ACTION_TYPE' | ||
| type: 'SECEOND_ACTION_TYPE' | ||
| payload: ... | ||
@@ -74,3 +74,3 @@ }) | ||
| payload: { | ||
| promise: Promise.resolve((action, dispatch, getState) => { | ||
| promise: Promise.resolve((dispatch, getState) => { | ||
| dispatch({ type: 'SECEOND_ACTION_TYPE', payload: ... }) | ||
@@ -83,4 +83,2 @@ dispatch(someActionCreator()) | ||
| Note that this behavior uses thunks, so you will need to include [thunk middleware](https://github.com/gaearon/redux-thunk) in your middleware stack. | ||
| ## Type suffix configuration | ||
@@ -87,0 +85,0 @@ |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
35908
234%32
433.33%793
957.33%12
9.09%111
-1.77%3
Infinity%4
Infinity%