├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── package.json ├── src └── index.js └── test ├── index.js └── mocha.opts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread", 7 | "add-module-exports" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | after_script: 5 | - npm run coveralls 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/travis/markdalgleish/redux-tap/master.svg?style=flat-square)](http://travis-ci.org/markdalgleish/redux-tap) [![Coverage Status](https://img.shields.io/coveralls/markdalgleish/redux-tap/master.svg?style=flat-square)](https://coveralls.io/r/markdalgleish/redux-tap) [![npm](https://img.shields.io/npm/v/redux-tap.svg?style=flat-square)](https://www.npmjs.com/package/redux-tap) 2 | 3 | # redux-tap 4 | 5 | Simple side-effect middleware for [Redux](https://github.com/reactjs/redux). 6 | 7 | ```bash 8 | $ npm install --save redux-tap 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | import { createStore, applyMiddleware } from 'redux'; 15 | import tap from 'redux-tap'; 16 | import rootReducer from './reducers'; 17 | 18 | // For example, select any action with metadata: 19 | const selectMeta = action => action.meta; 20 | 21 | // Once selected, access the selected value, action and store: 22 | const middleware = tap(selectMeta, (meta, action, store) => { 23 | // In this case, we'll simply log to the console: 24 | console.log(meta, action, store); 25 | }); 26 | 27 | // Note: this API requires redux@>=3.1.0 28 | const store = createStore( 29 | rootReducer, 30 | applyMiddleware(middleware) 31 | ); 32 | ``` 33 | 34 | As a real-world example, you can use redux-tap to declaratively track analytics events: 35 | 36 | ```js 37 | import { createStore, applyMiddleware } from 'redux'; 38 | import tap from 'redux-tap'; 39 | import rootReducer from './reducers'; 40 | import { track } from 'my-analytics-lib'; 41 | 42 | const selectAnalytics = ({ meta }) => meta && meta.analytics; 43 | const middleware = tap(selectAnalytics, ({ event, data }) => { 44 | track(event, data); 45 | }); 46 | 47 | const store = createStore( 48 | rootReducer, 49 | applyMiddleware(middleware) 50 | ); 51 | 52 | // Now, you can declare analytics metadata on any action: 53 | dispatch({ 54 | type: 'REPO_STARRED', 55 | payload: { id }, 56 | meta: { 57 | analytics: { 58 | event: 'Repo Starred', 59 | data: { id } 60 | } 61 | } 62 | }); 63 | ``` 64 | 65 | ## License 66 | 67 | [MIT License](http://markdalgleish.mit-license.org/) 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-tap", 3 | "version": "0.2.0", 4 | "description": "Simple side-effect middleware for Redux", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "babel-node ./node_modules/istanbul/lib/cli.js cover _mocha && istanbul check-coverage --branches 100", 8 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 9 | "build": "rm -rf lib/ && babel -d lib/ src/", 10 | "prepublish": "npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/markdalgleish/redux-tap.git" 15 | }, 16 | "author": "Mark Dalgleish", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/markdalgleish/redux-tap/issues" 20 | }, 21 | "homepage": "https://github.com/markdalgleish/redux-tap#readme", 22 | "devDependencies": { 23 | "babel-cli": "^6.6.5", 24 | "babel-core": "^6.7.4", 25 | "babel-plugin-add-module-exports": "^0.1.2", 26 | "babel-plugin-transform-object-rest-spread": "^6.6.5", 27 | "babel-preset-es2015": "^6.6.0", 28 | "babel-register": "^6.7.2", 29 | "chai": "^3.3.0", 30 | "coveralls": "^2.11.6", 31 | "istanbul": "^1.0.0-alpha.2", 32 | "mocha": "^2.3.3", 33 | "redux": "^3.0.2", 34 | "sinon": "^1.17.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const isFunction = x => typeof x === 'function'; 2 | 3 | export default (selector, callback) => store => next => action => { 4 | const returnValue = next(action); 5 | 6 | if (!isFunction(selector)) { 7 | return returnValue; 8 | } 9 | 10 | const selected = selector(action); 11 | 12 | if (!selected) { 13 | return returnValue; 14 | } 15 | 16 | callback(selected, action, store); 17 | 18 | return returnValue; 19 | }; 20 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { spy } from 'sinon'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import tap from '../src'; 5 | 6 | describe('Given: A Store with redux-tap middleware', () => { 7 | let store; 8 | let selectorSpy; 9 | let callbackSpy; 10 | 11 | beforeEach(() => { 12 | callbackSpy = spy(); 13 | selectorSpy = spy(); 14 | const selector = (...args) => { 15 | selectorSpy(...args); 16 | const { meta } = args[0]; 17 | return meta && meta.foobar; 18 | }; 19 | const createStoreWithMiddleware = applyMiddleware(tap(selector, callbackSpy))(createStore); 20 | const initialState = { name: 'jane smith', loggedIn: false }; 21 | const reducer = (state = initialState, action) => { 22 | switch (action.type) { 23 | case 'LOGIN': { 24 | return { ...state, loggedIn: true }; 25 | } 26 | 27 | default: { 28 | return state; 29 | } 30 | } 31 | } 32 | store = createStoreWithMiddleware(reducer, initialState); 33 | }); 34 | 35 | describe('When: An action with selected meta is dispatched', () => { 36 | beforeEach(() => store.dispatch({ 37 | type: 'LOGIN', 38 | meta: { foobar: 'payload' } 39 | })); 40 | 41 | it('Then: It should invoke the selector with the action', () => { 42 | const [ action ] = selectorSpy.getCall(0).args; 43 | assert.deepEqual(action, { 44 | type: 'LOGIN', 45 | meta: { foobar: 'payload' } 46 | }); 47 | }); 48 | 49 | it('Then: It should invoke the callback with the selected value as the first argument', () => { 50 | const [ meta ] = callbackSpy.getCall(0).args; 51 | assert.deepEqual(meta, 'payload'); 52 | }); 53 | 54 | it('Then: It should invoke the callback with the action as the second argument', () => { 55 | const [ _, action ] = callbackSpy.getCall(0).args; 56 | assert.deepEqual(action, { 57 | type: 'LOGIN', 58 | meta: { foobar: 'payload' } 59 | }); 60 | }); 61 | 62 | it('Then: It should invoke the callback with the store as the third argument', () => { 63 | const [ _, __, callbackStore ] = callbackSpy.getCall(0).args; 64 | const { getState, dispatch } = callbackStore; 65 | assert.equal(typeof getState, 'function'); 66 | assert.equal(typeof dispatch, 'function'); 67 | }); 68 | }); 69 | 70 | describe('When: An action that does not match the selector is dispatched', () => { 71 | beforeEach(() => store.dispatch({ 72 | type: 'LOGIN', 73 | meta: { not: 'selected' } 74 | })); 75 | 76 | it('Then: It should invoke the selector with the action', () => { 77 | const [ action ] = selectorSpy.getCall(0).args; 78 | assert.deepEqual(action, { 79 | type: 'LOGIN', 80 | meta: { not: 'selected' } 81 | }); 82 | }); 83 | 84 | it('Then: It should not invoke the callback', () => { 85 | assert.equal(callbackSpy.callCount, 0); 86 | }); 87 | 88 | it('Then: It should not interrupt the action', () => { 89 | assert.deepEqual(store.getState(), { name: 'jane smith', loggedIn: true }); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('Given: A Store with redux-tap middleware with an invalid selector', () => { 95 | 96 | let store; 97 | let callbackSpy; 98 | 99 | beforeEach(() => { 100 | callbackSpy = spy(); 101 | const selector = 'invalid'; 102 | const createStoreWithMiddleware = applyMiddleware(tap(selector, callbackSpy))(createStore); 103 | const initialState = { name: 'jane smith', loggedIn: false }; 104 | const reducer = (state = initialState, action) => { 105 | switch (action.type) { 106 | case 'LOGIN': { 107 | return { ...state, loggedIn: true }; 108 | } 109 | 110 | default: { 111 | return state; 112 | } 113 | } 114 | } 115 | store = createStoreWithMiddleware(reducer, initialState); 116 | }); 117 | 118 | describe('When: An action is dispatched', () => { 119 | beforeEach(() => store.dispatch({ 120 | type: 'LOGIN' 121 | })); 122 | 123 | it('Then: It should not invoke the callback', () => { 124 | assert.equal(callbackSpy.callCount, 0); 125 | }); 126 | 127 | it('Then: It should not interrupt the action', () => { 128 | assert.deepEqual(store.getState(), { name: 'jane smith', loggedIn: true }); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register 2 | --------------------------------------------------------------------------------