├── test ├── jest.init.js ├── warning.test.js ├── noConfigurationWarning.test.js ├── integration.test.js ├── factoryMap.test.js ├── factoryOptions.test.js └── factory.test.js ├── media └── data-flow.png ├── .flowconfig ├── .npmignore ├── src ├── utils │ ├── warning.js │ └── noConfigurationWarning.js ├── index.js ├── injectReducer.js ├── combineAsyncReducers.js ├── factoryMap.js └── factory.js ├── .travis.yml ├── .github └── dependabot.yml ├── .babelrc.js ├── LICENSE ├── .gitignore ├── package.json ├── .eslintrc ├── jest.config.js ├── README.md └── flow-typed └── npm └── jest_v23.x.x.js /test/jest.init.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | -------------------------------------------------------------------------------- /media/data-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSignal/redux-rags/master/media/data-flow.png -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | flow-typed 7 | 8 | [lints] 9 | 10 | [options] 11 | module.file_ext=.js 12 | 13 | [strict] 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | LICENSE 3 | CODE_OF_CONDUCT.md 4 | .babelrc 5 | .eslintrc 6 | .flowconfig 7 | flow-typed 8 | .idea 9 | coverage 10 | test 11 | .travis.yml 12 | .DS_Store 13 | media 14 | -------------------------------------------------------------------------------- /src/utils/warning.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default function warning(message: string) { 3 | if (typeof console !== 'undefined' && typeof console.error === 'function') { 4 | console.error(message); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '12' 5 | cache: 6 | directories: 7 | - node_modules 8 | before_install: 9 | - npm update 10 | install: 11 | - npm install 12 | script: 13 | - npm run build 14 | - npm run flow 15 | - npm run test 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: "@babel/preset-env" 11 | versions: 12 | - 7.13.8 13 | -------------------------------------------------------------------------------- /src/utils/noConfigurationWarning.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import warning from './warning'; 3 | 4 | const noConfigurationWarning = function() { 5 | if (process.env.NODE_ENV !== 'production') { 6 | warning('You must call configureRags(store, createRootReducer) to use redux-rags!'); 7 | } 8 | }; 9 | 10 | export default noConfigurationWarning; 11 | -------------------------------------------------------------------------------- /test/warning.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const consolespy = jest.spyOn(console, 'error').mockImplementation(() => {}); 3 | 4 | import warning from '../src/utils/warning'; 5 | 6 | describe('warning', () => { 7 | it('calls console error when it exists', () => { 8 | warning('warning test'); 9 | expect(consolespy).toHaveBeenCalledTimes(1); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/env", 5 | { 6 | exclude: ["transform-regenerator"] 7 | } 8 | ], 9 | "@babel/flow" 10 | ], 11 | plugins: [ 12 | "@babel/proposal-class-properties", 13 | "@babel/proposal-object-rest-spread", 14 | "transform-imports", 15 | "@babel/transform-modules-commonjs" 16 | ], 17 | env: { 18 | test: { 19 | presets: [["@babel/env"], "@babel/flow"] 20 | } 21 | } 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import makeReducerInjector from './injectReducer'; 2 | import createFactory from './factory'; 3 | import createFactoryMap from './factoryMap'; 4 | import combineAsyncReducers from './combineAsyncReducers'; 5 | import noConfigurationWarning from './utils/noConfigurationWarning'; 6 | 7 | let injectReducer = noConfigurationWarning; 8 | let ragFactory = noConfigurationWarning; 9 | let ragFactoryMap = noConfigurationWarning; 10 | 11 | function configureRags(store: *, createRootReducer: *) { 12 | injectReducer = makeReducerInjector(store, createRootReducer); 13 | ragFactory = createFactory(injectReducer); 14 | ragFactoryMap = createFactoryMap(injectReducer); 15 | } 16 | 17 | export { 18 | combineAsyncReducers, 19 | configureRags, 20 | injectReducer, 21 | ragFactory, 22 | ragFactoryMap, 23 | }; 24 | -------------------------------------------------------------------------------- /src/injectReducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Assumings keys came from 'path.to.reducer'.split('.'), or some equivalent nesting structure 3 | // Recurse in reducers until we find where this one should go. 4 | function replaceAsyncReducers(reducers: Object, keys: Array, reducer: Function) { 5 | let key = keys.shift(); 6 | if (keys.length === 0) { 7 | reducers[key] = reducer; 8 | return; 9 | } 10 | if (reducers[key] === undefined) { 11 | reducers[key] = {}; 12 | } 13 | let nextReducers = reducers[key]; 14 | replaceAsyncReducers(nextReducers, keys, reducer); 15 | } 16 | 17 | type ReducerInjector = (Array, *) => void; 18 | 19 | const dynamicReducers = {}; 20 | const makeReducerInjector = (store: { replaceReducer: Function }, createRootReducer: Function): ReducerInjector => (keys: Array, reducer: *) => { 21 | replaceAsyncReducers(dynamicReducers, keys, reducer); 22 | store.replaceReducer(createRootReducer(dynamicReducers)); 23 | }; 24 | 25 | export default makeReducerInjector; 26 | -------------------------------------------------------------------------------- /test/noConfigurationWarning.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | jest.mock('../src/utils/warning'); 3 | import warning from '../src/utils/warning'; 4 | import noConfigurationWarning from '../src/utils/noConfigurationWarning'; 5 | 6 | describe('noConfigurationWarning', () => { 7 | let _NODE_ENV = null; 8 | 9 | beforeAll(() => { 10 | _NODE_ENV = process.env.NODE_ENV; 11 | }); 12 | 13 | beforeEach(() => { 14 | // $FlowFixMe : Flow doesn't understand jest module mocks. 15 | warning.mockClear(); 16 | }); 17 | 18 | afterAll(() => { 19 | if (_NODE_ENV) { 20 | process.env.NODE_ENV = _NODE_ENV; 21 | } 22 | }); 23 | 24 | it('calls warning when env is not production', () => { 25 | process.env.NODE_ENV = 'develop'; 26 | noConfigurationWarning(); 27 | expect(warning).toHaveBeenCalledTimes(1); 28 | }); 29 | 30 | it('does not call warning if env is production', () => { 31 | process.env.NODE_ENV = 'production'; 32 | noConfigurationWarning(); 33 | expect(warning).toHaveBeenCalledTimes(0); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kyle Saxberg 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 | -------------------------------------------------------------------------------- /src/combineAsyncReducers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const recursivelyCombineAsyncReducers = function (combineReducers: Function, asyncReducers: Function | Object) { 4 | // Don't combine a reducer leaf 5 | if (typeof asyncReducers !== 'object') { 6 | return asyncReducers; 7 | } 8 | // Call combineReducers on every function, recursive walk. 9 | const reducers = {}; 10 | for (let prop of Object.getOwnPropertyNames(asyncReducers)) { 11 | const subreducer = asyncReducers[prop]; 12 | if (typeof subreducer === 'object') { 13 | reducers[prop] = recursivelyCombineAsyncReducers(combineReducers, subreducer); 14 | } else { 15 | reducers[prop] = subreducer; 16 | } 17 | } 18 | return combineReducers(reducers); 19 | }; 20 | 21 | const combineAsyncReducers = function (combineReducers: Function, asyncReducers: Function | Object): Function | Object { 22 | const newAsyncReducers = Object.getOwnPropertyNames(asyncReducers).reduce( 23 | (reducers, key) => { 24 | reducers[key] = recursivelyCombineAsyncReducers(combineReducers, asyncReducers[key]); 25 | return reducers; 26 | } 27 | , {}); 28 | return newAsyncReducers; 29 | }; 30 | 31 | export default combineAsyncReducers; 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | build 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | # Editor things 65 | .idea 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-rags", 3 | "version": "1.2.2", 4 | "description": "Redux Reducers, Actions, and Getters. Simplified!", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "build": "babel src -d build --copy-files", 8 | "lint": "eslint src/**", 9 | "lint:watch": "esw -w lib/**", 10 | "prepublish": "npm run build", 11 | "flow": "flow", 12 | "test": "npm run jest", 13 | "jest": "./node_modules/.bin/jest --maxWorkers=2", 14 | "jest:watch": "npm run jest -- --watchAll" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/CodeSignal/redux-rags.git" 19 | }, 20 | "files": [ 21 | "build" 22 | ], 23 | "keywords": [ 24 | "redux" 25 | ], 26 | "author": "CodeSignal", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/CodeSignal/redux-rags/issues" 30 | }, 31 | "homepage": "https://github.com/CodeSignal/redux-rags#readme", 32 | "devDependencies": { 33 | "@babel/cli": "^7.7.4", 34 | "@babel/core": "^7.7.4", 35 | "@babel/plugin-proposal-class-properties": "^7.7.4", 36 | "@babel/plugin-proposal-object-rest-spread": "^7.7.4", 37 | "@babel/plugin-transform-modules-commonjs": "^7.7.4", 38 | "@babel/preset-env": "^7.7.4", 39 | "@babel/preset-flow": "^7.7.4", 40 | "@babel/register": "^7.7.4", 41 | "babel-core": "^7.0.0-bridge.0", 42 | "babel-eslint": "^10.0.3", 43 | "babel-jest": "^28.0.3", 44 | "babel-plugin-transform-imports": "^2.0.0", 45 | "babel-polyfill": "^6.26.0", 46 | "eslint": "^8.2.0", 47 | "eslint-plugin-import": "^2.18.2", 48 | "eslint-watch": "^8.0.0", 49 | "flow-bin": "^0.182.0", 50 | "jest": "^28.0.3", 51 | "redux": "^4.0.4", 52 | "redux-thunk": "^2.3.0", 53 | "regenerator-runtime": "^0.13.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { compose, combineReducers, createStore, applyMiddleware } from 'redux'; 4 | import thunk from 'redux-thunk'; 5 | 6 | import { combineAsyncReducers, configureRags, ragFactory, ragFactoryMap } from '../src/index'; 7 | 8 | 9 | describe('integration test', () => { 10 | // Initializing the redux store. 11 | const createRootReducer = (dynamicReducers: Object = { root: state => state ?? null }) => { 12 | dynamicReducers = combineAsyncReducers(combineReducers, dynamicReducers); 13 | 14 | return combineReducers(Object.assign({}, dynamicReducers, { 15 | // ... list your reducers below 16 | })); 17 | }; 18 | 19 | const middleware = applyMiddleware(thunk); 20 | const store = createStore(createRootReducer(), compose(middleware)); 21 | configureRags(store, createRootReducer); 22 | 23 | 24 | it('initializes store properly', () => { 25 | expect(store).toBeTruthy(); 26 | }); 27 | 28 | it('adds a reducer to state when calling factory load', async () => { 29 | const { actions, getters } = ragFactory({ name: 'basic-test', load: () => true }); 30 | const state = store.getState(); 31 | expect(state['@@redux-rags']).toBeTruthy(); 32 | expect(getters.getData(state)).toBe(null); 33 | await store.dispatch(actions.load()); 34 | const nextState = store.getState(); 35 | expect(getters.getData(nextState)).toBe(true); 36 | }); 37 | 38 | it('adds a reducer to state when calling factoryMap load', async () => { 39 | let i = 1; 40 | const { actions, getters } = ragFactoryMap({ getInitialState: () => 0, name: 'map-test', load: (negative) => negative ? -1 * i++ : i++ }); 41 | const state = store.getState(); 42 | expect(getters.getData(state, true)).toBe(0); 43 | expect(getters.getData(state, false)).toBe(0); 44 | 45 | await store.dispatch(actions.load(true)); 46 | await store.dispatch(actions.load(false)); 47 | const nextState = store.getState(); 48 | expect(nextState['@@redux-rags/map']).toBeTruthy(); 49 | expect(getters.getData(nextState, true)).toBe(-1); 50 | expect(getters.getData(nextState, false)).toBe(2); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/factoryMap.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createFactoryMap from '../src/factoryMap'; 3 | const injectReducer = jest.fn(); 4 | const factoryMap = createFactoryMap(injectReducer); 5 | 6 | describe('factoryMap', () => { 7 | const loadNumber = jest.fn().mockReturnValue(10); 8 | 9 | const name = 'Test Data'; 10 | let { actions, getters } = factoryMap({ 11 | name, 12 | load: loadNumber, 13 | }); 14 | 15 | beforeEach(() => { 16 | let freshMap = factoryMap({ 17 | name, 18 | load: loadNumber, 19 | }); 20 | actions = freshMap.actions; 21 | getters = freshMap.getters; 22 | }); 23 | 24 | describe('getters', () => { 25 | describe('getWithArgs', () => { 26 | it('returns object with data and meta fields', () => { 27 | const obj = getters.getWithArgs('argument'); 28 | expect(obj).toEqual( 29 | expect.objectContaining({ 30 | data: null, 31 | meta: expect.any(Object), 32 | }) 33 | ); 34 | }); 35 | }); 36 | 37 | describe('getData', () => { 38 | it('returns null if nothing has been loaded', () => { 39 | const data = getters.getData('argument'); 40 | expect(data).toBe(null); 41 | }); 42 | }); 43 | 44 | describe('getMeta', () => { 45 | it('returns object with loaded false when nothing has been loaded', () => { 46 | const meta = getters.getMeta('argument'); 47 | expect(meta).toEqual( 48 | expect.objectContaining({ 49 | loaded: false, 50 | }) 51 | ); 52 | }); 53 | }); 54 | 55 | describe('getIsLoading', () => { 56 | it('returns false if we did not call load', () => { 57 | expect(getters.getIsLoading('argument')).toBe(false); 58 | }); 59 | }); 60 | }); 61 | 62 | describe('actions', () => { 63 | describe('load', () => { 64 | it('calls inject reducer on getting a new argument', () => { 65 | const thunk = actions.load('argument'); 66 | thunk(jest.fn()); 67 | expect(injectReducer).toHaveBeenCalledTimes(1); 68 | }); 69 | 70 | it('does not inject reducer when receiving the same argument', () => { 71 | let thunk = actions.load('argument'); 72 | thunk(jest.fn()); 73 | expect(injectReducer).toHaveBeenCalled(); 74 | // $FlowFixMe: Flow doesn't know this is a jest mock. 75 | injectReducer.mockClear(); 76 | thunk = actions.load('argument'); 77 | thunk(jest.fn()); 78 | expect(injectReducer).not.toHaveBeenCalled(); 79 | }); 80 | }); 81 | }); 82 | }); 83 | 84 | -------------------------------------------------------------------------------- /src/factoryMap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createFactory, { createGetInitialState, type BoilerState } from './factory'; 3 | 4 | export type MapState = { [string]: BoilerState }; 5 | 6 | type ConfigType = { 7 | name?: string, 8 | load?: (...args: G) => T | Promise, 9 | getInitialState?: () => T, 10 | }; 11 | 12 | type ReturnType> = { 13 | actions: * & { 14 | reset: () => *, 15 | clearErrors: () => *, 16 | load: (...args: G) => *, 17 | }, 18 | getters: * & { 19 | get: Object => MapState, 20 | getWithArgs: (store: Object, ...args: Array) => BoilerState, 21 | getData: (store: Object, ...args: Array) => $PropertyType, 'data'>, 22 | getMeta: (store: Object, ...args: Array) => $PropertyType, 'meta'>, 23 | getIsLoading: ( 24 | store: Object, 25 | ...args: Array 26 | ) => $PropertyType<$PropertyType, 'meta'>, 'loading'>, 27 | }, 28 | }; 29 | 30 | type FactoryMapGetter = ConfigType => ReturnType; 31 | 32 | const prefix = '@@redux-rags/map'; 33 | 34 | let generatedCount = 0; 35 | 36 | const convertArgsToString = (...args) => JSON.stringify(args); 37 | 38 | const createFactoryMap = >(injectReducer: Function): FactoryMapGetter => { 39 | const factory = createFactory(injectReducer); 40 | return (config: ConfigType): ReturnType => { 41 | const { name = '', load, getInitialState } = config; 42 | generatedCount += 1; 43 | 44 | const safeDataName = `${name}/${generatedCount}`; 45 | const mapArgsToGenerated = {}; 46 | class Getters { 47 | static _getInitialStateForKey: () => BoilerState = createGetInitialState(getInitialState); 48 | 49 | static get = (reduxStore: Object): MapState => 50 | reduxStore[prefix] && reduxStore[prefix][safeDataName]; 51 | 52 | static getWithArgs = (reduxStore, ...args) => { 53 | const argsKey = convertArgsToString(...args); 54 | const state = Getters.get(reduxStore); 55 | if (!state || !state.hasOwnProperty(argsKey)) { 56 | return Getters._getInitialStateForKey(); 57 | } 58 | return state[argsKey]; 59 | }; 60 | 61 | static getData = (reduxStore, ...args) => Getters.getWithArgs(reduxStore, ...args).data; 62 | 63 | static getMeta = (reduxStore, ...args) => Getters.getWithArgs(reduxStore, ...args).meta; 64 | 65 | static getIsLoading = (reduxStore: Object, ...args) => { 66 | const meta = Getters.getMeta(reduxStore, ...args); 67 | return meta.loading; 68 | }; 69 | } 70 | 71 | class Actions { 72 | static _queryOrCreateBoilerplate = (...args) => { 73 | const stringHash = convertArgsToString(...args); 74 | if (!mapArgsToGenerated[stringHash]) { 75 | // Need to generate everything for this. Luckily we have a generator 76 | const getOutOfStore: any = store => Getters.getWithArgs(store, ...args); 77 | mapArgsToGenerated[stringHash] = factory({ 78 | name: safeDataName, 79 | load, 80 | getInitialState, 81 | getInStore: getOutOfStore, 82 | }); 83 | const subreducer = mapArgsToGenerated[stringHash].subreducer; 84 | injectReducer([prefix, safeDataName, stringHash], subreducer); 85 | } 86 | return mapArgsToGenerated[stringHash]; 87 | }; 88 | 89 | // Links to argument-less actions generated by the factory. 90 | static _forwardActionForSubreducer = (actionName: string, { forwardArgs = false }: * = {}) => ( 91 | ...args: Array 92 | ) => async dispatch => { 93 | const actions = Actions._queryOrCreateBoilerplate(...args).actions; 94 | const action = actions[actionName]; 95 | if (forwardArgs) { 96 | return dispatch(action(...args)); // Assumed to be loading arguments. 97 | } 98 | return dispatch(action()); 99 | }; 100 | 101 | static load = Actions._forwardActionForSubreducer('load', { forwardArgs: true }); 102 | 103 | static reset = Actions._forwardActionForSubreducer('reset'); 104 | 105 | static clearErrors = Actions._forwardActionForSubreducer('clearErrors'); 106 | } 107 | 108 | return { 109 | actions: Actions, 110 | getters: Getters, 111 | }; 112 | }; 113 | }; 114 | 115 | export default createFactoryMap; 116 | 117 | -------------------------------------------------------------------------------- /test/factoryOptions.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createFactory from '../src/factory'; 3 | const injectReducer = jest.fn(); 4 | const factory = createFactory(injectReducer); 5 | 6 | describe('factoryOptions', () => { 7 | describe('update', () => { 8 | const update = (data: ?{ [string]: string }, id, value) => ({ 9 | ...(data || {}), 10 | [id]: value, 11 | }); 12 | const name = 'CACHE_TEST'; 13 | 14 | const getInStore = x => x; 15 | const { actions, getters } = factory({ 16 | name, 17 | update, 18 | getInStore, 19 | }); 20 | 21 | describe('thunks', () => { 22 | describe('update', () => { 23 | it('works', () => { 24 | const dispatch = jest.fn(); 25 | const getState = jest.fn().mockReturnValue(getters.getInitialState()); 26 | const asyncThunk = actions.update('key_1', 'val_1'); 27 | 28 | return asyncThunk(dispatch, getState).then(() => { 29 | expect(dispatch).toHaveBeenCalledWith( 30 | expect.objectContaining({ 31 | type: expect.stringContaining('update'), 32 | payload: expect.objectContaining({ 33 | key_1: 'val_1', 34 | }), 35 | }) 36 | ); 37 | }); 38 | }); 39 | }); 40 | 41 | describe('load', () => { 42 | it('does nothing as it was not defined', () => { 43 | const dispatch = jest.fn(); 44 | const getState = jest.fn().mockReturnValue(getters.getInitialState()); 45 | const asyncThunk = actions.load(); 46 | 47 | return asyncThunk(dispatch, getState).then(() => { 48 | expect(dispatch).not.toHaveBeenCalled(); 49 | }); 50 | }); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('loadOnlyOnce', () => { 56 | const load = jest.fn().mockResolvedValue(5); 57 | const name = 'LOAD_ONCE_TEST'; 58 | 59 | const getInStore = x => x; 60 | const { 61 | actions, 62 | subreducer, 63 | getters, 64 | } = factory({ 65 | name, 66 | load, 67 | getInStore, 68 | loadOnlyOnce: true, 69 | }); 70 | 71 | describe('thunks', () => { 72 | describe('load', () => { 73 | beforeEach(() => { 74 | load.mockClear(); 75 | }); 76 | 77 | it('loads value on first call', () => { 78 | const dispatch = jest.fn(); 79 | const getState = jest.fn().mockReturnValue(getters.getInitialState()); 80 | const asyncThunk = actions.load(); 81 | 82 | return asyncThunk(dispatch, getState).then(() => { 83 | expect(dispatch).toHaveBeenCalledWith( 84 | expect.objectContaining({ 85 | type: expect.stringContaining('update'), 86 | payload: 5, 87 | }) 88 | ); 89 | }); 90 | }); 91 | 92 | it('does not load after having been loaded', () => { 93 | const dispatch = jest.fn(); 94 | const getState = jest.fn().mockReturnValue({ 95 | data: 0, 96 | meta: { loaded: true }, 97 | }); 98 | const asyncThunk = actions.load(); 99 | 100 | return asyncThunk(dispatch, getState).then(() => { 101 | expect(dispatch).not.toHaveBeenCalled(); 102 | }); 103 | }); 104 | 105 | it('loads after resetting the values', () => { 106 | const dispatch = jest.fn(); 107 | const asyncThunk = actions.load(); 108 | 109 | const state = getters.getInitialState(); 110 | state.data = 0; 111 | state.meta.loaded = true; 112 | 113 | const stateAfterLoadAndReset = subreducer(state, actions.reset()); 114 | const getState = jest.fn().mockReturnValue(stateAfterLoadAndReset); 115 | 116 | return asyncThunk(dispatch, getState).then(() => { 117 | expect(dispatch).toHaveBeenCalledWith( 118 | expect.objectContaining({ 119 | type: expect.stringContaining('update'), 120 | }) 121 | ); 122 | }); 123 | }); 124 | }); 125 | }); 126 | }); 127 | 128 | describe('partialReducer', () => { 129 | describe('on initial creation', () => { 130 | const load = jest.fn().mockResolvedValue(5); 131 | const name = 'PARTIAL_REDUCER_TEST'; 132 | 133 | const getInStore = x => x; 134 | const partialReducer = (state: *, action) => { 135 | if (action.type === 'LOGOUT') { 136 | return { 137 | ...state, 138 | data: null, 139 | }; 140 | } 141 | return state; 142 | }; 143 | 144 | const { 145 | subreducer, 146 | getters, 147 | } = factory({ 148 | name, 149 | load, 150 | partialReducer, 151 | getInStore, 152 | loadOnlyOnce: true, 153 | }); 154 | 155 | it('updates data on recieving action in partialReducer', () => { 156 | const state = getters.getInitialState(); 157 | state.data = 'Fancy Data'; 158 | 159 | expect(subreducer(state, { type: 'LOGOUT' })).toMatchObject({ 160 | data: null, 161 | }); 162 | }); 163 | }); 164 | }); 165 | }); 166 | 167 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "commonjs": true, 7 | "es6": true, 8 | "jest": true, 9 | "node": true 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 6, 13 | "sourceType": "module", 14 | "ecmaFeatures": { 15 | "generators": true, 16 | "experimentalObjectRestSpread": true 17 | } 18 | }, 19 | "settings": { 20 | "import/ignore": [ 21 | "node_modules", 22 | "\\.(json|css|jpg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$" 23 | ], 24 | "import/extensions": [ 25 | ",.js" 26 | ], 27 | "import/resolver": { 28 | "node": { 29 | "extensions": [ 30 | ".js", 31 | ".json" 32 | ] 33 | } 34 | } 35 | }, 36 | "rules": { 37 | "array-callback-return": "warn", 38 | "camelcase": "warn", 39 | "curly": "warn", 40 | "default-case": [ 41 | "warn", 42 | { 43 | "commentPattern": "^no default$" 44 | } 45 | ], 46 | "dot-location": [ 47 | "warn", 48 | "property" 49 | ], 50 | "eol-last": "warn", 51 | "eqeqeq": [ 52 | "warn", 53 | "always" 54 | ], 55 | "indent": [ 56 | "warn", 57 | 2, 58 | { 59 | "SwitchCase": 1 60 | } 61 | ], 62 | "guard-for-in": "warn", 63 | "keyword-spacing": "warn", 64 | "new-parens": "warn", 65 | "no-array-constructor": "warn", 66 | "no-caller": "warn", 67 | "no-cond-assign": [ 68 | "warn", 69 | "always" 70 | ], 71 | "no-const-assign": "warn", 72 | "no-control-regex": "warn", 73 | "no-delete-var": "warn", 74 | "no-dupe-args": "warn", 75 | "no-dupe-class-members": "warn", 76 | "no-dupe-keys": "warn", 77 | "no-duplicate-case": "warn", 78 | "no-empty-character-class": "warn", 79 | "no-empty-pattern": "warn", 80 | "no-eval": "warn", 81 | "no-ex-assign": "warn", 82 | "no-extend-native": "warn", 83 | "no-extra-bind": "warn", 84 | "no-extra-label": "warn", 85 | "no-fallthrough": "warn", 86 | "no-func-assign": "warn", 87 | "no-global-assign": "warn", 88 | "no-implied-eval": "warn", 89 | "no-invalid-regexp": "warn", 90 | "no-iterator": "warn", 91 | "no-label-var": "warn", 92 | "no-labels": [ 93 | "warn", 94 | { 95 | "allowLoop": false, 96 | "allowSwitch": false 97 | } 98 | ], 99 | "no-lone-blocks": "warn", 100 | "no-loop-func": "warn", 101 | "no-mixed-operators": [ 102 | "warn", 103 | { 104 | "groups": [ 105 | [ 106 | "&", 107 | "|", 108 | "^", 109 | "~", 110 | "<<", 111 | ">>", 112 | ">>>" 113 | ], 114 | [ 115 | "==", 116 | "!=", 117 | "===", 118 | "!==", 119 | ">", 120 | ">=", 121 | "<", 122 | "<=" 123 | ], 124 | [ 125 | "&&", 126 | "||" 127 | ], 128 | [ 129 | "in", 130 | "instanceof" 131 | ] 132 | ], 133 | "allowSamePrecedence": false 134 | } 135 | ], 136 | "no-multi-str": "warn", 137 | "no-new-func": "warn", 138 | "no-new-object": "warn", 139 | "no-new-symbol": "warn", 140 | "no-new-wrappers": "warn", 141 | "no-obj-calls": "warn", 142 | "no-octal": "warn", 143 | "no-octal-escape": "warn", 144 | "no-redeclare": "warn", 145 | "no-regex-spaces": "warn", 146 | "no-restricted-syntax": [ 147 | "warn", 148 | "LabeledStatement", 149 | "WithStatement" 150 | ], 151 | "no-script-url": "warn", 152 | "no-self-assign": "warn", 153 | "no-self-compare": "warn", 154 | "no-sequences": "warn", 155 | "no-shadow-restricted-names": "warn", 156 | "no-sparse-arrays": "warn", 157 | "no-template-curly-in-string": "warn", 158 | "no-this-before-super": "warn", 159 | "no-throw-literal": "warn", 160 | "no-undef": "warn", 161 | "no-unexpected-multiline": "warn", 162 | "no-unreachable": "warn", 163 | "no-unsafe-negation": "warn", 164 | "no-unused-expressions": "warn", 165 | "no-unused-labels": "warn", 166 | "no-unused-vars": [ 167 | "warn", 168 | { 169 | "vars": "local", 170 | "args": "none" 171 | } 172 | ], 173 | "no-use-before-define": [ 174 | "warn", 175 | "nofunc" 176 | ], 177 | "no-useless-computed-key": "warn", 178 | "no-useless-concat": "warn", 179 | "no-useless-constructor": "warn", 180 | "no-useless-escape": "warn", 181 | "no-useless-rename": [ 182 | "warn", 183 | { 184 | "ignoreDestructuring": false, 185 | "ignoreImport": false, 186 | "ignoreExport": false 187 | } 188 | ], 189 | "no-with": "warn", 190 | "no-whitespace-before-property": "warn", 191 | "object-curly-spacing": [ 192 | "warn", 193 | "always" 194 | ], 195 | "operator-assignment": [ 196 | "warn", 197 | "always" 198 | ], 199 | "radix": "warn", 200 | "require-yield": "warn", 201 | "rest-spread-spacing": [ 202 | "warn", 203 | "never" 204 | ], 205 | "semi": "warn", 206 | "strict": [ 207 | "warn", 208 | "never" 209 | ], 210 | "unicode-bom": [ 211 | "warn", 212 | "never" 213 | ], 214 | "use-isnan": "warn", 215 | "valid-typeof": "warn", 216 | "allowAllCaps": "off" 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after the first failure 9 | // bail: false, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/var/folders/r8/lhjktmld64d_bdzjrp7_8yb80000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files usin a array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: null, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: null, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // An array of directory names to be searched recursively up from the requiring module's location 61 | // moduleDirectories: [ 62 | // "node_modules" 63 | // ], 64 | 65 | // An array of file extensions your modules use 66 | // moduleFileExtensions: [ 67 | // "js", 68 | // "json", 69 | // "jsx", 70 | // "node" 71 | // ], 72 | 73 | // A map from regular expressions to module names that allow to stub out resources with a single module 74 | // moduleNameMapper: {}, 75 | 76 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 77 | // modulePathIgnorePatterns: [], 78 | 79 | // Activates notifications for test results 80 | // notify: false, 81 | 82 | // An enum that specifies notification mode. Requires { notify: true } 83 | // notifyMode: "always", 84 | 85 | // A preset that is used as a base for Jest's configuration 86 | // preset: null, 87 | 88 | // Run tests from one or more projects 89 | // projects: null, 90 | 91 | // Use this configuration option to add custom reporters to Jest 92 | // reporters: undefined, 93 | 94 | // Automatically reset mock state between every test 95 | // resetMocks: false, 96 | 97 | // Reset the module registry before running each individual test 98 | // resetModules: false, 99 | 100 | // A path to a custom resolver 101 | // resolver: null, 102 | 103 | // Automatically restore mock state between every test 104 | // restoreMocks: false, 105 | 106 | // The root directory that Jest should scan for tests and modules within 107 | // rootDir: null, 108 | 109 | // A list of paths to directories that Jest should use to search for files in 110 | // roots: [ 111 | // "" 112 | // ], 113 | 114 | // Allows you to use a custom runner instead of Jest's default test runner 115 | // runner: "jest-runner", 116 | 117 | // The paths to modules that run some code to configure or set up the testing environment before each test 118 | setupFiles: ["/test/jest.init.js"], 119 | 120 | // The path to a module that runs some code to configure or set up the testing framework before each test 121 | // setupTestFrameworkScriptFile: null, 122 | 123 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 124 | // snapshotSerializers: [], 125 | 126 | // The test environment that will be used for testing 127 | testEnvironment: "node", 128 | 129 | // Options that will be passed to the testEnvironment 130 | // testEnvironmentOptions: {}, 131 | 132 | // Adds a location field to test results 133 | // testLocationInResults: false, 134 | 135 | // The glob patterns Jest uses to detect test files 136 | // testMatch: [ 137 | // "**/__tests__/**/*.js?(x)", 138 | // "**/?(*.)+(spec|test).js?(x)" 139 | // ], 140 | 141 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 142 | // testPathIgnorePatterns: [ 143 | // "/node_modules/" 144 | // ], 145 | 146 | // The regexp pattern Jest uses to detect test files 147 | // testRegex: "", 148 | 149 | // This option allows the use of a custom results processor 150 | // testResultsProcessor: null, 151 | 152 | // This option allows use of a custom test runner 153 | // testRunner: "jasmine2", 154 | 155 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 156 | // testURL: "http://localhost", 157 | 158 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 159 | // timers: "real", 160 | 161 | // A map from regular expressions to paths to transformers 162 | // transform: null, 163 | 164 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 165 | // transformIgnorePatterns: [ 166 | // "/node_modules/" 167 | // ], 168 | 169 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 170 | // unmockedModulePathPatterns: undefined, 171 | 172 | // Indicates whether each individual test should be reported during the run 173 | // verbose: null, 174 | 175 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 176 | // watchPathIgnorePatterns: [], 177 | 178 | // Whether to use watchman for file crawling 179 | // watchman: true, 180 | }; 181 | -------------------------------------------------------------------------------- /src/factory.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import warning from './utils/warning'; 3 | import { Dispatch } from 'redux'; 4 | 5 | export type BoilerState = { 6 | data: ?T, 7 | meta: { 8 | loading: boolean, 9 | loaded: boolean, 10 | changeCount: number, 11 | lastChangeTime: ?number, 12 | errors?: *, 13 | }, 14 | }; 15 | 16 | 17 | type Reducer = (state?: BoilerState, action: *) => BoilerState; 18 | type PartialReducer = (state: BoilerState, action: *) => BoilerState; 19 | 20 | type ConfigType = { 21 | name?: string, 22 | load?: (...args: G) => T | Promise, 23 | partialReducer?: PartialReducer, 24 | update?: (data: ?T, *) => ?T | Promise, 25 | getInStore?: (store: Object) => BoilerState, 26 | getInitialState?: () => T, 27 | // Options 28 | loadOnlyOnce?: ?boolean, 29 | }; 30 | 31 | type ReturnType> = { 32 | actions: * & { 33 | reset: () => *, 34 | errors: (*) => *, 35 | clearErrors: () => *, 36 | load: (...args: G) => *, 37 | updateData: (?T) => *, 38 | update: (*) => *, 39 | beginLoading: () => *, 40 | endLoading: () => *, 41 | }, 42 | subreducer: Reducer, 43 | getters: * & { 44 | get: Object => BoilerState, 45 | getData: Object => $PropertyType, 'data'>, 46 | getMeta: Object => $PropertyType, 'meta'>, 47 | }, 48 | }; 49 | 50 | type FactoryMapGetter = ConfigType => ReturnType; 51 | 52 | let generatedCount = 0; 53 | const prefix = '@@redux-rags'; 54 | type LoadingEnum = 'loading'; 55 | type EndLoadingEnum = 'endLoading'; 56 | type ErrorsEnum = 'errors'; 57 | type UpdateEnum = 'update'; 58 | type ResetEnum = 'reset'; 59 | const getLoadingType = (name): LoadingEnum => 60 | (`${prefix}/${generatedCount}/${name}: begin loading`: any); 61 | const getEndLoadingType = (name): EndLoadingEnum => 62 | (`${prefix}/${generatedCount}/${name}: end loading`: any); 63 | const getErrorsType = (name): ErrorsEnum => (`${prefix}/${generatedCount}/${name}: errors`: any); 64 | const getUpdateType = (name): UpdateEnum => (`${prefix}/${generatedCount}/${name}: update`: any); 65 | const getResetType = (name): ResetEnum => (`${prefix}/${generatedCount}/${name}: reset`: any); 66 | 67 | export const createGetInitialState = (getInitialState: ?() => mixed): () => BoilerState => (): BoilerState => ({ 68 | data: typeof getInitialState === 'function' ? getInitialState() : null, 69 | meta: { 70 | loaded: false, 71 | changeCount: 0, 72 | loading: false, 73 | lastChangeTime: null, 74 | errors: null, 75 | }, 76 | }); 77 | 78 | const createFactory = >(injectReducer: Function): FactoryMapGetter => (config: ConfigType): ReturnType => { 79 | const { 80 | name = '', 81 | load, 82 | loadOnlyOnce, 83 | getInStore, 84 | getInitialState, 85 | update, 86 | partialReducer, 87 | } = config; 88 | generatedCount += 1; 89 | 90 | const safeDataName = `${name}/${generatedCount}`; 91 | const wrappedGetInitialState: () => BoilerState = createGetInitialState(getInitialState); 92 | class Getters { 93 | static getInitialState = wrappedGetInitialState; 94 | 95 | static get = getInStore || 96 | ((reduxStore: Object): BoilerState => 97 | reduxStore[prefix][safeDataName] || Getters.getInitialState()); 98 | 99 | static getData = (reduxStore: Object): $PropertyType, 'data'> => { 100 | const state = Getters.get(reduxStore); 101 | if (!state.hasOwnProperty('data')) { 102 | warning(`redux-rags: getData failed to find the property 'data' on the object returned by Getters.get. 103 | This is likely caused by providing an incorrect 'getInStore' configuration option.`); 104 | } 105 | return state.data; 106 | }; 107 | 108 | static getMeta = (reduxStore: Object): $PropertyType, 'meta'> => { 109 | const state = Getters.get(reduxStore); 110 | if (!state.hasOwnProperty('meta')) { 111 | warning(`redux-rags: getData failed to find the property 'meta' on the object returned by Getters.get. 112 | This is likely caused by providing an incorrect 'getInStore' configuration option.`); 113 | } 114 | return state.meta; 115 | }; 116 | 117 | static getIsLoading = (reduxStore: Object) => { 118 | const meta = Getters.getMeta(reduxStore); 119 | return meta && meta.loading; 120 | }; 121 | } 122 | 123 | const BEGIN_LOADING = getLoadingType(name); 124 | const END_LOADING = getEndLoadingType(name); 125 | const ERRORS = getErrorsType(name); 126 | const UPDATE_DATA = getUpdateType(name); 127 | const RESET = getResetType(name); 128 | 129 | class Actions { 130 | static beginLoading = () => ({ 131 | type: BEGIN_LOADING, 132 | payload: null, 133 | }); 134 | 135 | static endLoading = () => ({ 136 | type: END_LOADING, 137 | payload: null, 138 | }); 139 | 140 | static reset = () => ({ 141 | type: RESET, 142 | payload: null, 143 | }); 144 | 145 | static errors = (errors: T) => ({ 146 | type: ERRORS, 147 | payload: errors, 148 | }); 149 | 150 | static clearErrors = () => ({ 151 | type: ERRORS, 152 | payload: null, 153 | }); 154 | 155 | static updateData = (data: ?T) => ({ 156 | type: UPDATE_DATA, 157 | payload: data, 158 | }); 159 | 160 | static update = (...args: *) => async (dispatch, getState: () => Object) => { 161 | if (!update || typeof update !== 'function') { 162 | return; 163 | } 164 | try { 165 | const manipulated = await Promise.resolve(update(Getters.getData(getState()), ...args)); 166 | dispatch(Actions.updateData(manipulated)); 167 | } catch (err) { 168 | dispatch(Actions.errors(err)); 169 | } 170 | }; 171 | 172 | static load = (...args: G) => async ( 173 | dispatch: typeof Dispatch, 174 | getState: () => Object 175 | ): Promise => { 176 | if (!load) { 177 | return null; 178 | } 179 | if (loadOnlyOnce) { 180 | const state = Getters.get(getState()); 181 | if (state && state.meta.loaded) { 182 | return state.data; 183 | } 184 | } 185 | dispatch(Actions.beginLoading()); 186 | try { 187 | const data = await Promise.resolve(load(...args)); 188 | dispatch(Actions.updateData(data)); 189 | return data; 190 | } catch (err) { 191 | dispatch(Actions.errors(err)); 192 | dispatch(Actions.endLoading()); 193 | return null; 194 | } 195 | }; 196 | } 197 | 198 | type Interpret = ((...Iterable) => R) => R; 199 | type ExtractReturn = $Call; 200 | 201 | type GeneratedAction = 202 | | ExtractReturn 203 | | ExtractReturn 204 | | ExtractReturn 205 | | ExtractReturn 206 | | ExtractReturn; 207 | 208 | class Subreducer { 209 | static partialReducer = partialReducer; 210 | 211 | static subreduce = ( 212 | state?: BoilerState = Getters.getInitialState(), 213 | action?: GeneratedAction | * // Support other action types 214 | ) => { 215 | if (!action || typeof action !== 'object' || !action.type) { 216 | return state; 217 | } 218 | switch (action.type) { 219 | case BEGIN_LOADING: 220 | return { ...state, meta: { ...state.meta, loading: true, } }; 221 | case END_LOADING: 222 | return { ...state, meta: { ...state.meta, loading: false } }; 223 | case ERRORS: 224 | return { ...state, meta: { ...state.meta, errors: action.payload } }; 225 | case UPDATE_DATA: 226 | return { 227 | ...state, 228 | data: action.payload, 229 | meta: { 230 | ...state.meta, 231 | loading: false, 232 | loaded: true, 233 | changeCount: state.meta.changeCount + 1, 234 | lastChangeTime: Date.now(), 235 | errors: null, 236 | } 237 | }; 238 | case RESET: 239 | return (Getters.getInitialState(): BoilerState); 240 | default: 241 | if (typeof Subreducer.partialReducer === 'function') { 242 | return { ...state, ...(Subreducer.partialReducer(state, action) || {}) }; 243 | } 244 | return state; 245 | } 246 | }; 247 | } 248 | 249 | if (!getInStore) { 250 | injectReducer([prefix, safeDataName], Subreducer.subreduce); 251 | } 252 | 253 | return { 254 | actions: Actions, 255 | subreducer: Subreducer.subreduce, 256 | getters: Getters, 257 | }; 258 | }; 259 | 260 | export default createFactory; 261 | 262 | -------------------------------------------------------------------------------- /test/factory.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createFactory from '../src/factory'; 3 | const injectReducer = jest.fn(); 4 | const factory = createFactory(injectReducer); 5 | 6 | describe('factory-simple', () => { 7 | const loadNumber = jest.fn().mockReturnValue(10); 8 | 9 | const name = 'TEST_DATA'; 10 | const getInStore = x => x; 11 | const { 12 | actions, 13 | subreducer, 14 | getters, 15 | } = factory({ 16 | name, 17 | load: loadNumber, 18 | getInStore, 19 | }); 20 | 21 | describe('getters', () => { 22 | describe('get', () => { 23 | it('is the same function as getInStore', () => { 24 | expect(getters.get).toBe(getInStore); 25 | }); 26 | }); 27 | 28 | describe('getData', () => { 29 | it('extracts the data field', () => { 30 | const mockStore = { data: {}, meta: {} }; 31 | expect(getters.getData(mockStore)).toBe(mockStore.data); 32 | }); 33 | }); 34 | 35 | describe('getMeta', () => { 36 | it('extracts the meta field', () => { 37 | const mockStore = { data: {}, meta: {} }; 38 | expect(getters.getMeta(mockStore)).toBe(mockStore.meta); 39 | }); 40 | }); 41 | 42 | describe('getInitialState', () => { 43 | it('returns object with data and meta', () => { 44 | const state = getters.getInitialState(); 45 | expect(typeof state).toEqual('object'); 46 | expect(state).toHaveProperty('data'); 47 | expect(state).toHaveProperty('meta'); 48 | }); 49 | 50 | it('returns meta with sensible defaults', () => { 51 | const meta = getters.getInitialState().meta; 52 | expect(meta.loaded).toBeFalsy(); 53 | expect(meta.changeCount).toBe(0); 54 | expect(meta.loading).toBeFalsy(); 55 | expect(meta.lastChangeTime).toBeFalsy(); 56 | expect(meta.errors).toBeFalsy(); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('actions', () => { 62 | describe('beginLoading', () => { 63 | it('is a valid action', () => { 64 | const action = actions.beginLoading(); 65 | expect(action).toHaveProperty('type'); 66 | expect(action.type).toEqual(expect.stringContaining('begin loading')); 67 | expect(action.type).toEqual(expect.stringContaining(name)); 68 | }); 69 | }); 70 | 71 | describe('endLoading', () => { 72 | it('is a valid action', () => { 73 | const action = actions.endLoading(); 74 | expect(action).toHaveProperty('type'); 75 | expect(action.type).toEqual(expect.stringContaining('end loading')); 76 | expect(action.type).toEqual(expect.stringContaining(name)); 77 | }); 78 | }); 79 | 80 | describe('reset', () => { 81 | it('is a valid action', () => { 82 | const action = actions.reset(); 83 | expect(action).toHaveProperty('type'); 84 | expect(action.type).toEqual(expect.stringContaining('reset')); 85 | expect(action.type).toEqual(expect.stringContaining(name)); 86 | }); 87 | }); 88 | 89 | describe('errors', () => { 90 | it('is a valid action', () => { 91 | const action = actions.errors('bad things happened'); 92 | expect(action).toHaveProperty('type'); 93 | expect(action.type).toEqual(expect.stringContaining('error')); 94 | expect(action.type).toEqual(expect.stringContaining(name)); 95 | 96 | expect(action).toHaveProperty('payload'); 97 | expect(action.payload).toEqual(expect.stringContaining('bad things')); 98 | }); 99 | }); 100 | 101 | describe('clearErrors', () => { 102 | it('is a valid action', () => { 103 | const action = actions.clearErrors(); 104 | expect(action).toHaveProperty('type'); 105 | expect(action.type).toEqual(expect.stringContaining('error')); 106 | expect(action.type).toEqual(expect.stringContaining(name)); 107 | 108 | expect(action).toHaveProperty('payload'); 109 | expect(action.payload).toEqual(null); 110 | }); 111 | }); 112 | 113 | describe('updateData', () => { 114 | it('is a valid action', () => { 115 | const action = actions.updateData('string data'); 116 | expect(action).toHaveProperty('type'); 117 | expect(action.type).toEqual(expect.stringContaining('update')); 118 | expect(action.type).toEqual(expect.stringContaining(name)); 119 | 120 | expect(action).toHaveProperty('payload'); 121 | expect(action.payload).toEqual('string data'); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('thunks', () => { 127 | describe('update', () => { 128 | it('does nothing since it was not defined', () => { 129 | const dispatch = jest.fn(); 130 | const getState = jest.fn().mockReturnValue(getters.getInitialState()); 131 | const asyncThunk = actions.update(); 132 | 133 | return asyncThunk(dispatch, getState).then(() => { 134 | expect(dispatch).not.toHaveBeenCalled(); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('load', () => { 140 | it('works', () => { 141 | const dispatch = jest.fn(); 142 | const getState = jest.fn().mockReturnValue(getters.getInitialState()); 143 | const asyncThunk = actions.load(); 144 | 145 | return asyncThunk(dispatch, getState).then(() => { 146 | expect(dispatch).toHaveBeenCalledWith( 147 | expect.objectContaining({ 148 | type: expect.stringContaining('begin loading'), 149 | }) 150 | ); 151 | expect(dispatch).toHaveBeenCalledWith( 152 | expect.objectContaining({ 153 | type: expect.stringContaining('update'), 154 | }) 155 | ); 156 | expect(dispatch).not.toHaveBeenCalledWith( 157 | expect.objectContaining({ 158 | type: expect.stringContaining('end loading'), 159 | }) 160 | ); 161 | expect(dispatch).not.toHaveBeenCalledWith( 162 | expect.objectContaining({ 163 | type: expect.stringContaining('errors'), 164 | }) 165 | ); 166 | }); 167 | }); 168 | }); 169 | }); 170 | 171 | describe('subreducer', () => { 172 | const mockState = getters.getInitialState(); 173 | const mockStateFilled = { 174 | data: 'special data', 175 | meta: { 176 | loading: false, 177 | loaded: true, 178 | lastChangeTime: 1, 179 | changeCount: 2, 180 | errors: 'could not load twice', 181 | }, 182 | }; 183 | 184 | it('returns correct initial state on no-op action', () => { 185 | const state = subreducer(); 186 | expect(state).toEqual(mockState); 187 | }); 188 | 189 | it('updates loading with begin load action', () => { 190 | expect(subreducer(mockState, actions.beginLoading())).toMatchObject({ 191 | meta: { 192 | loading: true, 193 | }, 194 | }); 195 | expect(subreducer(mockStateFilled, actions.beginLoading())).toMatchObject({ 196 | meta: { 197 | loading: true, 198 | }, 199 | }); 200 | }); 201 | 202 | it('updates loading with end load action', () => { 203 | expect(subreducer(mockState, actions.endLoading())).toMatchObject({ 204 | meta: { 205 | loading: false, 206 | }, 207 | }); 208 | expect(subreducer(mockStateFilled, actions.endLoading())).toMatchObject({ 209 | meta: { 210 | loading: false, 211 | }, 212 | }); 213 | }); 214 | 215 | it('updates errors with errors action', () => { 216 | expect(subreducer(mockState, actions.errors('error value'))).toMatchObject({ 217 | meta: { 218 | errors: 'error value', 219 | }, 220 | }); 221 | 222 | expect(subreducer(mockStateFilled, actions.errors('error value'))).toMatchObject({ 223 | meta: { 224 | errors: 'error value', 225 | }, 226 | }); 227 | }); 228 | 229 | it('clears errors with clearErrors action', () => { 230 | expect(subreducer(mockState, actions.clearErrors())).toMatchObject({ 231 | meta: { 232 | errors: null, 233 | }, 234 | }); 235 | 236 | expect(subreducer(mockStateFilled, actions.clearErrors())).toMatchObject({ 237 | meta: { 238 | errors: null, 239 | }, 240 | }); 241 | }); 242 | 243 | it('resets state on reset action', () => { 244 | const initialState = getters.getInitialState(); 245 | expect(subreducer(mockState, actions.reset())).toMatchObject(initialState); 246 | 247 | expect(subreducer(mockStateFilled, actions.reset())).toMatchObject(initialState); 248 | }); 249 | 250 | it('updates data field with update action', () => { 251 | expect(subreducer(mockState, actions.updateData(10))).toMatchObject({ 252 | data: 10, 253 | meta: { 254 | loaded: true, 255 | changeCount: 1, 256 | errors: null, 257 | }, 258 | }); 259 | }); 260 | }); 261 | }); 262 | 263 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux-RAGs 2 | [![npm version](https://img.shields.io/npm/v/redux-rags.svg?style=flat-square)](https://www.npmjs.com/package/redux-rags) 3 | [![npm downloads](https://img.shields.io/npm/dm/redux-rags.svg?style=flat-square)](https://www.npmjs.com/package/redux-rags) 4 | [![Coverage Status](https://coveralls.io/repos/github/CodeSignal/redux-rags/badge.svg?branch=master)](https://coveralls.io/github/CodeSignal/redux-rags?branch=master) 5 | 6 | > :warning: **This package is not maintained**: It has been archived. You are welcome to read it or even fork it, but it will not be updated going forward. 7 | 8 | 9 | Redux **R**educers, **A**ctions, and **G**etters: Simplified! 10 | 11 | TLDR: No need to create or connect subreducers for simple async requests. Your Redux connection is handled by a single function 12 | ```js 13 | const rag = ragFactory({ load }); 14 | 15 | const newLoad = rag.actions.load; 16 | const getData = rag.getters.getData; 17 | connect(state => ({ data: getData(store) }), { load: newLoad })(Component); 18 | ``` 19 | 20 | ## Motivation 21 | We found that a lot of our reducers were redefining a common theme. We have some endpoint 22 | that we want to query for data. We'd run through the same steps every time: 23 | * Begin loading state 24 | * Fetch Data 25 | * End Loading State 26 | * Set data or error 27 | 28 |
29 | 30 |
31 | 32 | And that's it: A mini state machine that we redefined over and over, creating dozens of 33 | similar actions and subreducers. But what if there were an easier way? What if we could 34 | define that mini state machine once and re-use the logic with each kind of query? Well `redux-rags` is 35 | here to help! 36 | 37 | ## Injecting a Subreducer?! 38 | Using Redux requires quite a lot of boilerplate. You need to create Actions and maybe Action Creators. You need to create Reducers and place them somewhere in the store, maybe as a part of another reducer. There are a myriad of tools out there to help you simplify this process, and we're adding another to the list! This one is built around the idea that there are some pieces of state that you would like to have stored outside of a particular rendering context, but don't want to go through all the migration work to create a custom reducer in the store for this data. You want to take advantage of the benefits of Redux without trudging through all the boilerplate. 39 | 40 | So what if you had a way to add subreducers to Redux? Then you could write code that generates the subreducer structures you use frequently and inject them! You could now start writing subreducers aimed at general cases of data interaction and vastly reduce the time it takes to migrate a piece of state to Redux. We've provided our most useful abstraction here, a simple data request lifecycle. Hopefully you can see the opportunities that injecting a subreducer provides and can use the code here as an example for how to start reusing your own subreducer logic. 41 | 42 | For more details, I encourage you to check out [this Medium article by Jimmy Shen on subreducer injection.](https://medium.com/@jimmy_shen/inject-reducer-arbitrarily-rather-than-top-level-for-redux-store-to-replace-reducer-fdc1060a6a7) 43 | 44 | ## Usage 45 | 46 | 47 | ### Single File Redux Connection Example 48 | Here's how you'd interact with Redux for a data request. We're going to create the actions and 49 | automatically create and inject the subreducer. The returned `load` function will take the same 50 | arguments as the `load` function passed in, so you have control over the `load` thunk. 51 | 52 | Here we want to hit an endpoint with the `userId` parameter. We'll avoid worrying about 53 | loading states or anything for now. The call to `ragFactory` creates the actions and injects 54 | the subreducer for us. Then this state information is stored in Redux, so when users return to the 55 | component they'll see the cached data. [Play with a similar example on CodeSandbox](https://codesandbox.io/s/k218x138l7) 56 | 57 | ```js 58 | import React from 'react'; 59 | import { connect } from 'react-redux'; 60 | import { ragFactory } from 'redux-rags'; 61 | 62 | // Pass load to factory, factory returns basically the same load that we can give to connect. 63 | const { actions: { load }, getters: { getData } } = ragFactory({ 64 | load: (userId) => axios.get('/users', { userId }) 65 | }); 66 | 67 | function ViewUser({userId, load, user}) { 68 | return ( 69 |
70 | 71 | {user && } 72 |
73 | ) 74 | } 75 | 76 | export default connect(state => ({ user: getData(state)}), { load })(ViewUser) 77 | ``` 78 | 79 | 80 | And that's it! That's all you need to do; no need to manually create or place a subreducer 81 | anywhere. The subreducer is pretty basic, it holds the return of `load` in the data attribute. 82 | It also keeps track of meta information, like last load time, number of times loaded, and errors. 83 | 84 | If you want to keep track of data by parameters passed to `load`, i.e. loading a whole bunch 85 | of users, you'll want to use `ragFactoryMap`. This will use `ragFactory` behind the scenes to 86 | create the simple mini-reducer for each set of parameters you specify. 87 | 88 | 89 | ## Prerequisites 90 | You'll need `redux-thunk` and to restructure your `createRootReducer` function. We'll 91 | need to handle the addition of dynamic reducers! 92 | 93 | Here's what your Redux store creation will look like: 94 | ```js 95 | import { compose, combineReducers, createStore, applyMiddleware } from 'redux'; 96 | import thunk from 'redux-thunk'; 97 | 98 | import { combineAsyncReducers, configureRags } from 'redux-rags'; 99 | 100 | // Import a few reducers like you normally would to create a top level reducer: we'll use `userReducer` as an example 101 | import userReducer from './userReducer'; 102 | 103 | const createRootReducer = (dynamicReducers: Object = {}) => { 104 | dynamicReducers = combineAsyncReducers(combineReducers, dynamicReducers); 105 | 106 | return combineReducers(Object.assign({}, dynamicReducers, { 107 | // Then list your reducers below 108 | userReducer 109 | })); 110 | } 111 | 112 | const middleware = applyMiddleware(thunk); 113 | const store = createStore(createRootReducer(), compose(middleware)); 114 | configureRags(store, createRootReducer); 115 | ``` 116 | 117 | ## What Actions and Getters are there? 118 | Getters take in the full store as the argument. For `ragFactory`, you'll have access to the following getters: 119 | 120 | - `get`: Returns `BoilerState`, so an object that looks like `{ data, meta }`. 121 | - `getData`: Returns the `data` attribute from `BoilerState`. It would be type `?T`, whatever your load / update functions passed to the `ragFactory` return. 122 | - `getMeta`: Returns the `meta` attribute from `BoilerState`. This will be an object that looks like `{ loading, lastLoadTime, errors }`, with a few more properties. Look for the types / additional properties in the type definition. 123 | - `getIsLoading`: Returns the `loading` attribute from the `meta` field. A common use case is for quick access to the loading state, so a special getter is provided for this meta attribute. 124 | 125 | Actions are implementations of commonly used features to manipulate the state. Most of these are action creators, with `load` and `update` being thunks. 126 | 127 | - `beginLoading`: Sets `loading` meta property to true. 128 | - `endLoading`: Sets `loading` meta property to false. 129 | - `reset`: Returns the subreducer to the initial state. 130 | - `errors`: Sets the errors meta value. 131 | - `clearErrors`: Clears the errors meta value. 132 | - `updateData`: Sets the data value for the subreducer. 133 | - `update`: Thunk. Calls the `update` function passed in to `ragsFactory` and sets the data value to the result. Might also set the error attribute if the `update` function throws an error. Passed in function should look like: `(dataValue, ...params) => newData`. Returned func has signature: `(...params) => newData`. This is because the current data value will be added to the function internally. 134 | - `load`: Thunk. Calls the appropriate sequence of `beginLoading`, `updateData`, `endLoading` actions while calling the `load` function passed to `ragsFactory`. If there are errors while executing, the error meta value will be set. Passed in function looks like: `(...params) => newData`, returned function looks like `(...params) => newData`. 135 | 136 | ## Details and explanations of exports 137 | 138 | ### A few types to consider: 139 | The generated subreducer uses the following type for the state: 140 | ```js 141 | type BoilerState = { 142 | data: ?T, 143 | meta: { 144 | loading: boolean, 145 | loaded: boolean, 146 | changeCount: number, 147 | lastChangeTime: ?number, 148 | errors?: * 149 | } 150 | } 151 | ``` 152 | | Meta Property | Description | 153 | |:----:|:---:| 154 | | loading | Is the data currently being loaded | 155 | | loaded | Has the data every been loaded? Equivalent to `changeCount > 0` | 156 | | changeCount | Number of times the data has been loaded / updated | 157 | | lastChangeTime | The time of the last change | 158 | | errors | Error object from the `load` or `update` function, if any | 159 | 160 | ### configureRags 161 | Signature: `(store: Object, createRootReducer: Function) => void`. 162 | 163 | This function is how you set up the module, giving it access to your Redux store. This is used to configure the reducer 164 | injector. We need `store` for the [`replaceReducer`](https://redux.js.org/api/store#replaceReducer) method. And we'll need to have a formatted `createRootReducer` function that accepts dynamic reducers. We've provided a `combineAsyncReducers` function to make this easier. 165 | 166 | ### combineAsyncReducers 167 | Signature: `(combineReducers: Function, dynamicReducers: Object) => newDynamicReducers` 168 | 169 | This function is designed to recursively call the `combineReducers` function on the second parameter `dynamicReducers`. This means that whatever the structure of `dynamicReducers` we can register the functions with `redux`. The package internally will add values underneath `@@redux-rags` and `@@redux-rags/map` keys in the `dynamicReducers` object. 170 | 171 | ### injectReducer 172 | Signature: `(keys: Array, reducer: Function) => void` 173 | 174 | This function will insert the given `reducer` at the nested key location in `dynamicReducers` specified by `keys`. For example, if the user has called `ragFactory` with the name `'user info'` internally we will call this function with `injectReducer(['@@redux-rags', 'user info'], subreducer)`. This will set values so that `dynamicReducers['@@redux-rags']['user info'] == subreducer` and update the store with the newly injected subreducer. 175 | 176 | You can call this function and inject your own subreducers. You could just use `configureRags`, `combineAsyncReducers`, and `injectReducer` to setup your store to allow for dynamic reducers and skip the rest of this module. 177 | 178 | ### ragFactory 179 | 180 | Though both `load` and `update` are optional, you pass at least one. With neither of these, there is no good way to 181 | update the data or metadata fields. 182 | 183 | The `ragFactory` will place the dynamically added subreducers in the `@@redux-rags` top leve of the Redux store. 184 | If you want to place this somewhere else, just put the returned `subreducer` somewhere in your Redux store and 185 | pass in the `getInStore` function. 186 | 187 | | Props | Type | Optional | Description | 188 | |:-------:|:------:|:----------:|:-------------:| 189 | | name | string | yes | A string that identifies the data. If you pass a name, the actions sent to Redux and the automatically injected subreducer will use that name. | 190 | | load | Function | yes | A function that loads data | 191 | | getInitialState | Function | yes | Function to create initial data for the return of `load`, will use null if not defined | 192 | | getInStore | Function | yes | `(state) => state.some.path.to.subreducer` Function to locate the subreducer in the store. Will place in default location if not specified, otherwise will use this location in getters. | 193 | | update | Function | yes | (dataValue, ...args) => { return newData; } A function to manipulate the data attribute. Returned `update` will have signature `(...args) => void` | 194 | | loadOnlyOnce | boolean | yes | Prevent the `load` function from being called more than once. | 195 | | partialReducer | Function | yes | Allows you to hook in to other actions in the Redux store and modify the state. You'll be working with the BoilerState argument. Extend the reducer and listen to other actions, for example you can could clear the data on user logout. Write this like a reducer to extend the functionality of the generated boilerplate. You can also delay assignment if you want to utilize actions in the returned Actions object, just assign the function to `subreducer.partialReducer`.| 196 | 197 | ### ragFactoryMap 198 | What's this `ragFactoryMap`? Well if you want to cache data based on the query parameters, then the `ragFactoryMap` is for you! 199 | For each collection of args, a new reducer will be injected. The params are stringified and used as the key for the injected subreducer. 200 | 201 | The `ragFactoryMap` builds on top of `ragFactory`. That factory is used internally to generate the subreducers and actions for each key. 202 | 203 | | Props | Type | Optional | Description | 204 | |:-----:|:----:|:--------:|:-----------:| 205 | | name | string | yes | A string that identifies the data. If you pass a name, the actions sent to Redux and the automatically injected subreducer will use that name. | 206 | | load | Function | no | A function that loads data. The returned function will have the same signature. | 207 | | getInitialState | Function | yes | Function to create initial data for the return of load, will use null if not defined | 208 | 209 | 210 | ## Additional Examples 211 | 212 | ### Defining the Redux interactions in a separate file and importing 213 | We'll use a basic fetch for this example, hitting some json endpoint. The returned load function 214 | has the same signature as the function we passed in, so in this case `loadFaq` will not use 215 | any parameters. 216 | ```js 217 | import { ragFactory } from 'redux-rags'; 218 | 219 | const axiosLoadFaq = () => fetch('http://api.example.com/data.json'); 220 | const { actions, getters } = ragFactory({ load: axiosLoadFaq }); 221 | 222 | export { 223 | loadFaq: actions.load, 224 | getFaq: getters.getData, 225 | getIsFaqLoading: getters.getIsLoading, 226 | }; 227 | ``` 228 | 229 | #### Use that Data Request in a React Component 230 | Import the Getters and Action as you normally would and use them with `react-redux`. If you're used to 231 | defining thunks and getters in a different file and import them, then the code below should look 232 | remarkably similar to code that you've already seen or even written. 233 | ```js 234 | import React from 'react'; 235 | import { connect } from 'react-redux'; 236 | import { loadFaq, getFaq, getIsFaqLoading } from './faqData'; 237 | import Loading from './Loading'; 238 | import FaqItem from './FaqItem'; 239 | 240 | class Faq extends React.Component { 241 | componentDidMount() { 242 | this.props.loadFaq(); 243 | } 244 | 245 | render() { 246 | const { isLoading, faqData } = this.props; 247 | if (isLoading) { 248 | return ; 249 | } 250 | 251 | return ( 252 | 253 | {faqData.map((data, index) => } 254 | 255 | ) 256 | } 257 | } 258 | 259 | const mapStateFromProps = state => ({ 260 | faqData: getFaq(state), 261 | isLoading: getIsFaqLoading(state) 262 | }); 263 | 264 | export default connect(mapStateFromProps, { loadFaq })(Faq); 265 | ``` 266 | 267 | ### Placing the subreducer in a custom location in Redux 268 | Connect subreducer to your desired location in the Redux store, and tell the generator 269 | where you put it. Getters are also factories for you. If you don't care where the 270 | subreducer lives, don't pass in a `getInStore` method and the generator will place it for you. 271 | In this case taking `subreducer` and adding it to the `combineSubreducers` function in the `currentUser` subreducer. 272 | But you can place it wherever you want, just update the `getInStore` function passed to `ragFactory`. 273 | ```js 274 | import { generator }; 275 | const { actions, subreducer, getters } = generator({ 276 | name: 'MY_DATA', 277 | load: (param) => methodService.run('fetchSomething', param), 278 | getInStore: (store) => store && store.currentUser && store.currentUser.my_data 279 | }); 280 | ``` 281 | 282 | ### Using update to manipulate the stored value 283 | In addition to `load` you can also pass in an `update` function, which will take the 284 | current `data` value as the first parameter. An example of this might be an update 285 | function that increments the current value by 2. The signature of the returned function 286 | is slightly different, as the `data` first argument is passed internally, so you pass 287 | in a function that takes parameters `(currentData, ...additionalParams)` and the returned 288 | `actions.update` function will take the parameters `(...additionalParams)`. 289 | [View an example that relies entirely on the `update` funciton on CodeSandbox](https://codesandbox.io/s/1307vwj8zj) 290 | ```js 291 | import { generator }; 292 | const { actions : { update }, getters } = generator({ 293 | name: 'MY_DATA', 294 | update: (currentData) => currentData + 2, 295 | }); 296 | ``` 297 | 298 | ### Using the ragFactoryMap 299 | Not much to say, it's pretty much how you would use `ragFactory`, except treating the input arguments 300 | as a key for a different mini state machine. 301 | 302 | ```js 303 | const { action: { load }, getters: { getData, getMeta } } = 304 | ragFactoryMap({ load: Function }); 305 | 306 | // In a component after `load` was passed in through mapDispatchToProps. 307 | 308 | class Component extends React.Component { 309 | componentDidMount() { 310 | const { loaded, load, taskId, userId } = this.props; 311 | // Load the data for given userId and taskId if not loaded 312 | !loaded && load(userId, taskId); 313 | } 314 | // ... Other functions of the class 315 | } 316 | 317 | function mapStateToProps(state, props) { 318 | const { userId, taskId } = props; 319 | return { 320 | loaded: getMeta(userId, taskId).loaded, 321 | taskData: getData(userId, taskId) 322 | } 323 | } 324 | export default connect(mapStateToProps)(Component); 325 | ``` 326 | 327 | ## License 328 | MIT 329 | 330 | ## Thanks 331 | 332 | Huge thanks to Jimmy Shen for [the amazing Medium article already linked above](https://medium.com/@jimmy_shen/inject-reducer-arbitrarily-rather-than-top-level-for-redux-store-to-replace-reducer-fdc1060a6a7) 333 | on dynamically injecting reducers into the Redux store. 334 | -------------------------------------------------------------------------------- /flow-typed/npm/jest_v23.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ad251f3a3446f6ab4e6691a94e701cad 2 | // flow-typed version: caa120caaa/jest_v23.x.x/flow_>=v0.39.x 3 | 4 | type JestMockFn, TReturn> = { 5 | (...args: TArguments): TReturn, 6 | /** 7 | * An object for introspecting mock calls 8 | */ 9 | mock: { 10 | /** 11 | * An array that represents all calls that have been made into this mock 12 | * function. Each call is represented by an array of arguments that were 13 | * passed during the call. 14 | */ 15 | calls: Array, 16 | /** 17 | * An array that contains all the object instances that have been 18 | * instantiated from this mock function. 19 | */ 20 | instances: Array, 21 | }, 22 | /** 23 | * Resets all information stored in the mockFn.mock.calls and 24 | * mockFn.mock.instances arrays. Often this is useful when you want to clean 25 | * up a mock's usage data between two assertions. 26 | */ 27 | mockClear(): void, 28 | /** 29 | * Resets all information stored in the mock. This is useful when you want to 30 | * completely restore a mock back to its initial state. 31 | */ 32 | mockReset(): void, 33 | /** 34 | * Removes the mock and restores the initial implementation. This is useful 35 | * when you want to mock functions in certain test cases and restore the 36 | * original implementation in others. Beware that mockFn.mockRestore only 37 | * works when mock was created with jest.spyOn. Thus you have to take care of 38 | * restoration yourself when manually assigning jest.fn(). 39 | */ 40 | mockRestore(): void, 41 | /** 42 | * Accepts a function that should be used as the implementation of the mock. 43 | * The mock itself will still record all calls that go into and instances 44 | * that come from itself -- the only difference is that the implementation 45 | * will also be executed when the mock is called. 46 | */ 47 | mockImplementation(fn: (...args: TArguments) => TReturn): JestMockFn, 48 | /** 49 | * Accepts a function that will be used as an implementation of the mock for 50 | * one call to the mocked function. Can be chained so that multiple function 51 | * calls produce different results. 52 | */ 53 | mockImplementationOnce(fn: (...args: TArguments) => TReturn): JestMockFn, 54 | /** 55 | * Accepts a string to use in test result output in place of "jest.fn()" to 56 | * indicate which mock function is being referenced. 57 | */ 58 | mockName(name: string): JestMockFn, 59 | /** 60 | * Just a simple sugar function for returning `this` 61 | */ 62 | mockReturnThis(): void, 63 | /** 64 | * Accepts a value that will be returned whenever the mock function is called. 65 | */ 66 | mockReturnValue(value: TReturn): JestMockFn, 67 | /** 68 | * Sugar for only returning a value once inside your mock 69 | */ 70 | mockReturnValueOnce(value: TReturn): JestMockFn, 71 | /** 72 | * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) 73 | */ 74 | mockResolvedValue(value: TReturn): JestMockFn>, 75 | /** 76 | * Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value)) 77 | */ 78 | mockResolvedValueOnce(value: TReturn): JestMockFn>, 79 | /** 80 | * Sugar for jest.fn().mockImplementation(() => Promise.reject(value)) 81 | */ 82 | mockRejectedValue(value: TReturn): JestMockFn>, 83 | /** 84 | * Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value)) 85 | */ 86 | mockRejectedValueOnce(value: TReturn): JestMockFn>, 87 | }; 88 | 89 | type JestAsymmetricEqualityType = { 90 | /** 91 | * A custom Jasmine equality tester 92 | */ 93 | asymmetricMatch(value: mixed): boolean, 94 | }; 95 | 96 | type JestCallsType = { 97 | allArgs(): mixed, 98 | all(): mixed, 99 | any(): boolean, 100 | count(): number, 101 | first(): mixed, 102 | mostRecent(): mixed, 103 | reset(): void, 104 | }; 105 | 106 | type JestClockType = { 107 | install(): void, 108 | mockDate(date: Date): void, 109 | tick(milliseconds?: number): void, 110 | uninstall(): void, 111 | }; 112 | 113 | type JestMatcherResult = { 114 | message?: string | (() => string), 115 | pass: boolean, 116 | }; 117 | 118 | type JestMatcher = (actual: any, expected: any) => JestMatcherResult; 119 | 120 | type JestPromiseType = { 121 | /** 122 | * Use rejects to unwrap the reason of a rejected promise so any other 123 | * matcher can be chained. If the promise is fulfilled the assertion fails. 124 | */ 125 | rejects: JestExpectType, 126 | /** 127 | * Use resolves to unwrap the value of a fulfilled promise so any other 128 | * matcher can be chained. If the promise is rejected the assertion fails. 129 | */ 130 | resolves: JestExpectType, 131 | }; 132 | 133 | /** 134 | * Jest allows functions and classes to be used as test names in test() and 135 | * describe() 136 | */ 137 | type JestTestName = string | Function; 138 | 139 | /** 140 | * Plugin: jest-styled-components 141 | */ 142 | 143 | type JestStyledComponentsMatcherValue = 144 | | string 145 | | JestAsymmetricEqualityType 146 | | RegExp 147 | | typeof undefined; 148 | 149 | type JestStyledComponentsMatcherOptions = { 150 | media?: string, 151 | modifier?: string, 152 | supports?: string, 153 | }; 154 | 155 | type JestStyledComponentsMatchersType = { 156 | toHaveStyleRule( 157 | property: string, 158 | value: JestStyledComponentsMatcherValue, 159 | options?: JestStyledComponentsMatcherOptions 160 | ): void, 161 | }; 162 | 163 | /** 164 | * Plugin: jest-enzyme 165 | */ 166 | type EnzymeMatchersType = { 167 | toBeChecked(): void, 168 | toBeDisabled(): void, 169 | toBeEmpty(): void, 170 | toBeEmptyRender(): void, 171 | toBePresent(): void, 172 | toContainReact(element: React$Element): void, 173 | toExist(): void, 174 | toHaveClassName(className: string): void, 175 | toHaveHTML(html: string): void, 176 | toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void), 177 | toHaveRef(refName: string): void, 178 | toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void), 179 | toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void), 180 | toHaveTagName(tagName: string): void, 181 | toHaveText(text: string): void, 182 | toIncludeText(text: string): void, 183 | toHaveValue(value: any): void, 184 | toMatchElement(element: React$Element): void, 185 | toMatchSelector(selector: string): void, 186 | }; 187 | 188 | // DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers 189 | type DomTestingLibraryType = { 190 | toBeInTheDOM(): void, 191 | toHaveTextContent(content: string): void, 192 | toHaveAttribute(name: string, expectedValue?: string): void, 193 | }; 194 | 195 | // Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers 196 | type JestJQueryMatchersType = { 197 | toExist(): void, 198 | toHaveLength(len: number): void, 199 | toHaveId(id: string): void, 200 | toHaveClass(className: string): void, 201 | toHaveTag(tag: string): void, 202 | toHaveAttr(key: string, val?: any): void, 203 | toHaveProp(key: string, val?: any): void, 204 | toHaveText(text: string | RegExp): void, 205 | toHaveData(key: string, val?: any): void, 206 | toHaveValue(val: any): void, 207 | toHaveCss(css: { [key: string]: any }): void, 208 | toBeChecked(): void, 209 | toBeDisabled(): void, 210 | toBeEmpty(): void, 211 | toBeHidden(): void, 212 | toBeSelected(): void, 213 | toBeVisible(): void, 214 | toBeFocused(): void, 215 | toBeInDom(): void, 216 | toBeMatchedBy(sel: string): void, 217 | toHaveDescendant(sel: string): void, 218 | toHaveDescendantWithText(sel: string, text: string | RegExp): void, 219 | }; 220 | 221 | // Jest Extended Matchers: https://github.com/jest-community/jest-extended 222 | type JestExtendedMatchersType = { 223 | /** 224 | * Note: Currently unimplemented 225 | * Passing assertion 226 | * 227 | * @param {String} message 228 | */ 229 | // pass(message: string): void; 230 | 231 | /** 232 | * Note: Currently unimplemented 233 | * Failing assertion 234 | * 235 | * @param {String} message 236 | */ 237 | // fail(message: string): void; 238 | 239 | /** 240 | * Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty. 241 | */ 242 | toBeEmpty(): void, 243 | 244 | /** 245 | * Use .toBeOneOf when checking if a value is a member of a given Array. 246 | * @param {Array.<*>} members 247 | */ 248 | toBeOneOf(members: any[]): void, 249 | 250 | /** 251 | * Use `.toBeNil` when checking a value is `null` or `undefined`. 252 | */ 253 | toBeNil(): void, 254 | 255 | /** 256 | * Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`. 257 | * @param {Function} predicate 258 | */ 259 | toSatisfy(predicate: (n: any) => boolean): void, 260 | 261 | /** 262 | * Use `.toBeArray` when checking if a value is an `Array`. 263 | */ 264 | toBeArray(): void, 265 | 266 | /** 267 | * Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x. 268 | * @param {Number} x 269 | */ 270 | toBeArrayOfSize(x: number): void, 271 | 272 | /** 273 | * Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set. 274 | * @param {Array.<*>} members 275 | */ 276 | toIncludeAllMembers(members: any[]): void, 277 | 278 | /** 279 | * Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set. 280 | * @param {Array.<*>} members 281 | */ 282 | toIncludeAnyMembers(members: any[]): void, 283 | 284 | /** 285 | * Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array. 286 | * @param {Function} predicate 287 | */ 288 | toSatisfyAll(predicate: (n: any) => boolean): void, 289 | 290 | /** 291 | * Use `.toBeBoolean` when checking if a value is a `Boolean`. 292 | */ 293 | toBeBoolean(): void, 294 | 295 | /** 296 | * Use `.toBeTrue` when checking a value is equal (===) to `true`. 297 | */ 298 | toBeTrue(): void, 299 | 300 | /** 301 | * Use `.toBeFalse` when checking a value is equal (===) to `false`. 302 | */ 303 | toBeFalse(): void, 304 | 305 | /** 306 | * Use .toBeDate when checking if a value is a Date. 307 | */ 308 | toBeDate(): void, 309 | 310 | /** 311 | * Use `.toBeFunction` when checking if a value is a `Function`. 312 | */ 313 | toBeFunction(): void, 314 | 315 | /** 316 | * Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`. 317 | * 318 | * Note: Required Jest version >22 319 | * Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same 320 | * 321 | * @param {Mock} mock 322 | */ 323 | toHaveBeenCalledBefore(mock: JestMockFn): void, 324 | 325 | /** 326 | * Use `.toBeNumber` when checking if a value is a `Number`. 327 | */ 328 | toBeNumber(): void, 329 | 330 | /** 331 | * Use `.toBeNaN` when checking a value is `NaN`. 332 | */ 333 | toBeNaN(): void, 334 | 335 | /** 336 | * Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`. 337 | */ 338 | toBeFinite(): void, 339 | 340 | /** 341 | * Use `.toBePositive` when checking if a value is a positive `Number`. 342 | */ 343 | toBePositive(): void, 344 | 345 | /** 346 | * Use `.toBeNegative` when checking if a value is a negative `Number`. 347 | */ 348 | toBeNegative(): void, 349 | 350 | /** 351 | * Use `.toBeEven` when checking if a value is an even `Number`. 352 | */ 353 | toBeEven(): void, 354 | 355 | /** 356 | * Use `.toBeOdd` when checking if a value is an odd `Number`. 357 | */ 358 | toBeOdd(): void, 359 | 360 | /** 361 | * Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive). 362 | * 363 | * @param {Number} start 364 | * @param {Number} end 365 | */ 366 | toBeWithin(start: number, end: number): void, 367 | 368 | /** 369 | * Use `.toBeObject` when checking if a value is an `Object`. 370 | */ 371 | toBeObject(): void, 372 | 373 | /** 374 | * Use `.toContainKey` when checking if an object contains the provided key. 375 | * 376 | * @param {String} key 377 | */ 378 | toContainKey(key: string): void, 379 | 380 | /** 381 | * Use `.toContainKeys` when checking if an object has all of the provided keys. 382 | * 383 | * @param {Array.} keys 384 | */ 385 | toContainKeys(keys: string[]): void, 386 | 387 | /** 388 | * Use `.toContainAllKeys` when checking if an object only contains all of the provided keys. 389 | * 390 | * @param {Array.} keys 391 | */ 392 | toContainAllKeys(keys: string[]): void, 393 | 394 | /** 395 | * Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys. 396 | * 397 | * @param {Array.} keys 398 | */ 399 | toContainAnyKeys(keys: string[]): void, 400 | 401 | /** 402 | * Use `.toContainValue` when checking if an object contains the provided value. 403 | * 404 | * @param {*} value 405 | */ 406 | toContainValue(value: any): void, 407 | 408 | /** 409 | * Use `.toContainValues` when checking if an object contains all of the provided values. 410 | * 411 | * @param {Array.<*>} values 412 | */ 413 | toContainValues(values: any[]): void, 414 | 415 | /** 416 | * Use `.toContainAllValues` when checking if an object only contains all of the provided values. 417 | * 418 | * @param {Array.<*>} values 419 | */ 420 | toContainAllValues(values: any[]): void, 421 | 422 | /** 423 | * Use `.toContainAnyValues` when checking if an object contains at least one of the provided values. 424 | * 425 | * @param {Array.<*>} values 426 | */ 427 | toContainAnyValues(values: any[]): void, 428 | 429 | /** 430 | * Use `.toContainEntry` when checking if an object contains the provided entry. 431 | * 432 | * @param {Array.} entry 433 | */ 434 | toContainEntry(entry: [string, string]): void, 435 | 436 | /** 437 | * Use `.toContainEntries` when checking if an object contains all of the provided entries. 438 | * 439 | * @param {Array.>} entries 440 | */ 441 | toContainEntries(entries: [string, string][]): void, 442 | 443 | /** 444 | * Use `.toContainAllEntries` when checking if an object only contains all of the provided entries. 445 | * 446 | * @param {Array.>} entries 447 | */ 448 | toContainAllEntries(entries: [string, string][]): void, 449 | 450 | /** 451 | * Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries. 452 | * 453 | * @param {Array.>} entries 454 | */ 455 | toContainAnyEntries(entries: [string, string][]): void, 456 | 457 | /** 458 | * Use `.toBeExtensible` when checking if an object is extensible. 459 | */ 460 | toBeExtensible(): void, 461 | 462 | /** 463 | * Use `.toBeFrozen` when checking if an object is frozen. 464 | */ 465 | toBeFrozen(): void, 466 | 467 | /** 468 | * Use `.toBeSealed` when checking if an object is sealed. 469 | */ 470 | toBeSealed(): void, 471 | 472 | /** 473 | * Use `.toBeString` when checking if a value is a `String`. 474 | */ 475 | toBeString(): void, 476 | 477 | /** 478 | * Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings. 479 | * 480 | * @param {String} string 481 | */ 482 | toEqualCaseInsensitive(string: string): void, 483 | 484 | /** 485 | * Use `.toStartWith` when checking if a `String` starts with a given `String` prefix. 486 | * 487 | * @param {String} prefix 488 | */ 489 | toStartWith(prefix: string): void, 490 | 491 | /** 492 | * Use `.toEndWith` when checking if a `String` ends with a given `String` suffix. 493 | * 494 | * @param {String} suffix 495 | */ 496 | toEndWith(suffix: string): void, 497 | 498 | /** 499 | * Use `.toInclude` when checking if a `String` includes the given `String` substring. 500 | * 501 | * @param {String} substring 502 | */ 503 | toInclude(substring: string): void, 504 | 505 | /** 506 | * Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times. 507 | * 508 | * @param {String} substring 509 | * @param {Number} times 510 | */ 511 | toIncludeRepeated(substring: string, times: number): void, 512 | 513 | /** 514 | * Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings. 515 | * 516 | * @param {Array.} substring 517 | */ 518 | toIncludeMultiple(substring: string[]): void, 519 | }; 520 | 521 | interface JestExpectType { 522 | not: JestExpectType & 523 | EnzymeMatchersType & 524 | DomTestingLibraryType & 525 | JestJQueryMatchersType & 526 | JestStyledComponentsMatchersType & 527 | JestExtendedMatchersType; 528 | /** 529 | * If you have a mock function, you can use .lastCalledWith to test what 530 | * arguments it was last called with. 531 | */ 532 | lastCalledWith(...args: Array): void; 533 | /** 534 | * toBe just checks that a value is what you expect. It uses === to check 535 | * strict equality. 536 | */ 537 | toBe(value: any): void; 538 | /** 539 | * Use .toBeCalledWith to ensure that a mock function was called with 540 | * specific arguments. 541 | */ 542 | toBeCalledWith(...args: Array): void; 543 | /** 544 | * Using exact equality with floating point numbers is a bad idea. Rounding 545 | * means that intuitive things fail. 546 | */ 547 | toBeCloseTo(num: number, delta: any): void; 548 | /** 549 | * Use .toBeDefined to check that a variable is not undefined. 550 | */ 551 | toBeDefined(): void; 552 | /** 553 | * Use .toBeFalsy when you don't care what a value is, you just want to 554 | * ensure a value is false in a boolean context. 555 | */ 556 | toBeFalsy(): void; 557 | /** 558 | * To compare floating point numbers, you can use toBeGreaterThan. 559 | */ 560 | toBeGreaterThan(number: number): void; 561 | /** 562 | * To compare floating point numbers, you can use toBeGreaterThanOrEqual. 563 | */ 564 | toBeGreaterThanOrEqual(number: number): void; 565 | /** 566 | * To compare floating point numbers, you can use toBeLessThan. 567 | */ 568 | toBeLessThan(number: number): void; 569 | /** 570 | * To compare floating point numbers, you can use toBeLessThanOrEqual. 571 | */ 572 | toBeLessThanOrEqual(number: number): void; 573 | /** 574 | * Use .toBeInstanceOf(Class) to check that an object is an instance of a 575 | * class. 576 | */ 577 | toBeInstanceOf(cls: Class<*>): void; 578 | /** 579 | * .toBeNull() is the same as .toBe(null) but the error messages are a bit 580 | * nicer. 581 | */ 582 | toBeNull(): void; 583 | /** 584 | * Use .toBeTruthy when you don't care what a value is, you just want to 585 | * ensure a value is true in a boolean context. 586 | */ 587 | toBeTruthy(): void; 588 | /** 589 | * Use .toBeUndefined to check that a variable is undefined. 590 | */ 591 | toBeUndefined(): void; 592 | /** 593 | * Use .toContain when you want to check that an item is in a list. For 594 | * testing the items in the list, this uses ===, a strict equality check. 595 | */ 596 | toContain(item: any): void; 597 | /** 598 | * Use .toContainEqual when you want to check that an item is in a list. For 599 | * testing the items in the list, this matcher recursively checks the 600 | * equality of all fields, rather than checking for object identity. 601 | */ 602 | toContainEqual(item: any): void; 603 | /** 604 | * Use .toEqual when you want to check that two objects have the same value. 605 | * This matcher recursively checks the equality of all fields, rather than 606 | * checking for object identity. 607 | */ 608 | toEqual(value: any): void; 609 | /** 610 | * Use .toHaveBeenCalled to ensure that a mock function got called. 611 | */ 612 | toHaveBeenCalled(): void; 613 | toBeCalled(): void; 614 | /** 615 | * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact 616 | * number of times. 617 | */ 618 | toHaveBeenCalledTimes(number: number): void; 619 | toBeCalledTimes(number: number): void; 620 | /** 621 | * 622 | */ 623 | toHaveBeenNthCalledWith(nthCall: number, ...args: Array): void; 624 | nthCalledWith(nthCall: number, ...args: Array): void; 625 | /** 626 | * 627 | */ 628 | toHaveReturned(): void; 629 | toReturn(): void; 630 | /** 631 | * 632 | */ 633 | toHaveReturnedTimes(number: number): void; 634 | toReturnTimes(number: number): void; 635 | /** 636 | * 637 | */ 638 | toHaveReturnedWith(value: any): void; 639 | toReturnWith(value: any): void; 640 | /** 641 | * 642 | */ 643 | toHaveLastReturnedWith(value: any): void; 644 | lastReturnedWith(value: any): void; 645 | /** 646 | * 647 | */ 648 | toHaveNthReturnedWith(nthCall: number, value: any): void; 649 | nthReturnedWith(nthCall: number, value: any): void; 650 | /** 651 | * Use .toHaveBeenCalledWith to ensure that a mock function was called with 652 | * specific arguments. 653 | */ 654 | toHaveBeenCalledWith(...args: Array): void; 655 | toBeCalledWith(...args: Array): void; 656 | /** 657 | * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called 658 | * with specific arguments. 659 | */ 660 | toHaveBeenLastCalledWith(...args: Array): void; 661 | lastCalledWith(...args: Array): void; 662 | /** 663 | * Check that an object has a .length property and it is set to a certain 664 | * numeric value. 665 | */ 666 | toHaveLength(number: number): void; 667 | /** 668 | * 669 | */ 670 | toHaveProperty(propPath: string, value?: any): void; 671 | /** 672 | * Use .toMatch to check that a string matches a regular expression or string. 673 | */ 674 | toMatch(regexpOrString: RegExp | string): void; 675 | /** 676 | * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. 677 | */ 678 | toMatchObject(object: Object | Array): void; 679 | /** 680 | * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object. 681 | */ 682 | toStrictEqual(value: any): void; 683 | /** 684 | * This ensures that an Object matches the most recent snapshot. 685 | */ 686 | toMatchSnapshot( 687 | propertyMatchers?: { [key: string]: JestAsymmetricEqualityType }, 688 | name?: string 689 | ): void; 690 | /** 691 | * This ensures that an Object matches the most recent snapshot. 692 | */ 693 | toMatchSnapshot(name: string): void; 694 | /** 695 | * Use .toThrow to test that a function throws when it is called. 696 | * If you want to test that a specific error gets thrown, you can provide an 697 | * argument to toThrow. The argument can be a string for the error message, 698 | * a class for the error, or a regex that should match the error. 699 | * 700 | * Alias: .toThrowError 701 | */ 702 | toThrow(message?: string | Error | Class | RegExp): void; 703 | toThrowError(message?: string | Error | Class | RegExp): void; 704 | /** 705 | * Use .toThrowErrorMatchingSnapshot to test that a function throws a error 706 | * matching the most recent snapshot when it is called. 707 | */ 708 | toThrowErrorMatchingSnapshot(): void; 709 | } 710 | 711 | type JestObjectType = { 712 | /** 713 | * Disables automatic mocking in the module loader. 714 | * 715 | * After this method is called, all `require()`s will return the real 716 | * versions of each module (rather than a mocked version). 717 | */ 718 | disableAutomock(): JestObjectType, 719 | /** 720 | * An un-hoisted version of disableAutomock 721 | */ 722 | autoMockOff(): JestObjectType, 723 | /** 724 | * Enables automatic mocking in the module loader. 725 | */ 726 | enableAutomock(): JestObjectType, 727 | /** 728 | * An un-hoisted version of enableAutomock 729 | */ 730 | autoMockOn(): JestObjectType, 731 | /** 732 | * Clears the mock.calls and mock.instances properties of all mocks. 733 | * Equivalent to calling .mockClear() on every mocked function. 734 | */ 735 | clearAllMocks(): JestObjectType, 736 | /** 737 | * Resets the state of all mocks. Equivalent to calling .mockReset() on every 738 | * mocked function. 739 | */ 740 | resetAllMocks(): JestObjectType, 741 | /** 742 | * Restores all mocks back to their original value. 743 | */ 744 | restoreAllMocks(): JestObjectType, 745 | /** 746 | * Removes any pending timers from the timer system. 747 | */ 748 | clearAllTimers(): void, 749 | /** 750 | * The same as `mock` but not moved to the top of the expectation by 751 | * babel-jest. 752 | */ 753 | doMock(moduleName: string, moduleFactory?: any): JestObjectType, 754 | /** 755 | * The same as `unmock` but not moved to the top of the expectation by 756 | * babel-jest. 757 | */ 758 | dontMock(moduleName: string): JestObjectType, 759 | /** 760 | * Returns a new, unused mock function. Optionally takes a mock 761 | * implementation. 762 | */ 763 | fn, TReturn>( 764 | implementation?: (...args: TArguments) => TReturn 765 | ): JestMockFn, 766 | /** 767 | * Determines if the given function is a mocked function. 768 | */ 769 | isMockFunction(fn: Function): boolean, 770 | /** 771 | * Given the name of a module, use the automatic mocking system to generate a 772 | * mocked version of the module for you. 773 | */ 774 | genMockFromModule(moduleName: string): any, 775 | /** 776 | * Mocks a module with an auto-mocked version when it is being required. 777 | * 778 | * The second argument can be used to specify an explicit module factory that 779 | * is being run instead of using Jest's automocking feature. 780 | * 781 | * The third argument can be used to create virtual mocks -- mocks of modules 782 | * that don't exist anywhere in the system. 783 | */ 784 | mock(moduleName: string, moduleFactory?: any, options?: Object): JestObjectType, 785 | /** 786 | * Returns the actual module instead of a mock, bypassing all checks on 787 | * whether the module should receive a mock implementation or not. 788 | */ 789 | requireActual(moduleName: string): any, 790 | /** 791 | * Returns a mock module instead of the actual module, bypassing all checks 792 | * on whether the module should be required normally or not. 793 | */ 794 | requireMock(moduleName: string): any, 795 | /** 796 | * Resets the module registry - the cache of all required modules. This is 797 | * useful to isolate modules where local state might conflict between tests. 798 | */ 799 | resetModules(): JestObjectType, 800 | /** 801 | * Exhausts the micro-task queue (usually interfaced in node via 802 | * process.nextTick). 803 | */ 804 | runAllTicks(): void, 805 | /** 806 | * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), 807 | * setInterval(), and setImmediate()). 808 | */ 809 | runAllTimers(): void, 810 | /** 811 | * Exhausts all tasks queued by setImmediate(). 812 | */ 813 | runAllImmediates(): void, 814 | /** 815 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout() 816 | * or setInterval() and setImmediate()). 817 | */ 818 | advanceTimersByTime(msToRun: number): void, 819 | /** 820 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout() 821 | * or setInterval() and setImmediate()). 822 | * 823 | * Renamed to `advanceTimersByTime`. 824 | */ 825 | runTimersToTime(msToRun: number): void, 826 | /** 827 | * Executes only the macro-tasks that are currently pending (i.e., only the 828 | * tasks that have been queued by setTimeout() or setInterval() up to this 829 | * point) 830 | */ 831 | runOnlyPendingTimers(): void, 832 | /** 833 | * Explicitly supplies the mock object that the module system should return 834 | * for the specified module. Note: It is recommended to use jest.mock() 835 | * instead. 836 | */ 837 | setMock(moduleName: string, moduleExports: any): JestObjectType, 838 | /** 839 | * Indicates that the module system should never return a mocked version of 840 | * the specified module from require() (e.g. that it should always return the 841 | * real module). 842 | */ 843 | unmock(moduleName: string): JestObjectType, 844 | /** 845 | * Instructs Jest to use fake versions of the standard timer functions 846 | * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, 847 | * setImmediate and clearImmediate). 848 | */ 849 | useFakeTimers(): JestObjectType, 850 | /** 851 | * Instructs Jest to use the real versions of the standard timer functions. 852 | */ 853 | useRealTimers(): JestObjectType, 854 | /** 855 | * Creates a mock function similar to jest.fn but also tracks calls to 856 | * object[methodName]. 857 | */ 858 | spyOn(object: Object, methodName: string, accessType?: 'get' | 'set'): JestMockFn, 859 | /** 860 | * Set the default timeout interval for tests and before/after hooks in milliseconds. 861 | * Note: The default timeout interval is 5 seconds if this method is not called. 862 | */ 863 | setTimeout(timeout: number): JestObjectType, 864 | }; 865 | 866 | type JestSpyType = { 867 | calls: JestCallsType, 868 | }; 869 | 870 | /** Runs this function after every test inside this context */ 871 | declare function afterEach(fn: (done: () => void) => ?Promise, timeout?: number): void; 872 | /** Runs this function before every test inside this context */ 873 | declare function beforeEach(fn: (done: () => void) => ?Promise, timeout?: number): void; 874 | /** Runs this function after all tests have finished inside this context */ 875 | declare function afterAll(fn: (done: () => void) => ?Promise, timeout?: number): void; 876 | /** Runs this function before any tests have started inside this context */ 877 | declare function beforeAll(fn: (done: () => void) => ?Promise, timeout?: number): void; 878 | 879 | /** A context for grouping tests together */ 880 | declare var describe: { 881 | /** 882 | * Creates a block that groups together several related tests in one "test suite" 883 | */ 884 | (name: JestTestName, fn: () => void): void, 885 | 886 | /** 887 | * Only run this describe block 888 | */ 889 | only(name: JestTestName, fn: () => void): void, 890 | 891 | /** 892 | * Skip running this describe block 893 | */ 894 | skip(name: JestTestName, fn: () => void): void, 895 | }; 896 | 897 | /** An individual test unit */ 898 | declare var it: { 899 | /** 900 | * An individual test unit 901 | * 902 | * @param {JestTestName} Name of Test 903 | * @param {Function} Test 904 | * @param {number} Timeout for the test, in milliseconds. 905 | */ 906 | (name: JestTestName, fn?: (done: () => void) => ?Promise, timeout?: number): void, 907 | /** 908 | * each runs this test against array of argument arrays per each run 909 | * 910 | * @param {table} table of Test 911 | */ 912 | each( 913 | table: Array> 914 | ): (name: JestTestName, fn?: (...args: Array) => ?Promise) => void, 915 | /** 916 | * Only run this test 917 | * 918 | * @param {JestTestName} Name of Test 919 | * @param {Function} Test 920 | * @param {number} Timeout for the test, in milliseconds. 921 | */ 922 | only( 923 | name: JestTestName, 924 | fn?: (done: () => void) => ?Promise, 925 | timeout?: number 926 | ): { 927 | each( 928 | table: Array> 929 | ): (name: JestTestName, fn?: (...args: Array) => ?Promise) => void, 930 | }, 931 | /** 932 | * Skip running this test 933 | * 934 | * @param {JestTestName} Name of Test 935 | * @param {Function} Test 936 | * @param {number} Timeout for the test, in milliseconds. 937 | */ 938 | skip(name: JestTestName, fn?: (done: () => void) => ?Promise, timeout?: number): void, 939 | /** 940 | * Run the test concurrently 941 | * 942 | * @param {JestTestName} Name of Test 943 | * @param {Function} Test 944 | * @param {number} Timeout for the test, in milliseconds. 945 | */ 946 | concurrent( 947 | name: JestTestName, 948 | fn?: (done: () => void) => ?Promise, 949 | timeout?: number 950 | ): void, 951 | }; 952 | declare function fit( 953 | name: JestTestName, 954 | fn: (done: () => void) => ?Promise, 955 | timeout?: number 956 | ): void; 957 | /** An individual test unit */ 958 | declare var test: typeof it; 959 | /** A disabled group of tests */ 960 | declare var xdescribe: typeof describe; 961 | /** A focused group of tests */ 962 | declare var fdescribe: typeof describe; 963 | /** A disabled individual test */ 964 | declare var xit: typeof it; 965 | /** A disabled individual test */ 966 | declare var xtest: typeof it; 967 | 968 | type JestPrettyFormatColors = { 969 | comment: { close: string, open: string }, 970 | content: { close: string, open: string }, 971 | prop: { close: string, open: string }, 972 | tag: { close: string, open: string }, 973 | value: { close: string, open: string }, 974 | }; 975 | 976 | type JestPrettyFormatIndent = string => string; 977 | type JestPrettyFormatRefs = Array; 978 | type JestPrettyFormatPrint = any => string; 979 | type JestPrettyFormatStringOrNull = string | null; 980 | 981 | type JestPrettyFormatOptions = {| 982 | callToJSON: boolean, 983 | edgeSpacing: string, 984 | escapeRegex: boolean, 985 | highlight: boolean, 986 | indent: number, 987 | maxDepth: number, 988 | min: boolean, 989 | plugins: JestPrettyFormatPlugins, 990 | printFunctionName: boolean, 991 | spacing: string, 992 | theme: {| 993 | comment: string, 994 | content: string, 995 | prop: string, 996 | tag: string, 997 | value: string, 998 | |}, 999 | |}; 1000 | 1001 | type JestPrettyFormatPlugin = { 1002 | print: ( 1003 | val: any, 1004 | serialize: JestPrettyFormatPrint, 1005 | indent: JestPrettyFormatIndent, 1006 | opts: JestPrettyFormatOptions, 1007 | colors: JestPrettyFormatColors 1008 | ) => string, 1009 | test: any => boolean, 1010 | }; 1011 | 1012 | type JestPrettyFormatPlugins = Array; 1013 | 1014 | /** The expect function is used every time you want to test a value */ 1015 | declare var expect: { 1016 | /** The object that you want to make assertions against */ 1017 | ( 1018 | value: any 1019 | ): JestExpectType & 1020 | JestPromiseType & 1021 | EnzymeMatchersType & 1022 | DomTestingLibraryType & 1023 | JestJQueryMatchersType & 1024 | JestStyledComponentsMatchersType & 1025 | JestExtendedMatchersType, 1026 | 1027 | /** Add additional Jasmine matchers to Jest's roster */ 1028 | extend(matchers: { [name: string]: JestMatcher }): void, 1029 | /** Add a module that formats application-specific data structures. */ 1030 | addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, 1031 | assertions(expectedAssertions: number): void, 1032 | hasAssertions(): void, 1033 | any(value: mixed): JestAsymmetricEqualityType, 1034 | anything(): any, 1035 | arrayContaining(value: Array): Array, 1036 | objectContaining(value: Object): Object, 1037 | /** Matches any received string that contains the exact expected string. */ 1038 | stringContaining(value: string): string, 1039 | stringMatching(value: string | RegExp): string, 1040 | not: { 1041 | arrayContaining: (value: $ReadOnlyArray) => Array, 1042 | objectContaining: (value: {}) => Object, 1043 | stringContaining: (value: string) => string, 1044 | stringMatching: (value: string | RegExp) => string, 1045 | }, 1046 | }; 1047 | 1048 | // TODO handle return type 1049 | // http://jasmine.github.io/2.4/introduction.html#section-Spies 1050 | declare function spyOn(value: mixed, method: string): Object; 1051 | 1052 | /** Holds all functions related to manipulating test runner */ 1053 | declare var jest: JestObjectType; 1054 | 1055 | /** 1056 | * The global Jasmine object, this is generally not exposed as the public API, 1057 | * using features inside here could break in later versions of Jest. 1058 | */ 1059 | declare var jasmine: { 1060 | DEFAULT_TIMEOUT_INTERVAL: number, 1061 | any(value: mixed): JestAsymmetricEqualityType, 1062 | anything(): any, 1063 | arrayContaining(value: Array): Array, 1064 | clock(): JestClockType, 1065 | createSpy(name: string): JestSpyType, 1066 | createSpyObj(baseName: string, methodNames: Array): { [methodName: string]: JestSpyType }, 1067 | objectContaining(value: Object): Object, 1068 | stringMatching(value: string): string, 1069 | }; 1070 | --------------------------------------------------------------------------------