├── .npmignore ├── .travis.yml ├── .gitignore ├── src └── index.js ├── LICENSE ├── package.json ├── test └── index.spec.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.2" 4 | script: "npm test" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | /node_modules 3 | .DS_Store 4 | node_modules/ 5 | .cache 6 | redux/ 7 | todos/ 8 | .log 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * redux-store-validator 3 | */ 4 | 5 | const noop = () => {return true}; 6 | 7 | export const INVALID_KEYS = '@@redux-store-validator@@INVALID_KEYS'; 8 | 9 | export const withValidation = (reducers, validators) => { 10 | const validatedReducers = {}; 11 | let invalidKeys = []; 12 | Object.entries(reducers).map(([reducerSubstate, reducer]) => { 13 | validators[reducerSubstate] = validators[reducerSubstate] || noop; 14 | validatedReducers[reducerSubstate] = (state, action) => { 15 | const newState = reducer(state, action); 16 | if (!validators[reducerSubstate](newState)) { 17 | invalidKeys.push(reducerSubstate); 18 | } 19 | return newState; 20 | } 21 | }) 22 | validatedReducers[INVALID_KEYS] = () => { 23 | const copy = invalidKeys.slice(); 24 | invalidKeys = []; 25 | return copy; 26 | } 27 | return validatedReducers; 28 | }; 29 | 30 | export default withValidation; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Alexander Wang 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-store-validator", 3 | "version": "0.0.3", 4 | "description": "Add validation to your Redux reducers", 5 | "main": "./lib/redux-store-validator.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "compile": "babel -d lib/ src/", 11 | "prepublish": "npm run compile", 12 | "test": "./node_modules/mocha/bin/mocha --compilers js:babel-core/register ./test/**/*.spec.js", 13 | "watch": "onchange 'src/*.js' -- npm run compile" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "github.com/alixander/redux-store-validator.git" 18 | }, 19 | "author": "Alexander Wang", 20 | "license": "MIT", 21 | "homepage": "https://github.com/alixander/redux-store-validator#readme", 22 | "babel": { 23 | "presets": [ 24 | "es2015" 25 | ], 26 | "plugins": [ 27 | [ 28 | "transform-runtime", 29 | { 30 | "polyfill": true, 31 | "regenerator": true 32 | } 33 | ] 34 | ] 35 | }, 36 | "dependencies": {}, 37 | "devDependencies": { 38 | "babel-cli": "^6.16.0", 39 | "babel-core": "^6.17.0", 40 | "babel-loader": "^6.2.5", 41 | "babel-plugin-transform-runtime": "^6.15.0", 42 | "babel-polyfill": "^6.16.0", 43 | "babel-preset-es2015": "^6.16.0", 44 | "mocha": "^3.2.0", 45 | "onchange": "^3.0.2", 46 | "redux": "^3.6.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, createStore } from 'redux'; 2 | import assert from 'assert'; 3 | import { withValidation, INVALID_KEYS } from '../lib'; 4 | 5 | describe('withValidation', function() { 6 | const aInitialState = { 7 | word: 'NA' 8 | }; 9 | const bInitialState = { 10 | counter: 0 11 | }; 12 | let aReducer = (state = aInitialState, action) => { 13 | switch (action.type) { 14 | case 'set': 15 | state.word = action.data; 16 | return state; 17 | default: 18 | return state; 19 | } 20 | }; 21 | let bReducer = (state = bInitialState, action) => { 22 | switch (action.type) { 23 | case 'increment': 24 | state.counter = (state.counter || 0) + 1; 25 | return state; 26 | default: 27 | return state; 28 | } 29 | }; 30 | let reducers = { 31 | a: aReducer, 32 | b: bReducer 33 | }; 34 | it('should identify when state invalid', function() { 35 | const aValidator = (state) => { 36 | return typeof state.word === 'string'; 37 | }; 38 | const validators = { 39 | a: aValidator 40 | }; 41 | const rootReducer = combineReducers(withValidation(reducers, validators)); 42 | const store = createStore(rootReducer); 43 | store.dispatch({ 44 | type: 'set', 45 | data: 'a string' 46 | }); 47 | assert.equal(0, (store.getState())[INVALID_KEYS].length); 48 | store.dispatch({ 49 | type: 'set', 50 | data: 1 51 | }); 52 | assert.deepEqual(store.getState()[INVALID_KEYS], ['a']); 53 | }); 54 | it('should adjust for returning back to valid states', function() { 55 | const aValidator = (state) => { 56 | return typeof state.word === 'string'; 57 | }; 58 | const validators = { 59 | a: aValidator 60 | }; 61 | const rootReducer = combineReducers(withValidation(reducers, validators)); 62 | const store = createStore(rootReducer); 63 | store.dispatch({ 64 | type: 'set', 65 | data: 1 66 | }); 67 | assert.deepEqual(store.getState()[INVALID_KEYS], ['a']); 68 | store.dispatch({ 69 | type: 'set', 70 | data: 'a string' 71 | }); 72 | assert.equal(0, (store.getState())[INVALID_KEYS].length); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redux Store Validator 2 | ========= 3 | ![travis-badge](https://travis-ci.org/alixander/redux-store-validator.svg?branch=master) 4 | 5 | Wrapper to add validation to your Redux reducers 6 | 7 | ``` 8 | npm install --save redux-store-validator 9 | ``` 10 | 11 | ### Purpose 12 | 13 | In a large React/Redux app, the store can become intractable. A common cause of bugs is when the data in the store has changed in a way that the developer didn't expect. `redux-store-validator` is intended to facilitate adding detection of when such a scenario occurs. You can add as many or as little validators to specific parts of your store, and act on it accordingly. For example, you can add a validator to check that a value which you expected to be always positive ever becomes negative. If so, you can log or recover right after the offending action modifies the store. 14 | 15 | ### Usage 16 | 17 | Wrap your reducers 18 | 19 | #### reducers/index.js 20 | 21 | ```js 22 | import { withValidation } from 'redux-store-validator'; 23 | 24 | import aReducer, { validator as aValidator } from './a'; 25 | import { bReducer } from './b'; 26 | 27 | const reducers = { 28 | a: aReducer, 29 | b: bReducer 30 | } 31 | 32 | const validators = { 33 | // Only add validators for substates you want validation on 34 | a: aValidator 35 | } 36 | 37 | // Instead of 38 | // const rootReducer = combineReducers(reducers); 39 | // You wrap your reducers in 'withValidation' and pass in the validators to execute 40 | const rootReducer = combineReducers(withValidation(reducers, validators)); 41 | ``` 42 | 43 | Add validators to reducers as needed 44 | 45 | #### reducers/a.js 46 | ```js 47 | export function validator(state) { 48 | return state.word === 'asdf'; 49 | } 50 | 51 | export default function(state, action) { 52 | switch(action.type) { 53 | case 'asdf': 54 | state.word = 'asdf'; 55 | return state; 56 | default: 57 | return state; 58 | } 59 | } 60 | ``` 61 | 62 | That's it. After you've wrapped your reducers and added validators, you can detect if the store has become invalid by querying the state. 63 | 64 | `redux-store-validator` adds the following to your redux state: 65 | 66 | `state[INVALID_KEYS]`: Array of keys which correspond to the substates that are invalid. 67 | 68 | You can act upon it however you like. Below are just a few examples 69 | 70 | ## Examples of acting upon invalid store 71 | 72 | In your component you can choose not to render with the new data if it's invalid. 73 | 74 | ### components/myComponent.jsx 75 | 76 | ```js 77 | import { INVALID_KEYS } from 'redux-store-validator'; 78 | ... 79 | 80 | const myComponent = React.createClass({ 81 | props: { 82 | text: PropTypes.string, 83 | isValid: PropTypes.bool 84 | }, 85 | 86 | shouldComponentUpdate(nextProps) { 87 | return nextProps.isValid; 88 | }, 89 | 90 | ... 91 | 92 | render() { 93 | return
{this.props.text}
; 94 | } 95 | }); 96 | 97 | function mapStateToProps(state) { 98 | return { 99 | text: state.a.word, 100 | isValid: state[INVALID_KEYS].includes('a') 101 | } 102 | } 103 | ... 104 | ``` 105 | 106 | --------------------------- 107 | 108 | You can replace the state with a default valid one as soon as an action caused it to become invalid. 109 | 110 | #### Back in reducers/index.js 111 | 112 | ```js 113 | import aReducer, { 114 | validator as aValidator, 115 | defaultState as aDefaultState 116 | } from './a'; 117 | import { bReducer } from './b'; 118 | 119 | import { INVALID_KEYS } from 'redux-store-validator'; 120 | 121 | const reducers = { 122 | a: aReducer, 123 | b: bReducer 124 | } 125 | 126 | const validators = { 127 | // Only add validators for substates you want validation on 128 | a: aValidator 129 | } 130 | 131 | const defaultStates = { 132 | a: aDefaultState 133 | } 134 | 135 | const rootReducer = combineReducers(withValidation(reducers, validators)); 136 | 137 | function replaceInvalid(combinedReducer) { 138 | return (state, action) => { 139 | const newState = combinedReducer(state, action); 140 | if (newState.INVALID_KEYS.length === 0) { 141 | return newState; 142 | } 143 | for (const validatedSubstate of Object.keys(validators)) { 144 | if (newState[INVALID_KEYS].includes(validatedSubstate)) { 145 | newState[validatedSubstate] = defaultStates[validatedSubstate] 146 | // If the default state is valid, the INVALID_KEYS will remove the state key in the next reduction step 147 | } 148 | } 149 | return newState; 150 | } 151 | } 152 | export default replaceInvalid(rootReducer); 153 | ``` 154 | 155 | --------------------------- 156 | 157 | You can log in a logger middleware 158 | 159 | #### middlewares/Logger.js 160 | 161 | ```js 162 | import { INVALID_KEYS } from 'redux-store-validator'; 163 | 164 | export default ({getState}) => (next) => (action) => { 165 | const state = getState(); 166 | if (state[INVALID_KEYS].length !== 0) { 167 | // Log the invalid states 168 | for (const key of state[INVALID_KEYS]) { 169 | const substate = state[key]; 170 | ... 171 | } 172 | ... 173 | } 174 | ... 175 | return next(action); 176 | } 177 | ``` 178 | --------------------------------------------------------------------------------