├── .eslintignore ├── .npmrc ├── CNAME ├── examples ├── read-resource │ ├── .npmrc │ ├── .env │ ├── .gitignore │ ├── src │ │ ├── index.js │ │ ├── action-creators │ │ │ └── read-book.js │ │ └── components │ │ │ └── Book.js │ ├── public │ │ └── index.html │ ├── package.json │ └── README.md └── lists-and-named-requests │ ├── .npmrc │ ├── .env │ ├── .gitignore │ ├── public │ └── index.html │ ├── package.json │ ├── src │ ├── index.js │ ├── action-creators │ │ ├── get-latest-books.js │ │ └── get-user-books.js │ └── components │ │ └── BooksList.js │ └── README.md ├── packages ├── redux-resource │ ├── .npmrc │ ├── src │ │ ├── utils │ │ │ ├── compose-reducers.js │ │ │ ├── initial-resource-meta-state.js │ │ │ ├── generate-default-initial-state.js │ │ │ ├── warning.js │ │ │ ├── upsert-meta.js │ │ │ ├── request-statuses.js │ │ │ ├── set-resource-meta.js │ │ │ ├── get-path-parts.js │ │ │ ├── upsert-resources.js │ │ │ ├── get-resources.js │ │ │ └── reducer-generator.js │ │ ├── action-types │ │ │ ├── action-types.js │ │ │ ├── deprecated.js │ │ │ └── index.js │ │ ├── request-statuses-plugin │ │ │ ├── read.js │ │ │ ├── update.js │ │ │ ├── create.js │ │ │ ├── action-reducers-map.js │ │ │ └── index.js │ │ └── index.js │ ├── test │ │ ├── .eslintrc │ │ └── unit │ │ │ ├── utils │ │ │ └── request-statuses.js │ │ │ ├── reducers │ │ │ ├── update.js │ │ │ ├── create.js │ │ │ ├── index.js │ │ │ └── plugins.js │ │ │ └── action-types.js │ ├── .babelrc │ ├── rollup.config.js │ ├── README.md │ ├── LICENSE │ ├── package.json │ └── docs │ │ └── migration-guides │ │ └── 1-to-2.md ├── redux-resource-xhr │ ├── .npmrc │ ├── .eslintrc │ ├── test │ │ └── .eslintrc │ ├── README.md │ ├── .babelrc │ ├── docs │ │ └── migration-guides │ │ │ └── 3-to-4.md │ ├── rollup.config.js │ ├── LICENSE │ ├── package.json │ └── src │ │ └── xhr.js ├── redux-resource-plugins │ ├── .npmrc │ ├── .eslintrc │ ├── test │ │ ├── .eslintrc │ │ └── unit │ │ │ └── selection.js │ ├── src │ │ ├── index.js │ │ ├── reset.js │ │ ├── included-resources.js │ │ ├── selection.js │ │ └── http-status-codes.js │ ├── README.md │ ├── .babelrc │ ├── docs │ │ └── migration-guides │ │ │ └── 2-to-3.md │ ├── rollup.config.js │ ├── LICENSE │ └── package.json ├── redux-resource-prop-types │ ├── .npmrc │ ├── test │ │ └── .eslintrc │ ├── README.md │ ├── docs │ │ ├── migration-guides │ │ │ ├── 3-to-4.md │ │ │ └── 2-to-3.md │ │ └── old-versions │ │ │ ├── 1.md │ │ │ └── 2.md │ ├── .babelrc │ ├── rollup.config.js │ ├── LICENSE │ ├── src │ │ └── index.js │ └── package.json └── redux-resource-action-creators │ ├── .npmrc │ ├── test │ └── .eslintrc │ ├── README.md │ ├── src │ ├── utils │ │ └── warning.js │ └── index.js │ ├── docs │ └── migration-guides │ │ └── 1-to-2.md │ ├── .babelrc │ ├── rollup.config.js │ ├── LICENSE │ └── package.json ├── .gitbook.yml ├── logo ├── logomark.png ├── combination-mark.png ├── combination-mark-white.png └── README.md ├── .prettierrc ├── lerna.json ├── .prettierignore ├── docs ├── faq │ ├── README.md │ ├── state-tree.md │ ├── actions.md │ └── lists.md ├── introduction │ ├── README.md │ ├── motivation.md │ ├── examples.md │ └── similar-projects.md ├── resources │ ├── README.md │ ├── resource-reducers.md │ ├── meta.md │ └── lists.md ├── other-guides │ ├── README.md │ ├── migration-guides.md │ └── usage-with-react.md ├── extras │ ├── README.md │ ├── reset-plugin.md │ ├── http-status-codes-plugin.md │ ├── redux-resource-plugins.md │ └── included-resources-plugin.md ├── requests │ ├── README.md │ ├── request-objects.md │ ├── request-names.md │ └── request-statuses.md ├── recipes │ ├── README.md │ ├── forms.md │ └── caching.md ├── api-reference │ ├── README.md │ ├── request-statuses.md │ ├── upsert-resources.md │ ├── set-resource-meta.md │ ├── resource-reducer.md │ └── get-resources.md └── README.md ├── test ├── setup │ ├── node.js │ ├── .globals.json │ ├── browser.js │ └── setup.js ├── .eslintrc └── runner.html ├── .eslintrc ├── .editorconfig ├── .travis.yml ├── .gitignore ├── LICENSE ├── ROADMAP.md ├── package.json └── CODE_OF_CONDUCT.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | redux-resource.js.org 2 | -------------------------------------------------------------------------------- /examples/read-resource/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/redux-resource/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/read-resource/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /packages/redux-resource-xhr/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/lists-and-named-requests/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/redux-resource-plugins/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/lists-and-named-requests/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.gitbook.yml: -------------------------------------------------------------------------------- 1 | structure: 2 | readme: INDEX.md 3 | summary: docs/README.md -------------------------------------------------------------------------------- /logo/logomark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/redux-resource/HEAD/logo/logomark.png -------------------------------------------------------------------------------- /packages/redux-resource-xhr/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Promise": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxBracketSameLine": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /logo/combination-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/redux-resource/HEAD/logo/combination-mark.png -------------------------------------------------------------------------------- /packages/redux-resource-plugins/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-confusing-arrow": "off" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "independent" 7 | } 8 | -------------------------------------------------------------------------------- /logo/combination-mark-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/redux-resource/HEAD/logo/combination-mark-white.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | _book 3 | dist 4 | es 5 | lib 6 | docs 7 | coverage 8 | .vscode 9 | .nyc_output 10 | *.md 11 | -------------------------------------------------------------------------------- /docs/faq/README.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | * [General](general.md) 4 | * [State Tree](state-tree.md) 5 | * [Actions](actions.md) 6 | * [Lists](lists.md) 7 | -------------------------------------------------------------------------------- /docs/introduction/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | * [Motivation](motivation.md) 4 | * [Core Concepts](core-concepts.md) 5 | * [Similar Projects](similar-projects.md) 6 | * [Examples](examples.md) 7 | -------------------------------------------------------------------------------- /test/setup/node.js: -------------------------------------------------------------------------------- 1 | global.chai = require('chai'); 2 | global.sinon = require('sinon'); 3 | global.chai.use(require('sinon-chai')); 4 | 5 | require('babel-core/register'); 6 | require('./setup')(); 7 | -------------------------------------------------------------------------------- /docs/resources/README.md: -------------------------------------------------------------------------------- 1 | * [Resource Reducers](resource-reducers.md) 2 | * [Resource Objects](resource-objects.md) 3 | * [Meta](meta.md) 4 | * [Lists](lists.md) 5 | * [Modifying Resources](modifying-resources.md) 6 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/compose-reducers.js: -------------------------------------------------------------------------------- 1 | export default function composeReducers(reducers) { 2 | return (state, action) => 3 | reducers.reduceRight( 4 | (prevState, reducer) => reducer(prevState, action), 5 | state 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /docs/other-guides/README.md: -------------------------------------------------------------------------------- 1 | * [Usage With React](usage-with-react.md) 2 | * [Tracking Request Statuses](tracking-request-statuses.md) 3 | * [Using Request Statuses](using-request-statuses.md) 4 | * [Custom Action Types](custom-action-types.md) 5 | * [Migration Guides](migration-guides.md) 6 | -------------------------------------------------------------------------------- /packages/redux-resource-xhr/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "stub": true, 7 | "expect": true 8 | }, 9 | "rules": { 10 | "prefer-arrow-callback": "off", 11 | "max-nested-callbacks": "off" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/setup/.globals.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "expect": true, 4 | "mock": true, 5 | "sandbox": true, 6 | "spy": true, 7 | "stub": true, 8 | "useFakeServer": true, 9 | "useFakeTimers": true, 10 | "useFakeXMLHttpRequest": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "stub": true, 7 | "expect": true 8 | }, 9 | "rules": { 10 | "prefer-arrow-callback": "off", 11 | "max-nested-callbacks": "off" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "stub": true, 7 | "expect": true 8 | }, 9 | "rules": { 10 | "prefer-arrow-callback": "off", 11 | "max-nested-callbacks": "off" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/read-resource/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | 13 | package-lock.json 14 | package-lock.json.* 15 | yarn.lock -------------------------------------------------------------------------------- /packages/redux-resource-plugins/src/index.js: -------------------------------------------------------------------------------- 1 | import httpStatusCodes from './http-status-codes'; 2 | import includedResources from './included-resources'; 3 | import selection from './selection'; 4 | import reset from './reset'; 5 | 6 | export { httpStatusCodes, selection, reset, includedResources }; 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 11, 5 | "sourceType": "module" 6 | }, 7 | "rules": { 8 | "no-console": "off" 9 | }, 10 | "env": { 11 | "es6": true, 12 | "browser": true, 13 | "node": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/lists-and-named-requests/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # production 7 | build 8 | 9 | # misc 10 | .DS_Store 11 | npm-debug.log 12 | 13 | package-lock.json 14 | package-lock.json.* 15 | yarn.lock -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/initial-resource-meta-state.js: -------------------------------------------------------------------------------- 1 | import requestStatuses from './request-statuses'; 2 | 3 | export default { 4 | createStatus: requestStatuses.IDLE, 5 | readStatus: requestStatuses.IDLE, 6 | updateStatus: requestStatuses.IDLE, 7 | deleteStatus: requestStatuses.IDLE 8 | }; 9 | -------------------------------------------------------------------------------- /test/setup/browser.js: -------------------------------------------------------------------------------- 1 | var mochaGlobals = require('./.globals.json').globals; 2 | 3 | window.mocha.setup('bdd'); 4 | window.onload = function() { 5 | window.mocha.checkLeaks(); 6 | window.mocha.globals(Object.keys(mochaGlobals)); 7 | window.mocha.run(); 8 | require('./setup')(window); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/redux-resource/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "stub": true, 7 | "expect": true 8 | }, 9 | "rules": { 10 | "prefer-arrow-callback": "off", 11 | "max-nested-callbacks": "off", 12 | "max-len": ["error", 200] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "stub": true, 7 | "expect": true 8 | }, 9 | "rules": { 10 | "prefer-arrow-callback": "off", 11 | "max-nested-callbacks": "off", 12 | "no-unused-expressions": "off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/extras/README.md: -------------------------------------------------------------------------------- 1 | # Ecosystem Extras 2 | 3 | The Redux Resource ecosystem provides bits of code that make working with the library even better. 4 | 5 | * [Redux Resource Action Creators](redux-resource-action-creators.md) 6 | * [Redux Resource XHR](redux-resource-xhr.md) 7 | * [Redux Resource Plugins](redux-resource-plugins.md) 8 | * [Redux Resource Prop Types](redux-resource-prop-types.md) 9 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/README.md: -------------------------------------------------------------------------------- 1 | # Redux Resource Plugins 2 | 3 | A collection of common plugins for Redux Resource. 4 | 5 | ### Installation 6 | 7 | To install the latest version: 8 | 9 | ``` 10 | npm install --save redux-resource-plugins 11 | ``` 12 | 13 | ### Documentation 14 | 15 | View the documentation at 16 | **[redux-resource.js.org ⇗](https://redux-resource.js.org/)**. 17 | -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/README.md: -------------------------------------------------------------------------------- 1 | # Redux Resource Prop Types 2 | 3 | Convenient Prop Types for users of Redux Resource. 4 | 5 | ### Installation 6 | 7 | To install the latest version: 8 | 9 | ``` 10 | npm install --save redux-resource-prop-types 11 | ``` 12 | 13 | ### Documentation 14 | 15 | View the documentation at 16 | **[redux-resource.js.org ⇗](https://redux-resource.js.org/)**. 17 | -------------------------------------------------------------------------------- /packages/redux-resource-xhr/README.md: -------------------------------------------------------------------------------- 1 | # Redux Resource XHR 2 | 3 | Redux Resource XHR is an action creator that simplifies CRUD operations. 4 | 5 | ### Installation 6 | 7 | To install the latest version: 8 | 9 | ``` 10 | npm install --save redux-resource-xhr 11 | ``` 12 | 13 | ### Documentation 14 | 15 | View the documentation at 16 | **[redux-resource.js.org ⇗](https://redux-resource.js.org/)**. 17 | -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/README.md: -------------------------------------------------------------------------------- 1 | # Redux Resource Action Creators 2 | 3 | Action creators for creating Redux Resource actions. 4 | 5 | ### Installation 6 | 7 | To install the latest version: 8 | 9 | ``` 10 | npm install --save redux-resource-action-creators 11 | ``` 12 | 13 | ### Documentation 14 | 15 | View the documentation at 16 | **[redux-resource.js.org ⇗](https://redux-resource.js.org/)**. 17 | -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/docs/migration-guides/3-to-4.md: -------------------------------------------------------------------------------- 1 | # Migrating 2 | 3 | This guide covers a migration from `redux-resource-prop-types@3.x` to `redux-resource-prop-types@4.0.0`. 4 | 5 | ### Upgrade to Redux Resource v3.0.0 6 | 7 | First, you must upgrade to `redux-resource@3.0.0`. 8 | 9 | ### That's all! 10 | 11 | That's all there is to it. v4 of this library works seamlessly with Redux Resource v3. 12 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../node_modules/pleaserc/.eslintrc-test", 4 | "./setup/.globals.json" 5 | ], 6 | "parserOptions": { 7 | "ecmaVersion": 6, 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "strict": 0, 12 | "quotes": [2, "single"], 13 | "no-unused-expressions": 0 14 | }, 15 | "env": { 16 | "browser": true, 17 | "node": true, 18 | "mocha": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true; 4 | 5 | [*] 6 | # Ensure there's no lingering whitespace 7 | trim_trailing_whitespace = true 8 | # Ensure a newline at the end of each file 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | # Unix-style newlines 13 | end_of_line = lf 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /docs/requests/README.md: -------------------------------------------------------------------------------- 1 | * [Request Objects](request-objects.md) 2 | * [Keys](request-keys.md) 3 | * [Names](request-names.md) 4 | * [Statuses](request-statuses.md) 5 | * [Request Actions](request-actions.md) 6 | * [Updating Lists](updating-lists.md) 7 | * [Reading Resources](reading-resources.md) 8 | * [Updating Resources](updating-resources.md) 9 | * [Creating Resources](creating-resources.md) 10 | * [Deleting Resources](deleting-resources.md) 11 | -------------------------------------------------------------------------------- /packages/redux-resource/src/action-types/action-types.js: -------------------------------------------------------------------------------- 1 | export default { 2 | UPDATE_RESOURCES: 'UPDATE_RESOURCES', 3 | DELETE_RESOURCES: 'DELETE_RESOURCES', 4 | 5 | // These will be used in Redux Resource v3.1.0. For now, 6 | // they are reserved action types. 7 | // REQUEST_IDLE: 'REQUEST_IDLE', 8 | // REQUEST_PENDING: 'REQUEST_PENDING', 9 | // REQUEST_FAILED: 'REQUEST_FAILED', 10 | // REQUEST_SUCCEEDED: 'REQUEST_SUCCEEDED', 11 | }; 12 | -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/src/utils/warning.js: -------------------------------------------------------------------------------- 1 | export default function warning(message) { 2 | if (typeof console !== 'undefined' && typeof console.error === 'function') { 3 | console.error(message); 4 | } 5 | 6 | try { 7 | // This error was thrown as a convenience so that if you enable 8 | // "break on all exceptions" in your console, 9 | // it would pause the execution at this line. 10 | throw new Error(message); 11 | } catch (e) { 12 | // Intentionally blank 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | sudo: false 5 | install: 6 | # Install the deps in the root project 7 | - npm i 8 | # Install the nested deps, but the linked repositories won't work at this 9 | # stage, because the deps aren't build 10 | - npm run bootstrap 11 | # Build the deps 12 | - npm run build 13 | notifications: 14 | email: false 15 | after_success: 16 | # Upload to coveralls, but don't _fail_ if coveralls is down. 17 | - cat coverage/lcov.info | node_modules/.bin/coveralls || echo "Coveralls upload failed" 18 | -------------------------------------------------------------------------------- /docs/recipes/README.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | These are patterns and snippets of code that will help you when using 4 | Redux Resource in an application. They assume that you understand the 5 | topics in the [resources](../resources/README.md) and 6 | [requests](../requests/README.md) sections of the documentation. 7 | 8 | * [Forms](forms.md) 9 | * [Canceling Requests](canceling-requests.md) 10 | * [Unauthorized Responses](unauthorized-responses.md) 11 | * [User Feedback](user-feedback.md) 12 | * [Related Resources](related-resources.md) 13 | * [Caching](caching.md) 14 | -------------------------------------------------------------------------------- /packages/redux-resource/test/unit/utils/request-statuses.js: -------------------------------------------------------------------------------- 1 | import { requestStatuses } from '../../../src'; 2 | 3 | describe('requestStatuses', function() { 4 | it('should be an object', () => { 5 | expect(requestStatuses).to.be.an('object'); 6 | }); 7 | 8 | it('should have the right statuses', () => { 9 | expect(requestStatuses.PENDING).to.equal('PENDING'); 10 | expect(requestStatuses.SUCCEEDED).to.equal('SUCCEEDED'); 11 | expect(requestStatuses.FAILED).to.equal('FAILED'); 12 | expect(requestStatuses.IDLE).to.equal('IDLE'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/docs/migration-guides/1-to-2.md: -------------------------------------------------------------------------------- 1 | # Migrating 2 | 3 | This guide covers a migration from `redux-resource-action-creators@1.x` to `redux-resource-action-creators@2.0.0`. 4 | 5 | ### Upgrade to Redux Resource v3.0.0 6 | 7 | You must upgrade to `redux-resource@3.0.0` to use `redux-resource-action-creators@2.0.0`. 8 | 9 | ### Replace calls to `.null()` with `.idle()` 10 | 11 | In Redux Resource v3, the `NULL` request status has been renamed to `IDLE`, and this method has 12 | been updated to reflect that change. 13 | 14 | ### That's all! 15 | 16 | That should do it. 17 | -------------------------------------------------------------------------------- /packages/redux-resource/src/request-statuses-plugin/read.js: -------------------------------------------------------------------------------- 1 | import reducerGenerator from '../utils/reducer-generator'; 2 | import cruReducerHelper from '../utils/cru-reducer-helper'; 3 | import requestStatuses from '../utils/request-statuses'; 4 | 5 | const read = reducerGenerator('read', requestStatuses.PENDING); 6 | const readFail = reducerGenerator('read', requestStatuses.FAILED); 7 | const readIdle = reducerGenerator('read', requestStatuses.IDLE); 8 | 9 | function readSucceed(state, action, options) { 10 | return cruReducerHelper(state, action, options, { 11 | readStatus: requestStatuses.SUCCEEDED, 12 | }); 13 | } 14 | 15 | export { read, readFail, readIdle, readSucceed }; 16 | -------------------------------------------------------------------------------- /packages/redux-resource/src/request-statuses-plugin/update.js: -------------------------------------------------------------------------------- 1 | import reducerGenerator from '../utils/reducer-generator'; 2 | import cruReducerHelper from '../utils/cru-reducer-helper'; 3 | import requestStatuses from '../utils/request-statuses'; 4 | 5 | const update = reducerGenerator('update', requestStatuses.PENDING); 6 | const updateFail = reducerGenerator('update', requestStatuses.FAILED); 7 | const updateIdle = reducerGenerator('update', requestStatuses.IDLE); 8 | 9 | function updateSucceed(state, action, options) { 10 | return cruReducerHelper(state, action, options, { 11 | updateStatus: requestStatuses.SUCCEEDED, 12 | }); 13 | } 14 | 15 | export { update, updateFail, updateIdle, updateSucceed }; 16 | -------------------------------------------------------------------------------- /docs/api-reference/README.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ### Top-Level Exports 4 | 5 | * [resourceReducer](resource-reducer.md) 6 | * [getStatus](get-status.md) 7 | * [getResources](get-resources.md) 8 | * [upsertResources](upsert-resources.md) 9 | * [setResourceMeta](set-resource-meta.md) 10 | * [actionTypes](action-types.md) 11 | * [requestStatuses](request-statuses.md) 12 | 13 | ### Importing 14 | 15 | Every function or object described above is a top-level export. You can import 16 | any of them like this: 17 | 18 | #### ES6 19 | 20 | ```js 21 | import { resourceReducer } from 'redux-resource'; 22 | ``` 23 | 24 | #### ES5 (CommonJS) 25 | 26 | ```js 27 | var resourceReducer = require('redux-resource').resourceReducer; 28 | ``` 29 | -------------------------------------------------------------------------------- /packages/redux-resource/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "build": { 4 | "presets": [ 5 | ["env", {"modules": false}], 6 | "stage-3" 7 | ], 8 | "plugins": ["external-helpers"] 9 | }, 10 | "es": { 11 | "presets": [ 12 | ["env", {"modules": false}], 13 | "stage-3" 14 | ] 15 | }, 16 | "commonjs": { 17 | "plugins": [ 18 | ["transform-es2015-modules-commonjs", {"loose": true}] 19 | ], 20 | "presets": [ 21 | "stage-3" 22 | ] 23 | }, 24 | "test": { 25 | "presets": [ 26 | "env", 27 | "stage-3" 28 | ], 29 | "plugins": [ 30 | "istanbul" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "build": { 4 | "presets": [ 5 | ["env", {"modules": false}], 6 | "stage-3" 7 | ], 8 | "plugins": ["external-helpers"] 9 | }, 10 | "es": { 11 | "presets": [ 12 | ["env", {"modules": false}], 13 | "stage-3" 14 | ] 15 | }, 16 | "commonjs": { 17 | "plugins": [ 18 | ["transform-es2015-modules-commonjs", {"loose": true}] 19 | ], 20 | "presets": [ 21 | "stage-3" 22 | ] 23 | }, 24 | "test": { 25 | "presets": [ 26 | "env", 27 | "stage-3" 28 | ], 29 | "plugins": [ 30 | "istanbul" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "build": { 4 | "presets": [ 5 | ["env", {"modules": false}], 6 | "stage-3" 7 | ], 8 | "plugins": ["external-helpers"] 9 | }, 10 | "es": { 11 | "presets": [ 12 | ["env", {"modules": false}], 13 | "stage-3" 14 | ] 15 | }, 16 | "commonjs": { 17 | "plugins": [ 18 | ["transform-es2015-modules-commonjs", {"loose": true}] 19 | ], 20 | "presets": [ 21 | "stage-3" 22 | ] 23 | }, 24 | "test": { 25 | "presets": [ 26 | "env", 27 | "stage-3" 28 | ], 29 | "plugins": [ 30 | "istanbul" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "build": { 4 | "presets": [ 5 | ["env", {"modules": false}], 6 | "stage-3" 7 | ], 8 | "plugins": ["external-helpers"] 9 | }, 10 | "es": { 11 | "presets": [ 12 | ["env", {"modules": false}], 13 | "stage-3" 14 | ] 15 | }, 16 | "commonjs": { 17 | "plugins": [ 18 | ["transform-es2015-modules-commonjs", {"loose": true}] 19 | ], 20 | "presets": [ 21 | "stage-3" 22 | ] 23 | }, 24 | "test": { 25 | "presets": [ 26 | "env", 27 | "stage-3" 28 | ], 29 | "plugins": [ 30 | "istanbul" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/read-resource/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import thunk from 'redux-thunk'; 5 | import { resourceReducer } from 'redux-resource'; 6 | import Book from './components/Book'; 7 | import readBook from './action-creators/read-book'; 8 | 9 | const store = createStore(resourceReducer('books'), applyMiddleware(thunk)); 10 | const rootEl = document.getElementById('root'); 11 | 12 | const render = () => 13 | ReactDOM.render( 14 | store.dispatch(readBook(24))} 18 | />, 19 | rootEl 20 | ); 21 | 22 | render(); 23 | store.subscribe(render); 24 | -------------------------------------------------------------------------------- /examples/read-resource/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Resource Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/redux-resource-xhr/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "build": { 4 | "presets": [ 5 | ["env", {"modules": false}], 6 | "stage-3" 7 | ], 8 | "plugins": ["external-helpers"] 9 | }, 10 | "es": { 11 | "presets": [ 12 | ["env", {"modules": false}], 13 | "stage-3" 14 | ] 15 | }, 16 | "commonjs": { 17 | "plugins": [ 18 | ["transform-es2015-modules-commonjs", {"loose": true}] 19 | ], 20 | "presets": [ 21 | "stage-3" 22 | ] 23 | }, 24 | "test": { 25 | "presets": [ 26 | "env", 27 | "stage-3" 28 | ], 29 | "plugins": [ 30 | "istanbul", 31 | "rewire" 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/redux-resource/src/request-statuses-plugin/create.js: -------------------------------------------------------------------------------- 1 | import reducerGenerator from '../utils/reducer-generator'; 2 | import cruReducerHelper from '../utils/cru-reducer-helper'; 3 | import requestStatuses from '../utils/request-statuses'; 4 | 5 | const create = reducerGenerator('create', requestStatuses.PENDING); 6 | const createFail = reducerGenerator('create', requestStatuses.FAILED); 7 | const createIdle = reducerGenerator('create', requestStatuses.IDLE); 8 | 9 | function createSucceed(state, action, options) { 10 | return cruReducerHelper(state, action, options, { 11 | readStatus: requestStatuses.SUCCEEDED, 12 | createStatus: requestStatuses.SUCCEEDED, 13 | }); 14 | } 15 | 16 | export { create, createFail, createIdle, createSucceed }; 17 | -------------------------------------------------------------------------------- /examples/lists-and-named-requests/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Redux Resource Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/generate-default-initial-state.js: -------------------------------------------------------------------------------- 1 | export default function generateDefaultInitialState() { 2 | return { 3 | // These is a complete collection of all of the resources that the server has sent 4 | // back, for all requests. The keys of this Object are the resource's ID. There's 5 | // no ordering here: use `lists` for that. 6 | resources: {}, 7 | // This is metadata about _specific_ resources. For instance, if a DELETE 8 | // is in flight for a book with ID 24, then you could find that here. 9 | meta: {}, 10 | // Named requests are used to track the statuses of requests that aren't 11 | // associated with a resource ID 12 | requests: {}, 13 | // Lists are ordered collections of resources 14 | lists: {} 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /examples/read-resource/src/action-creators/read-book.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from 'redux-resource'; 2 | 3 | export default function readBook(id) { 4 | return function(dispatch) { 5 | dispatch({ 6 | type: actionTypes.READ_RESOURCES_PENDING, 7 | resourceType: 'books', 8 | resources: [id], 9 | }); 10 | 11 | // This timeout is to mimic network latency. 12 | setTimeout(function() { 13 | dispatch({ 14 | type: actionTypes.READ_RESOURCES_SUCCEEDED, 15 | resourceType: 'books', 16 | resources: [ 17 | { 18 | id, 19 | title: 'The Brilliance of the Moon', 20 | author: 'Lian Hearn', 21 | releaseYear: 2004, 22 | }, 23 | ], 24 | }); 25 | }, 2000); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/warning.js: -------------------------------------------------------------------------------- 1 | let codeCache = {}; 2 | 3 | export default function warning(message, code) { 4 | // This ensures that each warning type is only logged out one time 5 | if (code) { 6 | if (codeCache[code]) { 7 | return; 8 | } 9 | 10 | codeCache[code] = true; 11 | } 12 | 13 | if (typeof console !== 'undefined' && typeof console.error === 'function') { 14 | console.error(message); 15 | } 16 | 17 | try { 18 | // This error was thrown as a convenience so that if you enable 19 | // "break on all exceptions" in your console, 20 | // it would pause the execution at this line. 21 | throw new Error(message); 22 | } catch (e) { 23 | // Intentionally blank 24 | } 25 | } 26 | 27 | export function resetCodeCache() { 28 | codeCache = {}; 29 | } 30 | -------------------------------------------------------------------------------- /packages/redux-resource/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import uglify from 'rollup-plugin-uglify'; 4 | import replace from 'rollup-plugin-replace'; 5 | 6 | var env = process.env.NODE_ENV; 7 | var config = { 8 | format: 'umd', 9 | moduleName: 'ReduxResource', 10 | plugins: [ 11 | nodeResolve({ 12 | jsnext: true 13 | }), 14 | babel({ 15 | exclude: 'node_modules/**' 16 | }), 17 | replace({ 18 | 'process.env.NODE_ENV': JSON.stringify(env) 19 | }) 20 | ] 21 | }; 22 | 23 | if (env === 'production') { 24 | config.plugins.push( 25 | uglify({ 26 | compress: { 27 | pure_getters: true, 28 | unsafe: true, 29 | unsafe_comps: true, 30 | } 31 | }) 32 | ); 33 | } 34 | 35 | export default config; 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | bower_components 27 | coverage 28 | tmp 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | .vscode 33 | 34 | .DS_Store 35 | dist 36 | es 37 | lib 38 | _site 39 | _book 40 | .nyc_output 41 | package-lock.json 42 | -------------------------------------------------------------------------------- /logo/README.md: -------------------------------------------------------------------------------- 1 | # Logo 2 | 3 | This is the official logo of Redux Resource. 4 | 5 | ### Logomark 6 | 7 | Redux Resource Logo 8 | 9 | ### Combination mark (black text) 10 | 11 | Redux Resource Logo 12 | 13 | ### Combination mark (white text) 14 | 15 | Redux Resource Logo 16 | 17 | This is the same as the black combination mark above, except the text is white (even though you can't see it). 18 | 19 | ## License 20 | 21 | The Redux Resource logo is licensed under CC0, waiving all copyright. 22 | [Read the license.](../LICENSE-logo.md) 23 | -------------------------------------------------------------------------------- /examples/read-resource/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "read-resource", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^3.2.0" 7 | }, 8 | "dependencies": { 9 | "prop-types": "^15.5.10", 10 | "react": "^16.0.0", 11 | "react-dom": "^16.0.0", 12 | "react-redux": "^7.1.1", 13 | "redux": "^4.0.4", 14 | "redux-thunk": "^2.2.0", 15 | "redux-resource": "^3.1.0" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "eject": "react-scripts eject", 21 | "test": "react-scripts test" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/setup/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = function(root) { 2 | root = root ? root : global; 3 | root.expect = root.chai.expect; 4 | 5 | beforeEach(() => { 6 | // Using these globally-available Sinon features is preferrable, as they're 7 | // automatically restored for you in the subsequent `afterEach` 8 | root.sandbox = root.sinon.sandbox.create(); 9 | root.stub = root.sandbox.stub.bind(root.sandbox); 10 | root.spy = root.sandbox.spy.bind(root.sandbox); 11 | root.mock = root.sandbox.mock.bind(root.sandbox); 12 | root.useFakeTimers = root.sandbox.useFakeTimers.bind(root.sandbox); 13 | root.useFakeXMLHttpRequest = root.sandbox.useFakeXMLHttpRequest.bind( 14 | root.sandbox 15 | ); 16 | root.useFakeServer = root.sandbox.useFakeServer.bind(root.sandbox); 17 | }); 18 | 19 | afterEach(() => { 20 | delete root.stub; 21 | delete root.spy; 22 | root.sandbox.restore(); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /examples/lists-and-named-requests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lists-and-named-requests", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "^3.2.0" 7 | }, 8 | "dependencies": { 9 | "prop-types": "^15.5.10", 10 | "react": "^16.0.0", 11 | "react-dom": "^16.0.0", 12 | "react-redux": "^7.1.1", 13 | "redux": "^4.0.4", 14 | "redux-resource": "^3.1.0", 15 | "redux-thunk": "^2.2.0" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "eject": "react-scripts eject", 21 | "test": "react-scripts test" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/docs/migration-guides/2-to-3.md: -------------------------------------------------------------------------------- 1 | # Migrating 2 | 3 | This guide covers a migration from `redux-resource-plugins@2.x` to `redux-resource-plugins@3.0.0`. 4 | 5 | ### Upgrade to Redux Resource v3.0.0 6 | 7 | You must upgrade to `redux-resource@3.0.0` to use `redux-resource-plugins@3.0.0`. 8 | 9 | ### HTTP Status Codes Plugin 10 | 11 | Actions without status codes will now set a status code of `null` rather than `0`. 12 | 13 | > Note: it is unlikely that your application was relying on this behavior. If you're 14 | > not sure, then you are most likely fine. 15 | 16 | ### Selection Plugin 17 | 18 | There are no breaking changes to the selection plugin, but you may be able to use 19 | the built-in `UPDATE_RESOURCES` action type instead to update lists directly. 20 | 21 | Read more about this action type in the 22 | [Modifying Resources guide](https://redux-resource.js.org/docs/resources/modifying-resources.html). 23 | -------------------------------------------------------------------------------- /docs/api-reference/request-statuses.md: -------------------------------------------------------------------------------- 1 | # requestStatuses 2 | 3 | An object that represents the four statuses that a request can have. The 4 | complete object is shown below: 5 | 6 | ```js 7 | { 8 | IDLE: 'IDLE', 9 | PENDING: 'PENDING', 10 | FAILED: 'FAILED', 11 | SUCCEEDED: 'SUCCEEDED', 12 | } 13 | ``` 14 | 15 | #### Example 16 | 17 | ```js 18 | import { requestStatuses } from 'redux-resource'; 19 | import store from './store'; 20 | 21 | const state = store.getState(); 22 | 23 | if (state.books.meta['23'].readStatus === requestStatuses.PENDING) { 24 | console.log('A book with id "23" is currently being fetched.'); 25 | } 26 | ``` 27 | 28 | #### Tips 29 | 30 | - Although this object _can_ be used to check the status of a request in your 31 | view layer, it's often more convenient to use [`getStatus`](get-status.md) 32 | for that purpose. For this reason, we recommend restricting your usage of this 33 | object to plugins, reducers, or action creators. 34 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import uglify from 'rollup-plugin-uglify'; 4 | import replace from 'rollup-plugin-replace'; 5 | 6 | var env = process.env.NODE_ENV; 7 | var config = { 8 | format: 'umd', 9 | moduleName: 'ReduxResourcePlugins', 10 | external: 'redux-resource', 11 | globals: { 12 | 'redux-resource': 'ReduxResource' 13 | }, 14 | plugins: [ 15 | nodeResolve({ 16 | jsnext: true 17 | }), 18 | babel({ 19 | exclude: 'node_modules/**' 20 | }), 21 | replace({ 22 | 'process.env.NODE_ENV': JSON.stringify(env) 23 | }) 24 | ] 25 | }; 26 | 27 | if (env === 'production') { 28 | config.plugins.push( 29 | uglify({ 30 | compress: { 31 | pure_getters: true, 32 | unsafe: true, 33 | unsafe_comps: true, 34 | } 35 | }) 36 | ); 37 | } 38 | 39 | export default config; 40 | -------------------------------------------------------------------------------- /packages/redux-resource/README.md: -------------------------------------------------------------------------------- 1 | # Redux Resource 2 | 3 | A tiny but powerful system for managing 'resources': data that is persisted to 4 | remote servers. 5 | 6 | ✓ Removes nearly all "boilerplate" code for remotely-stored data 7 | ✓ Tracks the status (pending, failed, success, etc.) of _every_ request 8 | ✓ Encourages [normalized state](http://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html) 9 | ✓ Works well with APIs that adhere to standardized formats, such as JSON API 10 | ✓ Works well with APIs that don't adhere to standardized formats, too 11 | ✓ Integrates well with lots of technologies: HTTP, gRPC, normalizr, redux-observable, redux-saga, and more 12 | ✓ Microscopic file size (2kb gzipped!) 13 | 14 | ### Installation 15 | 16 | To install the latest version: 17 | 18 | ``` 19 | npm install --save redux-resource 20 | ``` 21 | 22 | ### Documentation 23 | 24 | View the documentation at 25 | **[redux-resource.js.org ⇗](https://redux-resource.js.org/)**. 26 | -------------------------------------------------------------------------------- /examples/lists-and-named-requests/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 4 | import thunk from 'redux-thunk'; 5 | import { resourceReducer } from 'redux-resource'; 6 | import BooksList from './components/BooksList'; 7 | import getUserBooks from './action-creators/get-user-books'; 8 | import getLatestBooks from './action-creators/get-latest-books'; 9 | 10 | const reducer = combineReducers({ 11 | books: resourceReducer('books') 12 | }); 13 | 14 | const store = createStore(reducer, applyMiddleware(thunk)); 15 | 16 | const rootEl = document.getElementById('root'); 17 | 18 | const render = () => 19 | ReactDOM.render( 20 | store.dispatch(getUserBooks())} 23 | getLatestBooks={() => store.dispatch(getLatestBooks())} 24 | />, 25 | rootEl 26 | ); 27 | 28 | render(); 29 | store.subscribe(render); 30 | -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import uglify from 'rollup-plugin-uglify'; 4 | import replace from 'rollup-plugin-replace'; 5 | 6 | var env = process.env.NODE_ENV; 7 | var config = { 8 | format: 'umd', 9 | moduleName: 'ReduxResourceActionCreators', 10 | external: ['redux-resource'], 11 | globals: { 12 | 'redux-resource': 'ReduxResource' 13 | }, 14 | plugins: [ 15 | nodeResolve({ 16 | jsnext: true 17 | }), 18 | babel({ 19 | exclude: 'node_modules/**' 20 | }), 21 | replace({ 22 | 'process.env.NODE_ENV': JSON.stringify(env) 23 | }) 24 | ] 25 | }; 26 | 27 | if (env === 'production') { 28 | config.plugins.push( 29 | uglify({ 30 | compress: { 31 | pure_getters: true, 32 | unsafe: true, 33 | unsafe_comps: true, 34 | } 35 | }) 36 | ); 37 | } 38 | 39 | export default config; 40 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/upsert-meta.js: -------------------------------------------------------------------------------- 1 | // Add or update meta 2 | export default function upsertMeta(meta, newMeta, mergeMeta) { 3 | if (!newMeta) { 4 | return meta; 5 | } 6 | 7 | const mergeMetaOption = typeof mergeMeta !== 'undefined' ? mergeMeta : true; 8 | const shallowClone = Object.assign({}, meta); 9 | 10 | for (let id in newMeta) { 11 | const resource = newMeta[id]; 12 | 13 | const resourceAlreadyExists = Boolean(meta[id]); 14 | 15 | // If there is no existing resource, we just add it to the meta object 16 | if (!resourceAlreadyExists) { 17 | shallowClone[id] = resource; 18 | continue; 19 | } 20 | 21 | let resourceToInsert; 22 | if (mergeMetaOption) { 23 | const currentResource = shallowClone[id]; 24 | resourceToInsert = Object.assign({}, currentResource, resource); 25 | } else { 26 | resourceToInsert = resource; 27 | } 28 | 29 | shallowClone[id] = resourceToInsert; 30 | } 31 | 32 | return shallowClone; 33 | } 34 | -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import uglify from 'rollup-plugin-uglify'; 4 | import replace from 'rollup-plugin-replace'; 5 | 6 | var env = process.env.NODE_ENV; 7 | var config = { 8 | format: 'umd', 9 | moduleName: 'ReduxResourcePropTypes', 10 | external: ['redux-resource', 'prop-types'], 11 | globals: { 12 | 'prop-types': 'PropTypes', 13 | 'redux-resource': 'ReduxResource' 14 | }, 15 | plugins: [ 16 | nodeResolve({ 17 | jsnext: true 18 | }), 19 | babel({ 20 | exclude: 'node_modules/**' 21 | }), 22 | replace({ 23 | 'process.env.NODE_ENV': JSON.stringify(env) 24 | }) 25 | ] 26 | }; 27 | 28 | if (env === 'production') { 29 | config.plugins.push( 30 | uglify({ 31 | compress: { 32 | pure_getters: true, 33 | unsafe: true, 34 | unsafe_comps: true, 35 | } 36 | }) 37 | ); 38 | } 39 | 40 | export default config; 41 | -------------------------------------------------------------------------------- /packages/redux-resource/src/action-types/deprecated.js: -------------------------------------------------------------------------------- 1 | // This function generates the five statuses from a single CRUD action. 2 | // For instance, you'd probably pass "CREATE", "READ", "UPDATE", or "DELETE" 3 | // as `crudAction`. 4 | // 5 | // These are deprecated in favor of the simpler alternatives listed in 6 | // `./action-types`. Those work the _exact_ same way. Because there are 7 | // fewer of them, they should be easier to use. 8 | const mapConstant = crudAction => ({ 9 | [`${crudAction}_RESOURCES_PENDING`]: `${crudAction}_RESOURCES_PENDING`, 10 | [`${crudAction}_RESOURCES_SUCCEEDED`]: `${crudAction}_RESOURCES_SUCCEEDED`, 11 | [`${crudAction}_RESOURCES_FAILED`]: `${crudAction}_RESOURCES_FAILED`, 12 | [`${crudAction}_RESOURCES_IDLE`]: `${crudAction}_RESOURCES_IDLE`, 13 | }); 14 | 15 | const createTypes = mapConstant('CREATE'); 16 | const readTypes = mapConstant('READ'); 17 | const updateTypes = mapConstant('UPDATE'); 18 | const deleteTypes = mapConstant('DELETE'); 19 | 20 | export default { 21 | ...createTypes, 22 | ...readTypes, 23 | ...updateTypes, 24 | ...deleteTypes, 25 | }; 26 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/redux-resource-xhr/docs/migration-guides/3-to-4.md: -------------------------------------------------------------------------------- 1 | # Migrating 2 | 3 | This guide documents migration from `redux-resource-xhr@3.x` to `redux-resource-xhr@4.0.0`. 4 | 5 | ### Upgrade to Redux Resource v3.0.0 6 | 7 | You must upgrade to `redux-resource@3.0.0` to use `redux-resource-xhr@4.0.0`. 8 | 9 | ### Other Changes 10 | 11 | Although there are breaking changes between v3.x and v4.0, they are unlikely to affect you. 12 | The list of breaking changes is as follows: 13 | 14 | **HTTP status codes are set on requests objects by default** 15 | 16 | Previously, you needed to include the HTTP Status Codes plugin 17 | for status codes to be stored on /docs/requests/request-objects.md. This happens 18 | automatically now. 19 | 20 | **HTTP status codes are no longer wiped when a request becomes pending** 21 | 22 | Previously, the HTTP status codes would be reset to `0` when a request becomes pending. Now, 23 | the status code remains the same value that it was when the last successful request 24 | occurred. 25 | 26 | This behavior is more useful when developing interfaces that use the status codes to 27 | provide feedback to users. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 James Smith 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 | -------------------------------------------------------------------------------- /packages/redux-resource-xhr/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import uglify from 'rollup-plugin-uglify'; 4 | import commonjs from 'rollup-plugin-commonjs'; 5 | import replace from 'rollup-plugin-replace'; 6 | 7 | var env = process.env.NODE_ENV; 8 | var config = { 9 | format: 'umd', 10 | moduleName: 'ReduxResourceXHR', 11 | external: ['redux-resource', 'xhr'], 12 | globals: { 13 | 'redux-resource': 'ReduxResource', 14 | xhr: 'xhr' 15 | }, 16 | plugins: [ 17 | nodeResolve({ 18 | jsnext: true 19 | }), 20 | babel({ 21 | exclude: 'node_modules/**' 22 | }), 23 | commonjs({ 24 | include: 'node_modules/**', 25 | exclude: ['node_modules/redux-resource/**'] 26 | }), 27 | replace({ 28 | 'process.env.NODE_ENV': JSON.stringify(env) 29 | }) 30 | ] 31 | }; 32 | 33 | if (env === 'production') { 34 | config.plugins.push( 35 | uglify({ 36 | compress: { 37 | pure_getters: true, 38 | unsafe: true, 39 | unsafe_comps: true, 40 | } 41 | }) 42 | ); 43 | } 44 | 45 | export default config; 46 | -------------------------------------------------------------------------------- /examples/lists-and-named-requests/src/action-creators/get-latest-books.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from 'redux-resource'; 2 | 3 | // This action creator represents retrieving a list of recently-released books. 4 | export default function getLatestBooks() { 5 | return function(dispatch) { 6 | dispatch({ 7 | type: actionTypes.READ_RESOURCES_PENDING, 8 | resourceType: 'books', 9 | requestKey: 'readLatestBooks', 10 | list: 'latestBooks', 11 | }); 12 | 13 | // This timeout is to mimic network latency. 14 | setTimeout(function() { 15 | dispatch({ 16 | type: actionTypes.READ_RESOURCES_SUCCEEDED, 17 | resourceType: 'books', 18 | requestKey: 'readLatestBooks', 19 | list: 'latestBooks', 20 | resources: [ 21 | { 22 | id: 10, 23 | title: 'Captain Underpants', 24 | author: 'Dav Pilkey', 25 | releaseYear: 1997, 26 | }, 27 | { 28 | id: 24, 29 | title: 'My Name is Red', 30 | author: 'Orhan Pamuk', 31 | releaseYear: 1998, 32 | }, 33 | ], 34 | }); 35 | }, 900); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/redux-resource/src/request-statuses-plugin/action-reducers-map.js: -------------------------------------------------------------------------------- 1 | import * as readReducers from './read'; 2 | import * as createReducers from './create'; 3 | import * as updateReducers from './update'; 4 | import * as deleteReducers from './delete'; 5 | 6 | export default { 7 | CREATE_RESOURCES_PENDING: createReducers.create, 8 | CREATE_RESOURCES_FAILED: createReducers.createFail, 9 | CREATE_RESOURCES_SUCCEEDED: createReducers.createSucceed, 10 | CREATE_RESOURCES_IDLE: createReducers.createIdle, 11 | 12 | READ_RESOURCES_PENDING: readReducers.read, 13 | READ_RESOURCES_FAILED: readReducers.readFail, 14 | READ_RESOURCES_SUCCEEDED: readReducers.readSucceed, 15 | READ_RESOURCES_IDLE: readReducers.readIdle, 16 | 17 | UPDATE_RESOURCES_PENDING: updateReducers.update, 18 | UPDATE_RESOURCES_FAILED: updateReducers.updateFail, 19 | UPDATE_RESOURCES_SUCCEEDED: updateReducers.updateSucceed, 20 | UPDATE_RESOURCES_IDLE: updateReducers.updateIdle, 21 | 22 | DELETE_RESOURCES_PENDING: deleteReducers.del, 23 | DELETE_RESOURCES_FAILED: deleteReducers.delFail, 24 | DELETE_RESOURCES_SUCCEEDED: deleteReducers.delSucceed, 25 | DELETE_RESOURCES_IDLE: deleteReducers.delIdle, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/redux-resource/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 James Smith 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 | -------------------------------------------------------------------------------- /packages/redux-resource-xhr/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 James Smith 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 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 James Smith 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 | -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 James Smith 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 | -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 James Smith 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 | -------------------------------------------------------------------------------- /examples/read-resource/src/components/Book.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { getStatus } from 'redux-resource'; 4 | 5 | class Book extends Component { 6 | render() { 7 | const { state, readBook, bookId } = this.props; 8 | const readStatus = getStatus(state, 'meta[24].readStatus'); 9 | const book = state.resources[bookId]; 10 | 11 | return ( 12 |
13 | 14 |
15 | {readStatus.pending && 'Loading book data...'} 16 | {readStatus.succeeded && ( 17 |
18 |

{book.title}

19 |
20 | {book.author} - {book.releaseYear} 21 |
22 |
23 | )} 24 |
25 | ); 26 | } 27 | } 28 | 29 | Book.propTypes = { 30 | bookId: PropTypes.number.isRequired, 31 | readBook: PropTypes.func.isRequired, 32 | state: PropTypes.shape({ 33 | resources: PropTypes.object.isRequired, 34 | meta: PropTypes.object.isRequired, 35 | requests: PropTypes.object.isRequired, 36 | lists: PropTypes.object.isRequired 37 | }) 38 | }; 39 | 40 | export default Book; 41 | -------------------------------------------------------------------------------- /docs/faq/state-tree.md: -------------------------------------------------------------------------------- 1 | # State tree 2 | 3 | #### Can I store additional metadata for resources within `meta`? 4 | 5 | You can, and we encourage it. We recommend that you avoid changing the values of 6 | the request statuses directly (use the built-in Action types to do this), but 7 | feel free to store anything else in there that you want. 8 | 9 | You can use the `UPDATE_RESOURCES` action type to change the metadata of 10 | resources. 11 | 12 | If you'd like to update metadata alongside a request, then you can write a 13 | [plugin](../other-guides/custom-action-types.md) for that. 14 | 15 | A future version of Redux Resource will support this without a plugin. 16 | 17 | #### Can I store additional properties on each state slice? 18 | 19 | Yes, you can. The only requirement is that you don't change the structure of the 20 | state that you start out with: make sure that `resources`, `meta`, `lists`, and 21 | `requests` remain Objects. If you stick with that you shouldn't run into any issues. 22 | 23 | As a convention, we recommend only storing data relevant to the resource in the 24 | slice. Use another slice for other information. 25 | 26 | #### Can I store more than one resource per state slice? 27 | 28 | We don't recommend doing this. 29 | -------------------------------------------------------------------------------- /docs/recipes/forms.md: -------------------------------------------------------------------------------- 1 | # Forms 2 | 3 | Redux Resource complements, but does not replace, form libraries. If you're already using a form 4 | library, we encourage you to continue using it alongside Redux Resource. 5 | 6 | ### Recommendations 7 | 8 | The following are two popular forms libraries for Redux: 9 | 10 | - [react-redux-form](https://github.com/davidkpiano/react-redux-form) 11 | - [redux-form](https://github.com/erikras/redux-form) 12 | 13 | Using component state for form data is also worth considering. However you decide to 14 | manage your form data, it should work well alongside Redux Resource. 15 | 16 | ### Using Resource Slices 17 | 18 | You can also store form information inside of the resource slice. A Redux best practice is to 19 | separate your client-side data from your server-side data, so form information should 20 | be kept separate from the actual resource objects themselves. 21 | 22 | Instead, you might choose to use [`meta`](../resources/meta.md) for form information, or 23 | perhaps an additional "top-level" key within the resource slice, such as `forms`. 24 | 25 | One thing to consider before going with this approach is whether or not your form allows a user 26 | to modify more than one resource at a time. If it does, then you may want to consider storing 27 | the form data outside of an individual resource slice. 28 | -------------------------------------------------------------------------------- /examples/lists-and-named-requests/src/action-creators/get-user-books.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from 'redux-resource'; 2 | 3 | // This action creator represents retrieving a user's books. 4 | export default function getUserBooks() { 5 | return function(dispatch) { 6 | dispatch({ 7 | type: actionTypes.READ_RESOURCES_PENDING, 8 | resourceType: 'books', 9 | requestKey: 'readUserBooks', 10 | list: 'userBooks', 11 | }); 12 | 13 | // This timeout is to mimic network latency. 14 | setTimeout(function() { 15 | dispatch({ 16 | type: actionTypes.READ_RESOURCES_SUCCEEDED, 17 | resourceType: 'books', 18 | requestKey: 'readUserBooks', 19 | list: 'userBooks', 20 | resources: [ 21 | { 22 | id: 1, 23 | title: 'The Brilliance of the Moon', 24 | author: 'Lian Hearn', 25 | releaseYear: 2004, 26 | }, 27 | { 28 | id: 24, 29 | title: 'My Name is Red', 30 | author: 'Orhan Pamuk', 31 | releaseYear: 1998, 32 | }, 33 | { 34 | id: 25, 35 | title: 'Love in the Time of Cholera', 36 | author: 'Gabriel García Márquez', 37 | releaseYear: 1985, 38 | }, 39 | ], 40 | }); 41 | }, 1800); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/request-statuses.js: -------------------------------------------------------------------------------- 1 | import warning from './warning'; 2 | 3 | const requestStatuses = { 4 | IDLE: 'IDLE', 5 | PENDING: 'PENDING', 6 | FAILED: 'FAILED', 7 | SUCCEEDED: 'SUCCEEDED', 8 | }; 9 | 10 | if (process.env.NODE_ENV !== 'production') { 11 | Object.defineProperty(requestStatuses, 'NULL', { 12 | // eslint-disable-next-line getter-return 13 | get() { 14 | warning( 15 | `You attempted to access the NULL request status from the requestStatus object ` + 16 | `that is exported from Redux Resource. The NULL request status ` + 17 | `has been renamed to IDLE in Redux Resource v3. Please update your ` + 18 | `application to use the new request status. Typically, this can be ` + 19 | `done by doing a find and replace within your source code to ` + 20 | `replace the string "NULL" with "IDLE". For more information, refer to ` + 21 | `the documentation for the request statuses at: ` + 22 | `https://redux-resource.js.org/docs/api-reference/request-statuses.html\n\n` + 23 | `Also, the migration guide to Redux Resource v3 can be found at: ` + 24 | `https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource/docs/migration-guides/2-to-3.md`, 25 | `NULL_REQUEST_STATUS_ACCESSED` 26 | ); 27 | }, 28 | }); 29 | } 30 | 31 | export default requestStatuses; 32 | -------------------------------------------------------------------------------- /docs/api-reference/upsert-resources.md: -------------------------------------------------------------------------------- 1 | # `upsertResources(resources, newResources, [mergeResources])` 2 | 3 | Add new or update existing resources in your state tree. 4 | 5 | #### Arguments 6 | 7 | 1. `resources` *(Object)*: The current resources object from your state tree. 8 | 9 | 2. `newResources` *(Array|Object)*: The new resources to add or update. 10 | 11 | 3. [`mergeResources`] *(Boolean)*: Whether or not to merge individual resources 12 | with the existing resource in the store, or to replace it with the new data. 13 | Defaults to `true`. 14 | 15 | #### Returns 16 | 17 | (*`Object`*): The updated resources object. 18 | 19 | #### Example 20 | 21 | ```js 22 | import { upsertResources } from 'redux-resource'; 23 | import actionTypes from './my-action-types'; 24 | 25 | export default function reducer(state, action) { 26 | switch (action.type) { 27 | case (actionTypes.CREATE_RESOURCES_CUSTOM): { 28 | const newResources = upsertResources({ 29 | resources: state.resources, 30 | newResources: action.resources 31 | }); 32 | 33 | return { 34 | ...state, 35 | resources: newResources 36 | }; 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | #### Tips 43 | 44 | - This is used internally within the reducer returned by 45 | [`resourceReducer`](resource-reducer.md) to add and update resources in the 46 | store. You will typically only need to use this function if you're authoring a 47 | [plugin](../other-guides/custom-action-types.md). 48 | -------------------------------------------------------------------------------- /docs/other-guides/migration-guides.md: -------------------------------------------------------------------------------- 1 | # Migration Guides 2 | 3 | On occasion, we release breaking changes to the libraries. This page 4 | links to various migration guides. 5 | 6 | #### redux-resource 7 | 8 | - [v2 ⇨ v3](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource/docs/migration-guides/2-to-3.md) 9 | - [`resourceful-redux` ⇨ `redux-resource`](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource/docs/migration-guides/1-to-2.md) 10 | 11 | #### redux-resource-xhr 12 | 13 | - [v3 ⇨ v4](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource-xhr/docs/migration-guides/3-to-4.md) 14 | - [v2 ⇨ v3](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource-xhr/docs/migration-guides/2-to-3.md) 15 | 16 | #### redux-resource-prop-types 17 | 18 | - [v3 ⇨ v4](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource-prop-types/docs/migration-guides/3-to-4.md) 19 | - [v2 ⇨ v3](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource-prop-types/docs/migration-guides/2-to-3.md) 20 | 21 | #### redux-resource-plugins 22 | 23 | - [v2 ⇨ v3](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource-plugins/docs/migration-guides/2-to-3.md) 24 | 25 | #### redux-resource-action-creators 26 | 27 | - [v1 ⇨ v2](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource-action-creators/docs/migration-guides/1-to-2.md) 28 | 29 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | ### Project Goals 4 | 5 | - Remain reasonably sized (< 4kb) 6 | - Changes should simplify the learning curve whenever possible 7 | - When possible, reduce the number of assumptions to increase flexibility 8 | - Ensure reasonable migration paths 9 | 10 | ### Future Versions 11 | 12 | #### v3.1.0 13 | 14 | - A new reducer specifically for requests. This allows for requests to be tracked independent 15 | from a specific resource type, paving the road for multi-operation and multi-resource requests. 16 | 17 | - New, simpler request action types. Right now, there are 16 action types for modifying requests. 18 | There should just be 4 with an action attribute where you can specify what the crud action is. 19 | This will deprecate the old action types. This has a few benefits: 20 | 21 | - it will be easier to learn and understand 22 | - prepare the library for supporting multi-operation requests 23 | - make the code smaller 24 | 25 | - Multiple resource types supported within request actions. This will deprecate the "included 26 | resources" plugin by bringing that functionality in the core library. 27 | 28 | - Deprecating use of the `mergeListMeta` feature. Imperatively update the list instead. 29 | 30 | #### Future 31 | 32 | - React bindings to simplify use of Redux Resource with React. 33 | 34 | - Deprecating request tracking on resource slices. The standalone request slice will be recommended instead. 35 | 36 | - Adopting the Standard Resource standard to add support for Computed Attributes and Relationships 37 | -------------------------------------------------------------------------------- /packages/redux-resource/src/index.js: -------------------------------------------------------------------------------- 1 | import resourceReducer from './resource-reducer'; 2 | import actionTypes from './action-types'; 3 | import requestStatuses from './utils/request-statuses'; 4 | import setResourceMeta from './utils/set-resource-meta'; 5 | import upsertResources from './utils/upsert-resources'; 6 | import getStatus from './utils/get-status'; 7 | import getResources from './utils/get-resources'; 8 | import warning from './utils/warning'; 9 | 10 | /* eslint-disable no-empty-function */ 11 | /* 12 | * This is a dummy function to check if the function name has been altered by minification. 13 | * If the function has been minified and NODE_ENV !== 'production', warn the user. 14 | */ 15 | function isCrushed() {} 16 | /* eslint-disable no-empty-function */ 17 | 18 | if ( 19 | process.env.NODE_ENV !== 'production' && 20 | typeof isCrushed.name === 'string' && 21 | isCrushed.name !== 'isCrushed' 22 | ) { 23 | warning( 24 | "You are currently using minified code outside of NODE_ENV === 'production'. " + 25 | 'This means that you are running a slower development build of Redux Resource. ' + 26 | 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 27 | 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 28 | 'to ensure you have the correct code for your production build.', 29 | 'MINIFIED' 30 | ); 31 | } 32 | 33 | export { 34 | resourceReducer, 35 | actionTypes, 36 | requestStatuses, 37 | setResourceMeta, 38 | upsertResources, 39 | getStatus, 40 | getResources, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/redux-resource/src/request-statuses-plugin/index.js: -------------------------------------------------------------------------------- 1 | import actionReducersMap from './action-reducers-map'; 2 | import initialResourceMetaState from '../utils/initial-resource-meta-state'; 3 | import warning from '../utils/warning'; 4 | 5 | export default function requestStatusesPlugin(resourceType, options = {}) { 6 | const customInitialMeta = options.initialResourceMeta || {}; 7 | const optionsToSend = { 8 | initialResourceMeta: { 9 | ...initialResourceMetaState, 10 | ...customInitialMeta, 11 | }, 12 | }; 13 | 14 | return function(state, action) { 15 | const reducer = actionReducersMap[action.type]; 16 | 17 | if (process.env.NODE_ENV !== 'production') { 18 | if (reducer && !action.resourceName && !action.resourceType) { 19 | warning( 20 | `A resourceType was not included in an action with type ` + 21 | `"${action.type}". Without a resourceType, Redux Resource will ` + 22 | `not be able to update a slice of your store. For more information, refer to ` + 23 | `the guide on Request Actions: ` + 24 | `https://redux-resource.js.org/docs/requests/request-actions.html`, 25 | 'MISSING_RESOURCE_TYPE' 26 | ); 27 | } 28 | } 29 | 30 | const actionResourceType = action.resourceType || action.resourceName; 31 | 32 | if (actionResourceType !== resourceType) { 33 | return state; 34 | } 35 | 36 | const callActionReducer = reducer && actionResourceType === resourceType; 37 | return callActionReducer ? reducer(state, action, optionsToSend) : state; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/src/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { requestStatuses } from 'redux-resource'; 3 | 4 | // Verifies a resource ID. Useful as a "building block" for 5 | // more advanced prop types. 6 | const idPropType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); 7 | 8 | // Verifies a request status. useful as a "building block" for 9 | // more advanced prop types. 10 | const requestStatusPropType = PropTypes.oneOf([ 11 | requestStatuses.IDLE, 12 | requestStatuses.PENDING, 13 | requestStatuses.FAILED, 14 | requestStatuses.SUCCEEDED, 15 | ]); 16 | 17 | // Similar to `.shape()`, except that it enforces an ID. 18 | const resourcePropType = function(props) { 19 | return PropTypes.shape({ 20 | ...props, 21 | id: idPropType.isRequired, 22 | }); 23 | }; 24 | 25 | // Similar to `.shape()`, except that it enforces proper request 26 | // statuses, as well as an ID list 27 | const requestPropType = function(props) { 28 | return PropTypes.shape({ 29 | ...props, 30 | resourceType: PropTypes.string.isRequired, 31 | requestKey: PropTypes.string.isRequired, 32 | requestName: PropTypes.string, 33 | ids: PropTypes.arrayOf(idPropType).isRequired, 34 | status: requestStatusPropType.isRequired, 35 | }); 36 | }; 37 | 38 | // The return value from `getStatus`. 39 | const statusPropType = PropTypes.shape({ 40 | idle: PropTypes.bool.isRequired, 41 | pending: PropTypes.bool.isRequired, 42 | failed: PropTypes.bool.isRequired, 43 | succeeded: PropTypes.bool.isRequired, 44 | }); 45 | 46 | export { 47 | idPropType, 48 | requestStatusPropType, 49 | resourcePropType, 50 | requestPropType, 51 | statusPropType, 52 | }; 53 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/set-resource-meta.js: -------------------------------------------------------------------------------- 1 | // Update the meta for `resources` 2 | export default function setResourceMeta(options) { 3 | const { 4 | resources, 5 | newMeta, 6 | meta, 7 | mergeMeta, 8 | initialResourceMeta = {} 9 | } = options; 10 | 11 | const next = { ...meta }; 12 | 13 | if (!resources) { 14 | return meta; 15 | } 16 | 17 | let mergeMetaOption = typeof mergeMeta !== 'undefined' ? mergeMeta : true; 18 | const resourcesArray = 19 | Array.isArray(resources) ? resources : Object.values(resources); 20 | 21 | if (!resourcesArray.length) { 22 | return next; 23 | } 24 | 25 | resourcesArray.forEach(resource => { 26 | const id = typeof resource === 'object' ? resource.id : resource; 27 | 28 | // If we have no ID for this resource, or if its not a number or string, 29 | // then we bail. This currently isn't logging so that we don't double-blast 30 | // the user with meta **and** attribute update problems. If the ID check 31 | // is moved into the success reducers directly, then we may be able to 32 | // remove these typeof checks for efficiency. 33 | if ( 34 | (!id && id !== 0) || 35 | (typeof id !== 'string' && typeof id !== 'number') 36 | ) { 37 | return; 38 | } 39 | 40 | const currentMeta = next[id]; 41 | 42 | let startMeta; 43 | if (currentMeta) { 44 | startMeta = { 45 | ...initialResourceMeta, 46 | ...currentMeta 47 | }; 48 | } else { 49 | startMeta = initialResourceMeta; 50 | } 51 | 52 | const baseMeta = mergeMetaOption ? startMeta : initialResourceMeta; 53 | 54 | next[id] = { 55 | ...baseMeta, 56 | ...newMeta 57 | }; 58 | }); 59 | 60 | return next; 61 | } 62 | -------------------------------------------------------------------------------- /docs/api-reference/set-resource-meta.md: -------------------------------------------------------------------------------- 1 | # `setResourceMeta(options)` 2 | 3 | Update one or more individual resources with the same metadata. 4 | 5 | #### Arguments 6 | 7 | 1. `options` *(Object)*: An object that defines how to update the metadata. The 8 | options are as follows: 9 | 10 | * `resources` *(Array|Object)*: An array of the resources, or resource IDs, to update 11 | with the new meta. 12 | 13 | * `newMeta` *(Object)*: The meta to set on each of the resources. 14 | 15 | * `meta` *(Object)*: The current resource meta object from this resource's 16 | store slice. Optional when `mergeMeta` is `false`, required otherwise. 17 | 18 | * [`initialResourceMeta`] *(Object)*: Additional metadata to add to any resource 19 | that previously did not have meta. 20 | 21 | * [`mergeMeta`] *(Boolean)*: Whether or not to merge a resource's old metadata 22 | with the new metadata. Defaults to `true`. 23 | 24 | #### Returns 25 | 26 | (*`Object`*): The new resource meta object. 27 | 28 | #### Example 29 | 30 | ```js 31 | import { setResourceMeta } from 'redux-resource'; 32 | import actionTypes from './my-action-types'; 33 | 34 | export default function reducer(state, action) { 35 | switch (action.type) { 36 | case (actionTypes.SELECT_MANY_RESOURCES): { 37 | const meta = setResourceMeta({ 38 | resources: action.resources, 39 | meta: state.meta, 40 | newMeta: { 41 | selected: true 42 | } 43 | }); 44 | 45 | return { 46 | ...state, 47 | meta 48 | }; 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | #### Tips 55 | 56 | - This is used internally within the reducer returned by 57 | [`resourceReducer`](./resource-reducer.md) to update the resource meta in your 58 | state tree. You will typically only need to use this method if you're writing 59 | a [plugin](../other-guides/custom-action-types.md). 60 | -------------------------------------------------------------------------------- /docs/introduction/motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | Many applications work with state that is persisted to a server. Because 4 | communicating with remote servers requires sending messages over a network, 5 | reading and writing this state does not happen instantly. The requests take 6 | time, and they may sometimes fail. 7 | 8 | These network requests are often made as a result of a user's action within the 9 | application. It's a developer's responsibility to provide feedback to the user 10 | about these requests. When using Redux, this means writing reducers that change 11 | your store's state tree based on the status of these requests. If your 12 | application has many resources, you can run into problems when you write these 13 | reducers by hand. 14 | 15 | For one, simply figuring out what information needs to be tracked can be 16 | difficult to figure out. Request tracking is a complicated problem. 17 | 18 | When you do figure out something that works, it may be implemented slightly 19 | differently for different resources in your state tree. This inconsistency will 20 | propagate to your view layer. Code bases that are not consistent are more 21 | difficult to maintain. 22 | 23 | Additionally, tracking all of this data for every request requires writing a lot 24 | of reducer code. You may omit writing some of that code to save on time. This 25 | contributes to inconsistency, and also gives you, the developer, less 26 | information to use when providing feedback to users. 27 | 28 | Redux Resource is intended to solve these problems. It provides a system of 29 | organizing information about request state in a consistent way. It also comes 30 | with reducers that keep track of as much information as possible about every 31 | request made to remote servers, so that you don't have to. 32 | 33 | Use Redux Resource to have more time to build great interfaces, rather than 34 | writing boilerplate Redux code. 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-resource-project", 3 | "private": true, 4 | "scripts": { 5 | "test": "npm run lint && npm run test:unit", 6 | "test:unit": "cross-env BABEL_ENV=test nyc --reporter=lcov --reporter=text mocha --compilers js:babel-register --colors test/setup/node.js packages/*/test/unit/**", 7 | "lint": "eslint \"packages/*/{src,test}/**/*.js\" \"./packages/*/rollup.config.js\"", 8 | "build": "lerna run build", 9 | "clean": "lerna clean", 10 | "prettier": "prettier **/*.js --write", 11 | "publish": "npm run build && lerna publish", 12 | "bootstrap": "lerna bootstrap", 13 | "contributors:add": "all-contributors add", 14 | "contributors:generate": "all-contributors generate" 15 | }, 16 | "license": "MIT", 17 | "devDependencies": { 18 | "all-contributors-cli": "^4.4.0", 19 | "babel-core": "^6.25.0", 20 | "babel-plugin-istanbul": "^4.1.4", 21 | "babel-plugin-rewire": "^1.1.0", 22 | "babel-preset-env": "^1.6.0", 23 | "babel-preset-stage-3": "^6.24.1", 24 | "babel-register": "^6.24.1", 25 | "chai": "^4.0.2", 26 | "coveralls": "^3.0.0", 27 | "cross-env": "^7.0.2", 28 | "eslint": "^6.8.0", 29 | "gitbook-cli": "^2.3.0", 30 | "lerna": "^2.0.0", 31 | "mocha": "^3.4.2", 32 | "nyc": "^11.0.3", 33 | "pleaserc": "^2.4.0", 34 | "prettier": "^1.10.2", 35 | "rimraf": "^2.6.1", 36 | "sinon": "^2.3.6", 37 | "sinon-chai": "^2.11.0" 38 | }, 39 | "nyc": { 40 | "sourceMap": false, 41 | "instrument": false, 42 | "exclude": [ 43 | "packages/*/{dist,test,lib}/**/*.js", 44 | "packages/node_modules/**/*" 45 | ] 46 | }, 47 | "dependencies": { 48 | "gitbook-plugin-anchorjs": "^2.0.0", 49 | "gitbook-plugin-edit-link": "2.0.2", 50 | "gitbook-plugin-ga": "1.0.1", 51 | "gitbook-plugin-github": "2.0.0", 52 | "gitbook-plugin-prism": "2.3.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/resources/resource-reducers.md: -------------------------------------------------------------------------------- 1 | # Resource Reducers 2 | 3 | The main export of Redux Resource, [`resourceReducer`](../api-reference/resource-reducer.html), 4 | is a function that returns a reducer. For each resource type in your application, you should use 5 | this function to create a reducer. A resource reducer will manage all of the state for its resource 6 | type. 7 | 8 | Creating a resource reducer is the first step to getting started with Redux Resource. If your 9 | application manages books, then you would create a reducer for the books resource like so: 10 | 11 | ```js 12 | import { resourceReducer } from 'redux-resource'; 13 | 14 | const booksReducer = resourceReducer('books'); 15 | ``` 16 | 17 | Once you have your resource reducers, you need to combine them into a single reducer. Users of Redux 18 | frequently use [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html) 19 | for this purpose. When you use `combineReducers`, your store is divided up into slices, where each 20 | reducer that you input manages its own section of the state. These sections are often called "slices." 21 | 22 | The rest of this documentation will frequently refer to "resource slices," which are the slices of your 23 | store that are managed by a resource reducer. 24 | 25 | The following code snippet demonstrates how you might set this up: 26 | 27 | ```js 28 | import { combineReducers } from 'redux'; 29 | import { resourceReducer } from 'redux-resource'; 30 | 31 | const reducer = combineReducers({ 32 | books: resourceReducer('books'), 33 | authors: resourceReducer('authors'), 34 | people: resourceReducer('people'), 35 | }); 36 | ``` 37 | 38 | The `resourceReducer` function takes a second option, which can be used to configure the resource reducer. 39 | Refer to [the API documentation for `resourceReducer`](../api-reference/resource-reducer.md) to 40 | learn more. 41 | 42 | > Note: keep in mind that Redux Resource works with or without `combineReducers`. 43 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/src/reset.js: -------------------------------------------------------------------------------- 1 | import { requestStatuses } from 'redux-resource'; 2 | 3 | const actionTypes = { 4 | RESET_RESOURCE: 'RESET_RESOURCE', 5 | }; 6 | 7 | function resetResource(resourceType, { request, requestKey, list } = {}) { 8 | return { 9 | type: 'RESET_RESOURCE', 10 | resourceType, 11 | request, 12 | requestKey, 13 | list, 14 | }; 15 | } 16 | 17 | function reset(resourceType, options = {}) { 18 | return function(state, action) { 19 | const typeToUse = action.resourceType || action.resourceName; 20 | if ( 21 | action.type !== actionTypes.RESET_RESOURCE || 22 | typeToUse !== resourceType 23 | ) { 24 | return state; 25 | } 26 | 27 | const { request, requestKey, list } = action; 28 | const keyToUse = requestKey || request; 29 | 30 | if (!keyToUse && !list) { 31 | return { 32 | resources: {}, 33 | meta: {}, 34 | lists: {}, 35 | requests: {}, 36 | ...options.initialState, 37 | }; 38 | } 39 | 40 | const newState = { 41 | ...state, 42 | }; 43 | 44 | if (keyToUse) { 45 | const existingRequest = state.requests[keyToUse]; 46 | const requestName = existingRequest && existingRequest.requestName; 47 | 48 | const newRequest = { 49 | resourceType: typeToUse, 50 | requestKey: keyToUse, 51 | ids: [], 52 | status: requestStatuses.IDLE, 53 | }; 54 | 55 | if (requestName) { 56 | newRequest.requestName = requestName; 57 | } 58 | 59 | newState.requests = { 60 | ...state.requests, 61 | [keyToUse]: newRequest, 62 | }; 63 | } 64 | 65 | if (list) { 66 | newState.lists = { 67 | ...state.lists, 68 | [list]: [], 69 | }; 70 | } 71 | 72 | return newState; 73 | }; 74 | } 75 | 76 | reset.actionTypes = actionTypes; 77 | reset.resetResource = resetResource; 78 | 79 | export default reset; 80 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/src/included-resources.js: -------------------------------------------------------------------------------- 1 | import { 2 | upsertResources, 3 | setResourceMeta, 4 | requestStatuses, 5 | actionTypes, 6 | } from 'redux-resource'; 7 | 8 | // This plugin adds support for "compound documents," or including multiple 9 | // resource types in a single action. 10 | export default function includedResources(resourceType, options = {}) { 11 | return (state, action) => { 12 | const { initialResourceMeta } = options; 13 | const { includedResources, mergeMeta, mergeResources, type } = action; 14 | 15 | const isReadType = type === actionTypes.READ_RESOURCES_SUCCEEDED; 16 | const isCreateType = type === actionTypes.CREATE_RESOURCES_SUCCEEDED; 17 | const isUpdateType = type === actionTypes.UPDATE_RESOURCES_SUCCEEDED; 18 | 19 | if (!isReadType && !isCreateType && !isUpdateType) { 20 | return state; 21 | } 22 | 23 | // If this action has no includedResources, then there is nothing to do 24 | if (!includedResources) { 25 | return state; 26 | } 27 | 28 | // If there are no included resources for this slice, then we do nothing 29 | const includedResourceList = includedResources[resourceType]; 30 | 31 | if (!includedResourceList) { 32 | return state; 33 | } 34 | 35 | const resources = upsertResources( 36 | state.resources, 37 | includedResourceList, 38 | mergeResources 39 | ); 40 | 41 | const newMeta = { 42 | readStatus: requestStatuses.SUCCEEDED, 43 | }; 44 | 45 | if (isCreateType) { 46 | newMeta.createStatus = requestStatuses.SUCCEEDED; 47 | } else if (isUpdateType) { 48 | newMeta.updateStatus = requestStatuses.SUCCEEDED; 49 | } 50 | 51 | const meta = setResourceMeta({ 52 | resources: includedResourceList, 53 | meta: state.meta, 54 | newMeta, 55 | initialResourceMeta, 56 | mergeMeta, 57 | }); 58 | 59 | return { 60 | ...state, 61 | meta, 62 | resources, 63 | }; 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /packages/redux-resource/src/action-types/index.js: -------------------------------------------------------------------------------- 1 | import actionTypes from './action-types'; 2 | import deprecated from './deprecated'; 3 | import warning from '../utils/warning'; 4 | 5 | const allTypes = { 6 | ...deprecated, 7 | ...actionTypes, 8 | }; 9 | 10 | if (process.env.NODE_ENV !== 'production') { 11 | // eslint-disable-next-line 12 | const warn = function(propName) { 13 | const newPropName = propName 14 | .split('_') 15 | .slice(0, 2) 16 | .concat('IDLE') 17 | .join('_'); 18 | 19 | warning( 20 | `You attempted to access the Redux Resource action type: ${propName}. ` + 21 | `This action type has been renamed to ${newPropName} ` + 22 | `in Redux Resource v3. Please update your application to ` + 23 | `use the new action type. For more information, refer to the action types ` + 24 | `documentation at: ` + 25 | `https://redux-resource.js.org/docs/api-reference/action-types.html\n\n` + 26 | `Also, the migration guide to Redux Resource v3 can be found at: ` + 27 | `https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource/docs/migration-guides/2-to-3.md`, 28 | `INVALID_PROP_${propName}_ACCESSED` 29 | ); 30 | } 31 | 32 | Object.defineProperties(allTypes, { 33 | READ_RESOURCES_NULL: { 34 | // eslint-disable-next-line getter-return 35 | get() { 36 | warn('READ_RESOURCES_NULL'); 37 | }, 38 | }, 39 | CREATE_RESOURCES_NULL: { 40 | // eslint-disable-next-line getter-return 41 | get() { 42 | warn('READ_RESOURCES_NULL'); 43 | }, 44 | }, 45 | UPDATE_RESOURCES_NULL: { 46 | // eslint-disable-next-line getter-return 47 | get() { 48 | warn('READ_RESOURCES_NULL'); 49 | }, 50 | }, 51 | DELETE_RESOURCES_NULL: { 52 | // eslint-disable-next-line getter-return 53 | get() { 54 | warn('READ_RESOURCES_NULL'); 55 | }, 56 | }, 57 | }); 58 | } 59 | 60 | export default { 61 | ...deprecated, 62 | ...actionTypes, 63 | }; 64 | -------------------------------------------------------------------------------- /docs/resources/meta.md: -------------------------------------------------------------------------------- 1 | # Meta 2 | 3 | The `meta` section of a resource slice is structured similarly to the `resources` section: 4 | it is an object, and each of its keys is a resource ID. 5 | 6 | You can store additional information about individual resources within `meta`. You can put 7 | anything that you would like here. Typically, the information that you store within `meta` 8 | are the things that are useful for the client-side application, but that don't need to be 9 | sent to the server. 10 | 11 | Resource metadata is intended to solve the problem some developers encounter when they pollute 12 | server-side data with client-side data. Mixing the two can get messy. Use metadata to keep things 13 | organized. 14 | 15 | The following example slice has some metadata, `selected`, associated with two resources. This 16 | value could represent that a user has "selected" these resources in the UI. 17 | 18 | ```js 19 | { 20 | resourceType: 'books', 21 | meta: { 22 | 4: { 23 | selected: true 24 | }, 25 | 102: { 26 | selected: true 27 | } 28 | }, 29 | // ...there are other things on a resource slice, too. But this guide will 30 | // focus on meta. 31 | } 32 | ``` 33 | 34 | ### "CRUD" Metadata 35 | 36 | Out of the box, Redux Resource sets some metadata on your resources for you. This metadata 37 | represents whether or not that particular resource is being created, read, 38 | updated, or deleted using a network request. The four values are: 39 | 40 | - `createStatus` 41 | - `readStatus` 42 | - `updateStatus` 43 | - `deleteStatus` 44 | 45 | The value of each of these properties will be one of the four 46 | [request statuses](../requests/request-statuses.md): `IDLE`, `PENDING`, `SUCCEEDED`, 47 | or `FAILED`. 48 | 49 | This metadata can be used to track the request status 50 | of CRUD operations against a particular resource in some situations. For more, refer to the 51 | [Tracking Request Statuses guide](../other-guides/tracking-request-statuses.md). 52 | 53 | ### Modifying Metadata 54 | 55 | There are two ways to modify metadata: synchronously and asynchronously. The guide on 56 | [modifying resources](modifying-resources.md) describes both of these 57 | approaches. -------------------------------------------------------------------------------- /examples/read-resource/README.md: -------------------------------------------------------------------------------- 1 | # Read Resource Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /examples/lists-and-named-requests/README.md: -------------------------------------------------------------------------------- 1 | # Labels: Read Many Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app), which provides a simple way to start React projects with no build configuration needed. 4 | 5 | Projects built with Create-React-App include support for ES6 syntax, as well as several unofficial / not-yet-final forms of Javascript syntax such as Class Properties and JSX. See the list of [language features and polyfills supported by Create-React-App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#supported-language-features-and-polyfills) for more information. 6 | 7 | ## Available Scripts 8 | 9 | In the project directory, you can run: 10 | 11 | ### `npm start` 12 | 13 | Runs the app in the development mode.
14 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 15 | 16 | The page will reload if you make edits.
17 | You will also see any lint errors in the console. 18 | 19 | ### `npm run build` 20 | 21 | Builds the app for production to the `build` folder.
22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes.
25 | Your app is ready to be deployed! 26 | 27 | ### `npm run eject` 28 | 29 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 30 | 31 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 32 | 33 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 34 | 35 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 36 | -------------------------------------------------------------------------------- /docs/api-reference/resource-reducer.md: -------------------------------------------------------------------------------- 1 | # `resourceReducer(resourceType, [options])` 2 | 3 | Creates a Redux [reducer](http://redux.js.org/docs/basics/Reducers.html) that 4 | manages a [resource slice](../introduction/core-concepts.md). 5 | 6 | #### Arguments 7 | 8 | 1. `resourceType` *(String)*: The type of your resource. Typically, you'll want 9 | to use a plural type. For instance, "books," rather than "book." When using 10 | [combineReducers](http://redux.js.org/docs/api/combineReducers.html), you should 11 | also use this as the key of your store slice for consistency. 12 | 13 | 2. [`options`] *(Object)*: Options that can be used to configure the reducer. 14 | The options are: 15 | - `initialState`: Initial state to shallowly merge into the default initial 16 | state for the slice of the store. 17 | 18 | - `plugins`: An array of reducer functions that will be called after the 19 | default reducer function. Use this to augment the behavior of the built-in 20 | reducer, or to add support for custom action types for this store slice. 21 | Plugins are functions that are called with the arguments 22 | `(state, action, options)`, where `options` are the same options that you 23 | passed to `resourceReducer`. For more, refer to the 24 | [Plugins](../other-guides/custom-action-types.md) documentation. 25 | 26 | - `initialResourceMeta`: Additional metadata to include on any new resource's 27 | metadata after a read or create operation. 28 | 29 | #### Returns 30 | 31 | ([*`Reducer`*](http://redux.js.org/docs/basics/Reducers.html)): A reducing 32 | function for this resource. 33 | 34 | #### Example 35 | 36 | ```js 37 | import { createStore, combineReducers } from 'redux'; 38 | import { resourceReducer } from 'redux-resource'; 39 | 40 | let store = createStore( 41 | combineReducers({ 42 | books: resourceReducer('books'), 43 | users: resourceReducer('users') 44 | }) 45 | ); 46 | ``` 47 | 48 | #### Tips 49 | 50 | - Any options you pass to the `resourceReducer` will also be passed to the 51 | plugins. You can use this fact to add your own custom `options` to 52 | configure the behavior of your plugins. To learn more about plugins, refer 53 | to [the Plugins guide](../other-guides/custom-action-types.md). 54 | -------------------------------------------------------------------------------- /docs/requests/request-objects.md: -------------------------------------------------------------------------------- 1 | # Request Objects 2 | 3 | A request object represents an asynchronous operation. Typically, they are used to represent 4 | HTTP requests, but they are designed to be generic enough for any kind of networking technology. 5 | Requests are stored in your Redux store. 6 | 7 | Data about requests is useful for providing feedback to users of your application, 8 | such as if the operation is still in flight, if it failed, or if it succeeded. 9 | 10 | ### Location in the Store 11 | 12 | Requests are stored on the resource slice, along with other information such `resources`, `meta`, and 13 | `lists`. Every request has a resource type associated with it, which is the resource that is 14 | primarily affected by the request. This determines which resource slice the request is stored within. 15 | 16 | Requests, like resources, require a unique identifier. For resources, the unique identifier is 17 | the `id` property. For requests, the identifier is called a key, and it is stored on the request 18 | object under the property `requestKey`. 19 | 20 | ### Properties 21 | 22 | A request object has the following properties: 23 | 24 | - `requestKey`: A string that serves as an identifier for the request 25 | - `requestName`: A human-readable string that can be useful for debugging 26 | - `resourceType`: The type of resource that is primarily affected by this request. This is 27 | the [resource slice](../resources/resource-reducers.md) that the request will be stored in. 28 | - `ids`: The resource IDs that are affected by this request 29 | - `status`: The "request status" of this request. This represents the state that the 30 | request is in. It is one of "IDLE", "PENDING", "SUCCEEDED", or "FAILED". 31 | 32 | After a successful request to read a user's favorite books, the books resource slice might look 33 | something like the following: 34 | 35 | ```js 36 | { 37 | resourceType: 'books', 38 | resources: { 39 | // resources here 40 | }, 41 | meta: { 42 | // resource metadata here 43 | }, 44 | lists: { 45 | // books lists here 46 | }, 47 | requests: { 48 | readFavoriteBooks: { 49 | requestKey: 'readFavoriteBooks', 50 | ids: ['1403', '1051', '93'], 51 | status: 'SUCCEEDED' 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | The next few guides will cover these properties of requests in greater detail. 58 | 59 | -------------------------------------------------------------------------------- /docs/introduction/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | A number of examples are distributed with the library's 4 | [source code](https://github.com/jamesplease/redux-resource). 5 | 6 | ### Read Resource 7 | 8 | To run this example: 9 | 10 | ``` 11 | git clone https://github.com/jamesplease/redux-resource.git 12 | 13 | cd redux-resource/examples/read-resource 14 | npm install 15 | npm start 16 | 17 | open http://localhost:3000/ 18 | ``` 19 | 20 | This example shows what the most basic usage of Redux Resource looks 21 | like. Two differences between a real-world application and this example are: 22 | 23 | - A real-world application would likely use the performant 24 | [React Redux](https://github.com/reactjs/react-redux) bindings for 25 | re-rendering. 26 | 27 | - A real-world application would likely use [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html) 28 | so that it could have multiple resources in its state tree. 29 | 30 | ### Lists and Named Requests 31 | 32 | To run this example: 33 | 34 | ``` 35 | git clone https://github.com/jamesplease/redux-resource.git 36 | 37 | cd redux-resource/examples/lists-and-named-requests 38 | npm install 39 | npm start 40 | 41 | open http://localhost:3000/ 42 | ``` 43 | 44 | This example shows how you can use request objects to track requests that 45 | don't target a specific resource ID. It also shows you how lists can be used 46 | to display two different ordered subsets of the same type of resource. 47 | 48 | In this example, a user's owned books are fetched, then displayed in a list on 49 | the interface. At the same time, a list of recently released books are also 50 | fetched, and displayed in another list in the interface. 51 | 52 | A real-world application would likely use the performant 53 | [React Redux](https://github.com/reactjs/react-redux) bindings for re-rendering. 54 | 55 | --- 56 | 57 | ### "Real World" Examples 58 | 59 | This is a list of open-source projects that are using Redux Resource. 60 | 61 | - [Chronos Timetracker](https://github.com/web-pal/chronos-timetracker): A desktop client 62 | for JIRA. 63 | 64 | - [Zero boilerplate redux](https://github.com/jamesplease/zero-boilerplate-redux): A simple 65 | recreation of the GitHub Gists webapp. 66 | 67 | > Do you have an open source project that uses Redux Resource that you would like to 68 | add to the list? Open an issue or make a Pull Request! 69 | -------------------------------------------------------------------------------- /docs/extras/reset-plugin.md: -------------------------------------------------------------------------------- 1 | # Reset Plugin 2 | 3 | The reset plugin allows you to remove all of the data in a slice, effectively 4 | "resetting" it. You may optionally scope the resetting to affect a single 5 | [list](../resources/lists.md) or [request object](../requests/request-objects.md). 6 | 7 | ### Usage 8 | 9 | First, you need to register this plugin for any slice that needs it. 10 | 11 | ```js 12 | import { resourceReducer } from 'redux-resource'; 13 | import { reset } from 'redux-resource-plugins'; 14 | 15 | const reducer = resourceReducer('books', { 16 | plugins: [reset] 17 | }); 18 | ``` 19 | 20 | Then, you can use the action creator that ships with the plugin to perform the 21 | reset. 22 | 23 | ```js 24 | import { reset } from 'redux-resource-plugins'; 25 | import store from './store'; 26 | 27 | store.dispatch(reset.resetResource('books')); 28 | ``` 29 | 30 | Resetting a slice will leave you with the following state: 31 | 32 | ```js 33 | { 34 | resources: {}, 35 | meta: {}, 36 | lists: {}, 37 | requests: {} 38 | } 39 | ``` 40 | 41 | Resetting a list will set the list to be an empty array. 42 | 43 | The additional initial state that you pass to `resourceReducer` will also 44 | be included when you reset your state. 45 | 46 | You can pass a second argument, `options`, to scope what is reset: 47 | 48 | ```js 49 | import { reset } from 'redux-resource-plugins'; 50 | import store from './store'; 51 | 52 | // Reset just the "createBook" request 53 | store.dispatch(reset.resetResource('books', { 54 | requestKey: 'createBook' 55 | })); 56 | 57 | // Reset just the "favorites" list 58 | store.dispatch(reset.resetResource('books', { 59 | list: 'favorites' 60 | })); 61 | 62 | // Reset a list and a request at the same time 63 | store.dispatch(reset.resetResource('books', { 64 | list: 'favorites', 65 | requestKey: 'readFavorites' 66 | })); 67 | ``` 68 | 69 | --- 70 | 71 | ### `resetResource(resourceType, [options])` 72 | 73 | Resets the slice for `resourceType`. Pass `options` to scope what's reset. 74 | There are two valid options: 75 | 76 | - `requestKey`: Reset the request with this key 77 | - `list`: Reset the list with this name 78 | 79 | #### Arguments 80 | 81 | 1. `resourceType` *(String)*: The name of the slice to reset. 82 | 83 | 2. [`options`] *(String)*: Options to scope what is reset. 84 | 85 | #### Returns 86 | 87 | (*`Object`*): A Redux action to be dispatched. 88 | -------------------------------------------------------------------------------- /docs/faq/actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | #### Are there Action Creators? 4 | 5 | Not in the core library. 6 | 7 | When it comes to making requests, there is an 8 | [HTTP Action Creators library](../extras/redux-resource-xhr.md). 9 | 10 | Developers have different preferences when it comes to making requests, so 11 | we made this library easy to use with any library that you choose. We've 12 | had great success with the library linked to above, but should you choose to 13 | write your own, we have guides to help you. 14 | 15 | Refer to the [Request Actions](../requests/request-actions.md) guide to learn more 16 | about how to build your own action creator, or the four CRUD guides for examples 17 | of action creators: 18 | 19 | - [Reading resources](../requests/reading-resources.md) 20 | - [Updating resources](../requests/reading-resources.md) 21 | - [Creating resources](../requests/reading-resources.md) 22 | - [Deleting resources](../requests/reading-resources.md) 23 | 24 | #### Does Redux Resource require you to use a specific tool for making HTTP requests? 25 | 26 | No, you can use any system for making requests that you'd like. We do strongly encourage 27 | you to use a library that supports [cancellation](../recipes/canceling-requests.md). 28 | 29 | #### Should all Actions include a request key? 30 | 31 | If you're manually coming up with the request keys, then it is typically extra boilerplate 32 | to _always_ use them. In those situations, we recommend only using request keys 33 | when they provide you value. 34 | 35 | Because request keys are used to make request objects within the store, anytime that you need a 36 | request object is when you should use a request key. The two most common use cases for request 37 | objects are: 38 | 39 | 1. creating resources 40 | 41 | 2. multiple bulk reads of the same resource type on the same page 42 | 43 | If you're not doing either of those things, then you might not need to name the request, 44 | and that's fine. 45 | 46 | A handy rule of thumb for when to use request objects, as well as much more 47 | information about requests, can be found in 48 | [Request Keys and Names guide](../requests/keys-and-names.md#when-to-use-request-keys). 49 | 50 | #### When is setting the `mergeMeta` action attribute to `false` useful? 51 | 52 | At the moment, we don't know of any use case for doing this. We included this attribute 53 | in the event that you find one – we're sure there's one out there! 54 | -------------------------------------------------------------------------------- /docs/introduction/similar-projects.md: -------------------------------------------------------------------------------- 1 | # Similar Projects 2 | 3 | This isn't the only library that aims to reduce boilerplate when it comes to 4 | managing resources. Below is a brief comparison between this library and a 5 | selection of others. Keep in mind that you're reading this comparison on 6 | the Redux Resource documentation site, so it may be biased. 7 | 8 | #### [redux-rest-resource](https://github.com/mgcrea/redux-rest-resource) 9 | 10 | redux-rest-resource does not track metadata on a per-resource basis. 11 | Instead, it merges all metadata for all resources onto a 12 | single property. 13 | 14 | Consider, for instance, if a user decides to save two resources simultaneously. 15 | Redux Resource will track those two actions independently, but 16 | redux-rest-resource will only track that at least one resource is being saved. 17 | 18 | #### [redux-resources](https://github.com/travisbloom/redux-resources) 19 | 20 | A short list of things that redux-resources does differently from this library 21 | is: 22 | 23 | 1. resources are split up in the store based on the requests that you make. 24 | Redux Resource stores all resources of the same type into one object, 25 | and provides request lists to organize your resources by requests. 26 | 1. it does not provide metadata on a per-resource level 27 | 1. it provides timestamps for the operations that you perform out of the box 28 | 1. it keeps a cache of errors returned from the server out of the box 29 | 30 | The features of redux-resources that are not included in Redux Resource 31 | would be straightforward to add in via [plugins](../other-guides/custom-action-types.md). 32 | However, getting the level of detail that Redux Resource provides for 33 | requests appears like it would be difficult to achieve using redux-resources. 34 | 35 | #### [redux-json-api](https://github.com/dixieio/redux-json-api) 36 | 37 | redux-json-api provides less detail about individual resource's metadata than 38 | Redux Resource: it stores a single number that counts the number of 39 | concurrent requests of the same type that are in flight, whereas 40 | Redux Resource tracks all requests separately. 41 | 42 | In addition, redux-json-api requires that your backend adhere to 43 | [JSON API](http://jsonapi.org/). Although Redux Resource does not provide a 44 | complete integration with JSON API out of the box, the plugin system would 45 | enable you to add more features such as relationship support. 46 | -------------------------------------------------------------------------------- /docs/api-reference/get-resources.md: -------------------------------------------------------------------------------- 1 | # `getResources(resourceSlice, filter, [options])` 2 | 3 | Returns an array or object of resources from `resourceSlice` based on the `filter` provided. 4 | 5 | #### Arguments 6 | 7 | 1. `resourceSlice` *(Object)*: The slice of your state that a `resourceReducer` is 8 | responsible for. 9 | 10 | 2. `filter` *(Array|String|Function)*: The filter to apply. It can be an array of resource 11 | IDs, or the name of a [list](../resources/lists.md). If a function is provided, then 12 | `getResources` will iterate over the collection of resources, returning the 13 | resources that the function returns truthy for. The function will be called with three arguments: 14 | `(resource, resourceMeta, resourceSlice)`. If no `filter` is provided, then all of the 15 | resources in the `resourceSlice` will be returned. 16 | 17 | 3. `options` *(Object)*: An object to customize the behavior of `getResources`. Presently, only 18 | one option is supported: `byId`. Pass `{ byId true }` to receive the results as an object 19 | instead of an array. 20 | 21 | #### Returns 22 | 23 | (*`Array|Object`*): An Array of resources, unless `byId` is passed as true, in which case an 24 | object will be returned instead. 25 | 26 | #### Example 27 | 28 | ```js 29 | import { getResources } from 'redux-resource'; 30 | import store from './store'; 31 | 32 | const state = store.getState(); 33 | 34 | // Retrieve resources by an array of IDs 35 | const someBooks = getResources(state.books, [1, 12, 23]); 36 | 37 | // Retrieve those same resources as an object 38 | const someBooksAsObject = getResources(state.books, [1, 12, 23], { byId: true }); 39 | 40 | // Retrieve resources by a list name 41 | const popularBooks = getResources(state.books, 'mostPopular'); 42 | 43 | // Retrieve the "selected" resources 44 | const selectedBooks = getResources(state.books, (resource, meta) => meta.selected); 45 | 46 | // Returns all resources 47 | const allResources = getResources(state.books); 48 | ``` 49 | 50 | ### Tips 51 | 52 | - You don't _always_ need to use this method to access resources. Just need one 53 | resource? If the resource is on a slice called `books`, you can directly access it 54 | using `store.getState().books.resources[bookId]`. 55 | 56 | - When the order of your resources doesn't matter, then it probably makes sense to 57 | pass `{ byId: true }` as `options` so that you can look up your resources more 58 | quickly. 59 | -------------------------------------------------------------------------------- /packages/redux-resource/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-resource", 3 | "version": "3.1.1", 4 | "description": "Resource management for Redux.", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "scripts": { 8 | "clean": "rimraf dist es tmp lib", 9 | "prepublish": "in-publish && npm run build || not-in-publish", 10 | "lint": "eslint src/**/*.js test/**/*.js webpack.config.js", 11 | "build": "npm run clean && npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:commonjs", 12 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 13 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 14 | "build:umd": "cross-env NODE_ENV=development BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource.js", 15 | "build:umd:min": "cross-env NODE_ENV=production BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource.min.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/jamesplease/redux-resource.git" 20 | }, 21 | "keywords": [ 22 | "redux", 23 | "api", 24 | "json api", 25 | "boilerplate", 26 | "resource", 27 | "resources", 28 | "restful", 29 | "rest", 30 | "react", 31 | "flux", 32 | "framework", 33 | "crud", 34 | "http", 35 | "database", 36 | "xhr", 37 | "state", 38 | "data", 39 | "store", 40 | "reducer", 41 | "reducers", 42 | "action", 43 | "action creators", 44 | "type", 45 | "creator" 46 | ], 47 | "author": "James Smith ", 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/jamesplease/redux-resource/issues" 51 | }, 52 | "files": [ 53 | "dist", 54 | "lib", 55 | "es" 56 | ], 57 | "homepage": "https://redux-resource.js.org", 58 | "devDependencies": { 59 | "babel-cli": "^6.24.1", 60 | "babel-core": "6.23.0", 61 | "babel-plugin-external-helpers": "^6.22.0", 62 | "babel-plugin-istanbul": "^4.1.4", 63 | "babel-preset-env": "^1.6.0", 64 | "babel-preset-stage-3": "^6.17.0", 65 | "babel-register": "^6.24.1", 66 | "cross-env": "^5.0.1", 67 | "in-publish": "^2.0.0", 68 | "rimraf": "^2.6.1", 69 | "rollup": "^0.45.1", 70 | "rollup-plugin-babel": "^2.7.1", 71 | "rollup-plugin-node-resolve": "^3.0.0", 72 | "rollup-plugin-replace": "^1.2.1", 73 | "rollup-plugin-uglify": "^2.0.1" 74 | }, 75 | "peerDependencies": { 76 | "redux": "3.x || 4.x" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/redux-resource/test/unit/reducers/update.js: -------------------------------------------------------------------------------- 1 | import { resourceReducer } from '../../../src'; 2 | import { resetCodeCache } from '../../../src/utils/warning'; 3 | 4 | describe('reducers: update', function() { 5 | beforeEach(() => { 6 | resetCodeCache(); 7 | }); 8 | 9 | // Intentionally mostly-empty. Refer to the read reducer tests and the 10 | // reducer-generator tests 11 | 12 | describe('UPDATE_RESOURCES_SUCCEEDED:', () => { 13 | it('returns the right state without a request name, without IDs', () => { 14 | stub(console, 'error'); 15 | const initialState = { 16 | resources: { 17 | 1: { id: 1 }, 18 | 3: { id: 3 }, 19 | 4: { id: 4 }, 20 | }, 21 | lists: {}, 22 | requests: { 23 | pasta: { 24 | hungry: true, 25 | }, 26 | }, 27 | meta: { 28 | 1: { 29 | name: 'what', 30 | }, 31 | 3: { 32 | deleteStatus: 'sandwiches', 33 | }, 34 | }, 35 | }; 36 | 37 | const reducer = resourceReducer('hellos', { initialState }); 38 | 39 | const reduced = reducer(undefined, { 40 | type: 'UPDATE_RESOURCES_SUCCEEDED', 41 | resourceType: 'hellos', 42 | }); 43 | 44 | expect(reduced).to.deep.equal({ 45 | ...initialState, 46 | resourceType: 'hellos', 47 | }); 48 | expect(console.error.callCount).to.equal(1); 49 | }); 50 | 51 | it('(deprecated) returns the right state without a request name, without IDs (resourceName)', () => { 52 | stub(console, 'error'); 53 | const initialState = { 54 | resources: { 55 | 1: { id: 1 }, 56 | 3: { id: 3 }, 57 | 4: { id: 4 }, 58 | }, 59 | lists: {}, 60 | requests: { 61 | pasta: { 62 | hungry: true, 63 | }, 64 | }, 65 | meta: { 66 | 1: { 67 | name: 'what', 68 | }, 69 | 3: { 70 | deleteStatus: 'sandwiches', 71 | }, 72 | }, 73 | }; 74 | 75 | const reducer = resourceReducer('hellos', { initialState }); 76 | 77 | const reduced = reducer(undefined, { 78 | type: 'UPDATE_RESOURCES_SUCCEEDED', 79 | resourceName: 'hellos', 80 | }); 81 | 82 | expect(reduced).to.deep.equal({ 83 | ...initialState, 84 | resourceType: 'hellos', 85 | }); 86 | expect(console.error.callCount).to.equal(2); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/test/unit/selection.js: -------------------------------------------------------------------------------- 1 | import { selection } from '../../src'; 2 | 3 | const { 4 | selectResources, 5 | deselectResources, 6 | clearSelectedResources 7 | } = selection; 8 | 9 | describe('selection', function() { 10 | beforeEach(() => { 11 | stub(console, 'error'); 12 | }); 13 | 14 | it('should not change the state when the resource name does not match', () => { 15 | const reducer = selection('books'); 16 | 17 | const state = { 18 | selectedIds: [24] 19 | }; 20 | 21 | const action = selectResources('sandwiches', [24]); 22 | 23 | const result = reducer(state, action); 24 | expect(result).to.equal(state); 25 | }); 26 | 27 | it('should not change the state when the action type does not match', () => { 28 | const reducer = selection('books'); 29 | 30 | const state = { 31 | selectedIds: [24] 32 | }; 33 | 34 | const action = { 35 | type: 'PLS_AND_TY', 36 | resourceName: 'books', 37 | resources: [24] 38 | }; 39 | 40 | const result = reducer(state, action); 41 | expect(result).to.equal(state); 42 | }); 43 | 44 | it('should update the state without dupes for SELECT_RESOURCES', () => { 45 | const reducer = selection('books'); 46 | 47 | const state = { 48 | pasta: true, 49 | selectedIds: [24] 50 | }; 51 | 52 | const action = selectResources('books', [1, 24, 100, 210]); 53 | 54 | const result = reducer(state, action); 55 | expect(result).to.deep.equal({ 56 | pasta: true, 57 | selectedIds: [24, 1, 100, 210] 58 | }); 59 | }); 60 | 61 | it('should update the state for DESELECT_RESOURCES', () => { 62 | const reducer = selection('books'); 63 | 64 | const state = { 65 | pasta: true, 66 | selectedIds: [24, 100, 1000] 67 | }; 68 | 69 | const action = deselectResources('books', [ 70 | { 71 | id: 100 72 | } 73 | ]); 74 | 75 | const result = reducer(state, action); 76 | expect(result).to.deep.equal({ 77 | pasta: true, 78 | selectedIds: [24, 1000] 79 | }); 80 | }); 81 | 82 | it('should update the state for CLEAR_SELECTED_RESOURCES', () => { 83 | const reducer = selection('books'); 84 | 85 | const state = { 86 | pasta: true, 87 | selectedIds: [24, 100, 1000] 88 | }; 89 | 90 | const action = clearSelectedResources('books'); 91 | 92 | const result = reducer(state, action); 93 | expect(result).to.deep.equal({ 94 | pasta: true, 95 | selectedIds: [] 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /examples/lists-and-named-requests/src/components/BooksList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { getStatus, getResources } from 'redux-resource'; 4 | 5 | class BooksList extends Component { 6 | render() { 7 | const { state } = this.props; 8 | const searchStatus = getStatus( 9 | state, 10 | 'books.requests.readUserBooks.status', 11 | true 12 | ); 13 | const latestStatus = getStatus( 14 | state, 15 | 'books.requests.readLatestBooks.status', 16 | true 17 | ); 18 | const userBooks = getResources(state.books, 'userBooks'); 19 | const latestBooks = getResources(state.books, 'latestBooks'); 20 | 21 | return ( 22 |
23 |
24 |

Your Books

25 | {searchStatus.pending && 'Loading your books...'} 26 | {searchStatus.succeeded && ( 27 |
28 | {userBooks.map(book => ( 29 |
30 |

{book.title}

31 |
32 | {book.author} - {book.releaseYear} 33 |
34 |
35 | ))} 36 |
37 | )} 38 |
39 |
40 |

Recently Released

41 | {latestStatus.pending && 'Loading recently released books...'} 42 | {latestStatus.succeeded && ( 43 |
44 | {latestBooks.map(book => ( 45 |
46 |

{book.title}

47 |
48 | {book.author} - {book.releaseYear} 49 |
50 |
51 | ))} 52 |
53 | )} 54 |
55 |
56 | ); 57 | } 58 | 59 | componentDidMount() { 60 | const { getUserBooks, getLatestBooks } = this.props; 61 | getUserBooks(); 62 | getLatestBooks(); 63 | } 64 | } 65 | 66 | BooksList.propTypes = { 67 | getUserBooks: PropTypes.func.isRequired, 68 | getLatestBooks: PropTypes.func.isRequired, 69 | state: PropTypes.shape({ 70 | books: PropTypes.shape({ 71 | resources: PropTypes.object.isRequired, 72 | meta: PropTypes.object.isRequired, 73 | requests: PropTypes.object.isRequired, 74 | lists: PropTypes.object.isRequired 75 | }) 76 | }).isRequired 77 | }; 78 | 79 | export default BooksList; 80 | -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-resource-action-creators", 3 | "version": "2.0.2", 4 | "description": "Action creators for creating Redux Resource actions.", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "scripts": { 8 | "clean": "rimraf dist es tmp lib", 9 | "prepublish": "in-publish && npm run build || not-in-publish", 10 | "lint": "eslint src/**/*.js test/**/*.js webpack.config.js", 11 | "build": "npm run clean && npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:commonjs", 12 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 13 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 14 | "build:umd": "cross-env NODE_ENV=development BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource-action-creators.js", 15 | "build:umd:min": "cross-env NODE_ENV=production BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource-action-creators.min.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/jamesplease/redux-resource.git" 20 | }, 21 | "keywords": [ 22 | "redux", 23 | "boilerplate", 24 | "resource", 25 | "resources", 26 | "framework", 27 | "crud", 28 | "http", 29 | "xhr", 30 | "state", 31 | "data", 32 | "store", 33 | "reducer", 34 | "reducers", 35 | "action", 36 | "action creators", 37 | "creator", 38 | "creators" 39 | ], 40 | "author": "Stephen Rivas Jr ", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/jamesplease/redux-resource/issues" 44 | }, 45 | "files": [ 46 | "dist", 47 | "lib", 48 | "es" 49 | ], 50 | "homepage": "https://redux-resource.js.org", 51 | "dependencies": { 52 | "redux-resource": "^3.0.0" 53 | }, 54 | "devDependencies": { 55 | "babel-cli": "^6.24.1", 56 | "babel-core": "6.23.0", 57 | "babel-plugin-external-helpers": "^6.22.0", 58 | "babel-plugin-istanbul": "^4.1.4", 59 | "babel-preset-env": "^1.6.0", 60 | "babel-preset-stage-3": "^6.17.0", 61 | "babel-register": "^6.24.1", 62 | "cross-env": "^5.0.1", 63 | "in-publish": "^2.0.0", 64 | "mocha": "^3.4.2", 65 | "rimraf": "^2.6.1", 66 | "rollup": "^0.45.1", 67 | "rollup-plugin-babel": "^2.7.1", 68 | "rollup-plugin-node-resolve": "^3.0.0", 69 | "rollup-plugin-replace": "^1.2.1", 70 | "rollup-plugin-uglify": "^2.0.1", 71 | "webpack": "^3.1.0" 72 | }, 73 | "peerDependencies": { 74 | "redux": "3.x || 4.x" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/src/selection.js: -------------------------------------------------------------------------------- 1 | const actionTypes = { 2 | SELECT_RESOURCES: 'SELECT_RESOURCES', 3 | DESELECT_RESOURCES: 'DESELECT_RESOURCES', 4 | CLEAR_SELECTED_RESOURCES: 'CLEAR_SELECTED_RESOURCES', 5 | }; 6 | 7 | const initialState = { 8 | selectedIds: [], 9 | }; 10 | 11 | function selectResources(resourceType, resources) { 12 | return { 13 | type: actionTypes.SELECT_RESOURCES, 14 | resourceType, 15 | resources, 16 | }; 17 | } 18 | 19 | function deselectResources(resourceType, resources) { 20 | return { 21 | type: actionTypes.DESELECT_RESOURCES, 22 | resourceType, 23 | resources, 24 | }; 25 | } 26 | 27 | function clearSelectedResources(resourceType) { 28 | return { 29 | type: actionTypes.CLEAR_SELECTED_RESOURCES, 30 | resourceType, 31 | }; 32 | } 33 | 34 | function selection(resourceType) { 35 | return function(state, action) { 36 | // Ignore actions that were dispatched for another resource type 37 | const typeToUse = action.resourceType || action.resourceName; 38 | if (typeToUse !== resourceType) { 39 | return state; 40 | } 41 | 42 | let selectedIds, resourceIds; 43 | const resources = action.resources || []; 44 | 45 | if (action.type === actionTypes.SELECT_RESOURCES) { 46 | selectedIds = [...state.selectedIds] || []; 47 | resourceIds = resources.map(r => (typeof r === 'object' ? r.id : r)); 48 | 49 | const selectedIdsSet = new Set(selectedIds); 50 | 51 | resourceIds.forEach(id => { 52 | if (!selectedIdsSet.has(id)) { 53 | selectedIds.push(id); 54 | } 55 | }); 56 | 57 | return { 58 | ...state, 59 | selectedIds, 60 | }; 61 | } else if (action.type === actionTypes.DESELECT_RESOURCES) { 62 | selectedIds = [...state.selectedIds] || []; 63 | resourceIds = resources.map(r => (typeof r === 'object' ? r.id : r)); 64 | const resourceIdsSet = new Set(resourceIds); 65 | 66 | return { 67 | ...state, 68 | selectedIds: selectedIds.filter(id => !resourceIdsSet.has(id)), 69 | }; 70 | } else if (action.type === actionTypes.CLEAR_SELECTED_RESOURCES) { 71 | return { 72 | ...state, 73 | selectedIds: [], 74 | }; 75 | } 76 | 77 | return state; 78 | }; 79 | } 80 | 81 | selection.actionTypes = actionTypes; 82 | selection.selectResources = selectResources; 83 | selection.deselectResources = deselectResources; 84 | selection.clearSelectedResources = clearSelectedResources; 85 | selection.initialState = initialState; 86 | 87 | export default selection; 88 | -------------------------------------------------------------------------------- /packages/redux-resource/test/unit/reducers/create.js: -------------------------------------------------------------------------------- 1 | import { resourceReducer } from '../../../src'; 2 | import { resetCodeCache } from '../../../src/utils/warning'; 3 | 4 | describe('reducers: create', function() { 5 | // Intentionally mostly-empty. Refer to the read reducer tests and the 6 | // reducer-generator tests 7 | 8 | beforeEach(() => { 9 | resetCodeCache(); 10 | }); 11 | 12 | describe('CREATE_RESOURCES_SUCCEEDED:', () => { 13 | it('returns the right state without a request name, without IDs', () => { 14 | stub(console, 'error'); 15 | const initialState = { 16 | resources: { 17 | 1: { id: 1 }, 18 | 3: { id: 3 }, 19 | 4: { id: 4 }, 20 | }, 21 | requests: { 22 | pasta: { 23 | hungry: true, 24 | }, 25 | }, 26 | lists: { 27 | bookmarks: [1, 2, 3], 28 | }, 29 | meta: { 30 | 1: { 31 | name: 'what', 32 | }, 33 | 3: { 34 | deleteStatus: 'sandwiches', 35 | }, 36 | }, 37 | }; 38 | 39 | const reducer = resourceReducer('hellos', { initialState }); 40 | 41 | const reduced = reducer(undefined, { 42 | type: 'CREATE_RESOURCES_SUCCEEDED', 43 | resourceType: 'hellos', 44 | }); 45 | 46 | expect(reduced).to.deep.equal({ 47 | ...initialState, 48 | resourceType: 'hellos', 49 | }); 50 | expect(console.error.callCount).to.equal(1); 51 | }); 52 | 53 | it('(deprecated) returns the right state without a request name, without IDs (resourceName)', () => { 54 | stub(console, 'error'); 55 | const initialState = { 56 | resources: { 57 | 1: { id: 1 }, 58 | 3: { id: 3 }, 59 | 4: { id: 4 }, 60 | }, 61 | requests: { 62 | pasta: { 63 | hungry: true, 64 | }, 65 | }, 66 | lists: { 67 | bookmarks: [1, 2, 3], 68 | }, 69 | meta: { 70 | 1: { 71 | name: 'what', 72 | }, 73 | 3: { 74 | deleteStatus: 'sandwiches', 75 | }, 76 | }, 77 | }; 78 | 79 | const reducer = resourceReducer('hellos', { initialState }); 80 | 81 | const reduced = reducer(undefined, { 82 | type: 'CREATE_RESOURCES_SUCCEEDED', 83 | resourceName: 'hellos', 84 | }); 85 | 86 | expect(reduced).to.deep.equal({ 87 | ...initialState, 88 | resourceType: 'hellos', 89 | }); 90 | expect(console.error.callCount).to.equal(2); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/get-path-parts.js: -------------------------------------------------------------------------------- 1 | // Given a path, such as "meta[24].readStatus", this will return an array of "path parts." In 2 | // that example, the path parts would be ['meta', '24', 'readStatus']. 3 | // This method supports dot notation as well as bracket notation. 4 | export default function getPathParts(path) { 5 | const parts = []; 6 | let i = 0; 7 | let nextDot, nextOpenBracket, openQuote, nextCloseBracket; 8 | 9 | while (i < path.length) { 10 | nextDot = path.indexOf('.', i); 11 | nextOpenBracket = path.indexOf('[', i); 12 | 13 | // When there are no dots nor opening brackets ahead of the 14 | // current index, then we've reached the final path part. 15 | if (nextDot === -1 && nextOpenBracket === -1) { 16 | parts.push(path.slice(i, path.length)); 17 | i = path.length; 18 | } else if ( 19 | nextOpenBracket === -1 || 20 | (nextDot !== -1 && nextDot < nextOpenBracket) 21 | ) { 22 | // This handles dots. When there are no more open brackets, or the next dot is before 23 | // the next open bracket, then we simply add the path part. 24 | parts.push(path.slice(i, nextDot)); 25 | i = nextDot + 1; 26 | } else { 27 | // If neither of the above two conditions are met, then we're dealing with a bracket. 28 | if (nextOpenBracket > i) { 29 | parts.push(path.slice(i, nextOpenBracket)); 30 | i = nextOpenBracket; 31 | } 32 | 33 | openQuote = path.slice(nextOpenBracket + 1, nextOpenBracket + 2); 34 | 35 | // This handles the situation when we do not have quotes. For instance, [24] or [asdf] 36 | if (openQuote !== '"' && openQuote !== "'") { 37 | nextCloseBracket = path.indexOf(']', nextOpenBracket); 38 | if (nextCloseBracket === -1) { 39 | nextCloseBracket = path.length; 40 | } 41 | parts.push(path.slice(i + 1, nextCloseBracket)); 42 | i = 43 | path.slice(nextCloseBracket + 1, nextCloseBracket + 2) === '.' 44 | ? nextCloseBracket + 2 45 | : nextCloseBracket + 1; 46 | } else { 47 | // This handles brackets that are wrapped in quotes. For instance, ["hello"] or ['24'] 48 | nextCloseBracket = path.indexOf(`${openQuote}]`, nextOpenBracket); 49 | if (nextCloseBracket === -1) { 50 | nextCloseBracket = path.length; 51 | } 52 | parts.push(path.slice(i + 2, nextCloseBracket)); 53 | i = 54 | path.slice(nextCloseBracket + 2, nextCloseBracket + 3) === '.' 55 | ? nextCloseBracket + 3 56 | : nextCloseBracket + 2; 57 | } 58 | } 59 | } 60 | 61 | return parts; 62 | } 63 | -------------------------------------------------------------------------------- /docs/requests/request-names.md: -------------------------------------------------------------------------------- 1 | # Request Names 2 | 3 | Request names are a feature that can be useful if you are automatically 4 | generating [request keys](requests/request-keys.md). If you are 5 | not automatically generating request keys, then you probably do not need 6 | to use request names. 7 | 8 | ### Motivation 9 | 10 | Automatically-generated request keys are useful for advanced networking features 11 | such as response caching and request deduplication, but they are often an inconvenience 12 | when it comes to debugging code. 13 | 14 | For instance, if you have an endpoint that allows a user to run a search, two 15 | searches against the endpoint may have the keys "aBui9Xc" and "9d8cdd3". These keys 16 | don't communicate the _purpose_ of these requests; they are just arbitrary strings. 17 | 18 | Providing a request name can help developers who are debugging the application. 19 | In that situation, a request name like `"searchBooks"` could be specified. Later, 20 | when a developer is looking at the request, they will have some context on what 21 | the intention of the request is. 22 | 23 | Request names are like function names in JavaScript. Although we could use anonymous 24 | functions everywhere, we tend to provide names for our functions so that developers know what 25 | they do. 26 | 27 | In summary, request names are optional, human-readable descriptions of what the 28 | request's intention is. 29 | 30 | ### Specifying a Request Name 31 | 32 | Add the `requestName` property to a [request action](./request-actions.md) to specify a 33 | name for that request. 34 | 35 | ```js 36 | import { actionTypes } fom 'redux-resource'; 37 | import store from './store'; 38 | 39 | store.dispatch({ 40 | type: actionTypes.READ_RESOURCES_PENDING, 41 | resourceType: 'books', 42 | 43 | // In this situation, we are generating a key. It could be a random string of data, 44 | // which makes debugging hard. 45 | requestKey: generateRequestKey(/* request data */), 46 | 47 | // By specifying a human-readable name, we are helping future developers out who are 48 | // debugging our application. 49 | requestName: 'searchBooks' 50 | }); 51 | ``` 52 | 53 | Every request has two actions: a start action, and an end action. You should specify the 54 | name for both of these actions. For more on request actions, refer to the 55 | [request actions guide](requests/request-actions.md). 56 | 57 | > Note: if you specify the `request` property on an action, then it will be used 58 | > as both the key and the name. This API is from Redux Resource < 3.0. Although 59 | > it will continue to work into the future, it is recommended that you explicitly 60 | > set `requestKey` and `requestName` separately going forward. 61 | -------------------------------------------------------------------------------- /packages/redux-resource/test/unit/action-types.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from '../../src'; 2 | 3 | describe('actionTypes', function() { 4 | it('should be an object', () => { 5 | expect(actionTypes).to.be.an('object'); 6 | }); 7 | 8 | describe('synchronous actions', () => { 9 | expect(actionTypes.UPDATE_RESOURCES).to.equal('UPDATE_RESOURCES'); 10 | expect(actionTypes.DELETE_RESOURCES).to.equal('DELETE_RESOURCES'); 11 | }); 12 | 13 | describe('create', () => { 14 | it('should have the right actionTypes', () => { 15 | expect(actionTypes.CREATE_RESOURCES_PENDING).to.equal( 16 | 'CREATE_RESOURCES_PENDING' 17 | ); 18 | expect(actionTypes.CREATE_RESOURCES_SUCCEEDED).to.equal( 19 | 'CREATE_RESOURCES_SUCCEEDED' 20 | ); 21 | expect(actionTypes.CREATE_RESOURCES_FAILED).to.equal( 22 | 'CREATE_RESOURCES_FAILED' 23 | ); 24 | expect(actionTypes.CREATE_RESOURCES_IDLE).to.equal( 25 | 'CREATE_RESOURCES_IDLE' 26 | ); 27 | }); 28 | }); 29 | 30 | describe('read', () => { 31 | it('should have the right actionTypes', () => { 32 | expect(actionTypes.READ_RESOURCES_PENDING).to.equal( 33 | 'READ_RESOURCES_PENDING' 34 | ); 35 | expect(actionTypes.READ_RESOURCES_SUCCEEDED).to.equal( 36 | 'READ_RESOURCES_SUCCEEDED' 37 | ); 38 | expect(actionTypes.READ_RESOURCES_FAILED).to.equal( 39 | 'READ_RESOURCES_FAILED' 40 | ); 41 | expect(actionTypes.READ_RESOURCES_IDLE).to.equal('READ_RESOURCES_IDLE'); 42 | }); 43 | }); 44 | 45 | describe('update', () => { 46 | it('should have the right actionTypes', () => { 47 | expect(actionTypes.UPDATE_RESOURCES_PENDING).to.equal( 48 | 'UPDATE_RESOURCES_PENDING' 49 | ); 50 | expect(actionTypes.UPDATE_RESOURCES_SUCCEEDED).to.equal( 51 | 'UPDATE_RESOURCES_SUCCEEDED' 52 | ); 53 | expect(actionTypes.UPDATE_RESOURCES_FAILED).to.equal( 54 | 'UPDATE_RESOURCES_FAILED' 55 | ); 56 | expect(actionTypes.UPDATE_RESOURCES_IDLE).to.equal( 57 | 'UPDATE_RESOURCES_IDLE' 58 | ); 59 | }); 60 | }); 61 | 62 | describe('delete', () => { 63 | it('should have the right actionTypes', () => { 64 | expect(actionTypes.DELETE_RESOURCES_PENDING).to.equal( 65 | 'DELETE_RESOURCES_PENDING' 66 | ); 67 | expect(actionTypes.DELETE_RESOURCES_SUCCEEDED).to.equal( 68 | 'DELETE_RESOURCES_SUCCEEDED' 69 | ); 70 | expect(actionTypes.DELETE_RESOURCES_FAILED).to.equal( 71 | 'DELETE_RESOURCES_FAILED' 72 | ); 73 | expect(actionTypes.DELETE_RESOURCES_IDLE).to.equal( 74 | 'DELETE_RESOURCES_IDLE' 75 | ); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /docs/other-guides/usage-with-react.md: -------------------------------------------------------------------------------- 1 | # Usage With React 2 | 3 | It is not a requirement you should use React to use Redux Resource, but the two work 4 | well together. We recommend using the [react-redux](https://github.com/reactjs/react-redux) 5 | library to [link Redux with React](http://redux.js.org/docs/basics/UsageWithReact.html). 6 | 7 | The following are some patterns that we find ourselves using frequently with Redux Resource. 8 | 9 | ### Using `mapStateToProps` 10 | 11 | We recommend placing your calls to [`getResources`](../api-reference/get-resources.md) 12 | and [`getStatus`](../api-reference/get-status.md) within 13 | `mapStateToProps`. That way, you can access this information from any of the 14 | component's lifecycle methods, without needing to compute them within each method. 15 | 16 | For example: 17 | 18 | ```js 19 | import { getResources, getStatus } from 'redux-resource'; 20 | 21 | function mapStateToProps(state) { 22 | const searchedBooks = getResources(state.books, 'searchResults'); 23 | const searchStatus = getStatus(state, 'books.requests.getSearchResults.status'); 24 | 25 | return { 26 | searchedBooks, 27 | searchStatus 28 | }; 29 | } 30 | ``` 31 | 32 | ### Type Checking with Prop Types 33 | 34 | Redux Resource Prop Types exports a number of helpful prop types for common props 35 | that you'll pass into your React Components. Read the [Redux Resource Prop Types 36 | documentation](../extras/redux-resource-prop-types.md) for more. 37 | 38 | ### Using Request Statuses 39 | 40 | This is such an important topic that there is a 41 | [dedicated guide for it](using-request-statuses.md). 42 | 43 | ### Determining When a Request Succeeds 44 | 45 | Sometimes, you want to know the exact moment that a request succeeds. You can do this 46 | within your components by comparing the previous state with the next state to determine 47 | when a request changes from one status to another. 48 | 49 | We recommend performing this check within `componentDidUpdate`. This 50 | might look like: 51 | 52 | ```js 53 | import { getResources, getStatus } from 'redux-resource'; 54 | 55 | class BooksList extends Component { 56 | render() { 57 | // Render contents here 58 | } 59 | 60 | // Let's log to the console whenever a search result succeeds 61 | componentDidUpdate(prevProps) { 62 | if (this.props.searchStatus.succeeded && prevProps.searchStatus.pending) { 63 | console.log('The search request just succeeded.'); 64 | } 65 | } 66 | } 67 | 68 | 69 | function mapStateToProps(state) { 70 | const searchStatus = getStatus(state, 'books.requests.getSearchResults.status'); 71 | 72 | return { 73 | searchStatus 74 | }; 75 | } 76 | ``` 77 | 78 | This same approach can also be used to detect the moment that a request fails. 79 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-resource-plugins", 3 | "version": "4.0.0", 4 | "description": "Official plugins for Redux Resource", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "scripts": { 8 | "clean": "rimraf dist es tmp lib", 9 | "prepublish": "in-publish && npm run build || not-in-publish", 10 | "lint": "eslint src/**/*.js test/**/*.js webpack.config.js", 11 | "build": "npm run clean && npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:commonjs", 12 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 13 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 14 | "build:umd": "cross-env NODE_ENV=development BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource-plugins.js", 15 | "build:umd:min": "cross-env NODE_ENV=production BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource-plugins.min.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/jamesplease/redux-resource.git" 20 | }, 21 | "keywords": [ 22 | "redux", 23 | "api", 24 | "json api", 25 | "boilerplate", 26 | "resource", 27 | "resources", 28 | "restful", 29 | "rest", 30 | "react", 31 | "flux", 32 | "framework", 33 | "crud", 34 | "http", 35 | "database", 36 | "xhr", 37 | "state", 38 | "data", 39 | "store", 40 | "reducer", 41 | "reducers", 42 | "action", 43 | "action creators", 44 | "type", 45 | "creator", 46 | "creators", 47 | "prop", 48 | "types", 49 | "type", 50 | "checking" 51 | ], 52 | "author": "James Smith ", 53 | "license": "MIT", 54 | "bugs": { 55 | "url": "https://github.com/jamesplease/redux-resource/issues" 56 | }, 57 | "files": [ 58 | "dist", 59 | "lib", 60 | "es" 61 | ], 62 | "homepage": "https://redux-resource.js.org", 63 | "devDependencies": { 64 | "babel-cli": "^6.24.1", 65 | "babel-core": "6.23.0", 66 | "babel-plugin-external-helpers": "^6.22.0", 67 | "babel-plugin-istanbul": "^4.1.4", 68 | "babel-preset-env": "^1.6.0", 69 | "babel-preset-stage-3": "^6.17.0", 70 | "babel-register": "^6.24.1", 71 | "cross-env": "^5.0.1", 72 | "in-publish": "^2.0.0", 73 | "mocha": "^3.4.2", 74 | "normalizr": "^3.2.4", 75 | "redux-resource": "3.0.0", 76 | "rimraf": "^2.6.1", 77 | "rollup": "^0.45.1", 78 | "rollup-plugin-babel": "^2.7.1", 79 | "rollup-plugin-node-resolve": "^3.0.0", 80 | "rollup-plugin-replace": "^1.2.1", 81 | "rollup-plugin-uglify": "^2.0.1", 82 | "webpack": "^3.1.0" 83 | }, 84 | "peerDependencies": { 85 | "redux": "3.x || 4.x", 86 | "redux-resource": "^3.0.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /docs/faq/lists.md: -------------------------------------------------------------------------------- 1 | # Lists 2 | 3 | #### How can you keep a list in order? 4 | 5 | Redux Resource currently only provides lists as a way to keep track 6 | of the initial sort order returned from a single request. As more 7 | requests are made against a single list, it will not respect any 8 | particular order. 9 | 10 | If you'd like to maintain ordering of some kind within a specific 11 | list, or all of your lists, then we recommend 12 | [writing a plugin](../other-guides/custom-action-types.md) to handle that. 13 | 14 | #### Can lists be used for keeping track of client-side lists of resources? 15 | 16 | Yes, they can. Use the `UPDATE_RESOURCES` action type to manage 17 | client-side things. 18 | 19 | #### How do you know when to use dynamically-named lists or not? 20 | 21 | In the majority of situations, you won't need to use dynamic lists. 22 | For instance, if you are building a banking application that lets a user 23 | display transactions for each of their bank accounts, you might think 24 | to make a list for each one, like this: 25 | 26 | `transactionsFor${accountId}` 27 | 28 | There are situations when this could be useful. For instance, if you wish 29 | to display the transactions of many bank accounts onscreen at once. Most 30 | applications, though, only let the user see one set of transactions per 31 | account at once. Therefore, it's much better to just use a single list. 32 | 33 | `transactionsForAccount` 34 | 35 | As the user moves between pages in the application, you can set `mergeListIds` 36 | to `false` to throw away the previous list, and start fresh. 37 | 38 | Concerned about caching? That should be handled at the request level instead. Check 39 | out [the caching recipe](../recipes/caching.md) for more. 40 | 41 | If you need animations, then you may consider using dynamic lists or a solution 42 | like [freezus](https://github.com/threepointone/freezus) to "freeze" the state 43 | of the outgoing component. 44 | 45 | #### Why is `mergeListIds` set to `true` in request actions by default? 46 | 47 | There are a few reasons. 48 | 49 | 1. It's nice that all of the `mergeX` attributes of the CRUD actions are `true` by default. 50 | 2. Multiple requests can contribute to a list. For instance, a user may read a list of 51 | favorites, and then create a new favorite. In this situation, we have multiple requests 52 | contributing to the same list, so it's good that the resources are merged, rather than 53 | replaced. 54 | 55 | We understand that `mergeListIds` is one of those attributes that you'll frequently be 56 | setting to false. We believe the reasons above justify keeping it `true` by default, but 57 | if you disagree, feel free to 58 | [open an issue](https://github.com/jamesplease/redux-resource/issues/new?title=mergeListIds+defaults+to+true) 59 | and we'd be happy to discuss it further with you! -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-resource-prop-types", 3 | "version": "4.0.2", 4 | "description": "prop-types for Redux Resource", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "scripts": { 8 | "clean": "rimraf dist es tmp lib", 9 | "prepublish": "in-publish && npm run build || not-in-publish", 10 | "lint": "eslint src/**/*.js test/**/*.js webpack.config.js", 11 | "build": "npm run clean && npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:commonjs", 12 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 13 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 14 | "build:umd": "cross-env NODE_ENV=development BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource-prop-types.js", 15 | "build:umd:min": "cross-env NODE_ENV=production BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource-prop-types.min.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/jamesplease/redux-resource.git" 20 | }, 21 | "keywords": [ 22 | "redux", 23 | "api", 24 | "json api", 25 | "boilerplate", 26 | "resource", 27 | "resources", 28 | "restful", 29 | "rest", 30 | "react", 31 | "flux", 32 | "framework", 33 | "crud", 34 | "http", 35 | "database", 36 | "xhr", 37 | "state", 38 | "data", 39 | "store", 40 | "reducer", 41 | "reducers", 42 | "action", 43 | "action creators", 44 | "type", 45 | "creator", 46 | "creators", 47 | "prop", 48 | "types", 49 | "type", 50 | "checking" 51 | ], 52 | "author": "James Smith ", 53 | "license": "MIT", 54 | "bugs": { 55 | "url": "https://github.com/jamesplease/redux-resource/issues" 56 | }, 57 | "files": [ 58 | "dist", 59 | "lib", 60 | "es" 61 | ], 62 | "homepage": "https://redux-resource.js.org", 63 | "dependencies": { 64 | "prop-types": "^15.5.10", 65 | "redux-resource": "^3.0.0" 66 | }, 67 | "devDependencies": { 68 | "babel-cli": "^6.24.1", 69 | "babel-core": "6.23.0", 70 | "babel-plugin-external-helpers": "^6.22.0", 71 | "babel-plugin-istanbul": "^4.1.4", 72 | "babel-preset-env": "^1.6.0", 73 | "babel-preset-stage-3": "^6.17.0", 74 | "babel-register": "^6.24.1", 75 | "cross-env": "^5.0.1", 76 | "in-publish": "^2.0.0", 77 | "mocha": "^3.4.2", 78 | "rimraf": "^2.6.1", 79 | "rollup": "^0.45.1", 80 | "rollup-plugin-babel": "^2.7.1", 81 | "rollup-plugin-node-resolve": "^3.0.0", 82 | "rollup-plugin-replace": "^1.2.1", 83 | "rollup-plugin-uglify": "^2.0.1", 84 | "webpack": "^3.1.0" 85 | }, 86 | "peerDependencies": { 87 | "redux": "3.x || 4.x" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /docs/extras/http-status-codes-plugin.md: -------------------------------------------------------------------------------- 1 | # HTTP Status Codes Plugin 2 | 3 | ### Documentation 4 | 5 | Add this plugin to keep track of status codes of your HTTP Requests on 6 | resource metadata. This is useful because status codes give you more 7 | detail information about your in-flight requests. 8 | 9 | Note that you can simply use [request objects](../requests/request-objects.md) 10 | instead of this plugin. For instance: 11 | 12 | ```js 13 | dispatch({ 14 | type: 'READ_RESOURCES_FAILED', 15 | resourceType: 'books', 16 | requestKey: 'searchBooks', 17 | requestProperties: { 18 | statusCode: 404 19 | } 20 | }); 21 | 22 | // => resource object: 23 | // 24 | // { 25 | // requestKey: 'searchBooks', 26 | // status: 'FAILED', 27 | // statusCode: 404 28 | // } 29 | ``` 30 | 31 | This plugin is only useful when you specifically want to track the status on 32 | resource metadata. 33 | 34 | ### Usage 35 | 36 | First, you need to register this plugin when you call 37 | [`resourceReducer`](../api-reference/resource-reducer.md). 38 | 39 | ```js 40 | import { resourceReducer } from 'redux-resource'; 41 | import { httpStatusCodes } from 'redux-resource-plugins'; 42 | 43 | const reducer = resourceReducer('books', { 44 | plugins: [httpStatusCodes] 45 | }); 46 | ``` 47 | 48 | This plugin doesn't come with any custom action types. Instead, it changes the 49 | way the state is tranformed with the built-in CRUD 50 | [action types](../api-reference/action-types.md). Any time that you pass a 51 | `statusCode` in an action with one of those types, then the code will be stored 52 | in your state tree. 53 | 54 | Passing the status code looks like the following: 55 | 56 | ```js 57 | import { actionTypes } from 'redux-resource'; 58 | import store from './store'; 59 | 60 | store.dispatch({ 61 | type: actionTypes.READ_RESOURCES_FAILED, 62 | resourceType: 'books', 63 | resources: [10], 64 | statusCode: 404 65 | }); 66 | ``` 67 | 68 | If you're using the 69 | [Redux Resource XHR](redux-resource-xhr.md) 70 | library, then you don't need to do anything differently: request status 71 | codes are already included in the actions dispatched from that library. 72 | 73 | Within your resource metadata, the status code will be available at one of four 74 | keys, depending on the CRUD operation being performed: 75 | 76 | - `createStatusCode` 77 | - `readStatusCode` 78 | - `updateStatusCode` 79 | - `deleteStatusCode` 80 | 81 | On a request object, the code is just available under `statusCode`. 82 | 83 | ```js 84 | import store from './store'; 85 | 86 | const state = store.getState(); 87 | 88 | // Access the status codes of some resource meta 89 | const bookStatusCode = state.books.meta[24].readStatusCode; 90 | 91 | // Access the status code from a request object 92 | const searchStatusCode = state.books.requests.search.statusCode; 93 | ``` 94 | -------------------------------------------------------------------------------- /packages/redux-resource-xhr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-resource-xhr", 3 | "version": "4.0.2", 4 | "description": "Action creators for Redux Resource using the xhr library", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "scripts": { 8 | "clean": "rimraf dist es tmp lib", 9 | "prepublish": "in-publish && npm run build || not-in-publish", 10 | "lint": "eslint src/**/*.js test/**/*.js webpack.config.js", 11 | "build": "npm run clean && cross-env npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:commonjs", 12 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 13 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 14 | "build:umd": "cross-env NODE_ENV=development BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource-xhr.js", 15 | "build:umd:min": "cross-env NODE_ENV=production BABEL_ENV=build rollup -c -i src/index.js -o dist/redux-resource-xhr.min.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/jamesplease/redux-resource.git" 20 | }, 21 | "keywords": [ 22 | "redux", 23 | "api", 24 | "json api", 25 | "boilerplate", 26 | "resource", 27 | "resources", 28 | "restful", 29 | "rest", 30 | "react", 31 | "flux", 32 | "framework", 33 | "crud", 34 | "http", 35 | "database", 36 | "xhr", 37 | "state", 38 | "data", 39 | "store", 40 | "reducer", 41 | "reducers", 42 | "action", 43 | "action creators", 44 | "type", 45 | "creator" 46 | ], 47 | "author": "James Smith ", 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/jamesplease/redux-resource/issues" 51 | }, 52 | "files": [ 53 | "dist", 54 | "lib", 55 | "es" 56 | ], 57 | "homepage": "https://redux-resource.js.org", 58 | "dependencies": { 59 | "querystringify": "^1.0.0", 60 | "redux-resource": "^3.0.0", 61 | "xhr": "^2.4.0" 62 | }, 63 | "devDependencies": { 64 | "babel-cli": "^6.24.1", 65 | "babel-core": "6.23.0", 66 | "babel-plugin-external-helpers": "^6.22.0", 67 | "babel-plugin-istanbul": "^4.1.4", 68 | "babel-plugin-rewire": "^1.1.0", 69 | "babel-preset-env": "^1.6.0", 70 | "babel-preset-stage-3": "^6.17.0", 71 | "babel-register": "^6.24.1", 72 | "cross-env": "^5.0.1", 73 | "in-publish": "^2.0.0", 74 | "qs": "^6.5.1", 75 | "rimraf": "^2.6.1", 76 | "rollup": "^0.45.1", 77 | "rollup-plugin-babel": "^2.7.1", 78 | "rollup-plugin-commonjs": "^8.0.2", 79 | "rollup-plugin-node-resolve": "^3.0.0", 80 | "rollup-plugin-replace": "^1.2.1", 81 | "rollup-plugin-uglify": "^2.0.1", 82 | "webpack": "^3.1.0" 83 | }, 84 | "peerDependencies": { 85 | "redux": "3.x || 4.x" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/docs/migration-guides/2-to-3.md: -------------------------------------------------------------------------------- 1 | # Migrating 2 | 3 | This guide covers a migration from `redux-resource-prop-types@2.x` to `redux-resource-prop-types@3.0.0`. 4 | 5 | v3.0.0 of `redux-resource-prop-types` introduced several breaking changes. The philosophy behind 6 | v3.0.0 is that we want to provide you with better primitive prop types to build more robust 7 | prop types. 8 | 9 | The v2 prop types were very basic, and consequently didn't do a very good at protecting you from 10 | bugs. Using the new prop types in v3, you can have much more confidence that the prop types 11 | that you build are actually helping you find issues with your props. 12 | 13 | ### New Prop Types 14 | 15 | Familiarize yourself with the new prop types. You'll want to use these to build new, better 16 | prop types. 17 | 18 | You can read about them on 19 | [the documentation page](https://redux-resource.js.org/docs/extras/redux-resource-prop-types.html). 20 | 21 | ### Removed Prop Types 22 | 23 | #### `resourceIdsPropType` 24 | 25 | You can instead use the new prop type, `idPropType`, like so: 26 | 27 | ```js 28 | import PropTypes from 'prop-types'; 29 | import { idPropType } from 'redux-resource-prop-types'; 30 | 31 | PropTypes.arrayOf(idPropType); 32 | ``` 33 | 34 | #### `resourcesPropType` 35 | 36 | If you'd like to continue using the old version, here's the code: 37 | 38 | ```js 39 | const resourcesPropType = PropTypes.arrayOf( 40 | PropTypes.shape({ 41 | id: PropTypes.oneOfType([ 42 | PropTypes.string, 43 | PropTypes.number, 44 | ]) 45 | }) 46 | ); 47 | ``` 48 | 49 | Instead, we recommend using the new `resourcePropType` (note that that name is singular!) to create 50 | a more robust prop type for your resources. Then, you can use that prop type to build an array prop 51 | type: 52 | 53 | ```js 54 | const booksPropType = PropTypes.arrayOf(bookPropType); 55 | ``` 56 | 57 | #### `slicePropType` 58 | 59 | If you'd like to continue using the old version in your application, then you can copy and paste this 60 | code into your application: 61 | 62 | ```js 63 | const slicePropType = PropTypes.shape({ 64 | resources: PropTypes.object.isRequired, 65 | meta: PropTypes.object.isRequired, 66 | requests: PropTypes.object.isRequired, 67 | lists: PropTypes.object.isRequired 68 | }); 69 | ``` 70 | 71 | However, we recommend building a more robust prop type, such as: 72 | 73 | ```js 74 | import { idsPropType } from 'redux-resource-prop-types'; 75 | 76 | const booksSlicePropType = PropTypes.shape({ 77 | resources: PropTypes.objectOf(booksPropType).isRequired, 78 | meta: PropTypes.objectOf(booksMetaPropType).isRequired, 79 | requests: PropType.objectOf(booksRequestPropType).isRequired, 80 | lists: PropType.objectOf(idsPropType).isRequired, 81 | customStuff: myCustomPropType, 82 | // ...and so on 83 | }); 84 | ``` -------------------------------------------------------------------------------- /docs/extras/redux-resource-plugins.md: -------------------------------------------------------------------------------- 1 | # Redux Resource Plugins 2 | 3 | [![npm version](https://img.shields.io/npm/v/redux-resource-plugins.svg)](https://www.npmjs.com/package/redux-resource-plugins) 4 | [![gzip size](http://img.badgesize.io/https://unpkg.com/redux-resource-plugins/dist/redux-resource-plugins.min.js?compression=gzip)](https://unpkg.com/redux-resource-plugins/dist/redux-resource-plugins.min.js) 5 | 6 | These plugins can be used to augment the reducer returned by `resourceReducer`. 7 | The plugins are a collection of common patterns that you may find yourself 8 | needing when writing a CRUD application. 9 | 10 | Do you find yourself using the same plugin over and over? 11 | [Let us know](https://github.com/jamesplease/redux-resource/issues/new?title=New+plugin+suggestion), 12 | and it might find its way into this package! 13 | 14 | ### Other Guides 15 | 16 | **Old Documentation** 17 | 18 | - [2.x documentation](https://github.com/jamesplease/redux-resource/blob/33a79cfdddb0dc5dae10f7073ece28e90dbd1455/docs/extras/redux-resource-plugins.md) 19 | 20 | **Migration Guides** 21 | 22 | - [v2 to v3](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource-plugins/docs/migration-guides/2-to-3.md) 23 | 24 | ### Installation 25 | 26 | Install `redux-resource-plugins` from npm: 27 | 28 | `npm install redux-resource-plugins --save` 29 | 30 | Then, import the pieces of the package that you need: 31 | 32 | ```js 33 | import { selection } from 'redux-resource-plugins'; 34 | ``` 35 | 36 | ### Usage 37 | 38 | This library is a collection of different plugins. Refer to their individual 39 | documentation pages to learn more. 40 | 41 | - [`reset`](reset-plugin.md): This plugin provides action types 42 | that let you reset the state of an entire slice. You can also pass a list to 43 | reset the state of just that list. 44 | 45 | - [`includedResources`](included-resources-plugin.md): This plugin 46 | adds support for including multiple resource types into a single action for 47 | read requests. This can be useful if you're using GraphQL, JSON API, or normalizr. 48 | 49 | - [`httpStatusCodes`](http-status-codes-plugin.md): Add this plugin 50 | to track the HTTP status codes associated with each request. The built-in 51 | reducer behavior doesn't provide any information specific to HTTP. What this 52 | means is that if a request fails, for instance, you won't be able to tell that 53 | it failed with a 404 response. 54 | 55 | ##### Deprecated Plugins 56 | 57 | The following plugins are deprecated. There are built-in features that provide 58 | the same functionality as these plugins. 59 | 60 | - [`selection`](selection-plugin.md): This plugin allows you to 61 | maintain a list of "selected" resource IDs. If your interface displays a list 62 | of resources that the user can select to perform bulk operations on, then this 63 | might be useful to you. 64 | -------------------------------------------------------------------------------- /packages/redux-resource-xhr/src/xhr.js: -------------------------------------------------------------------------------- 1 | import xhr from 'xhr'; 2 | import qs from 'querystringify'; 3 | 4 | /* eslint no-param-reassign: 'off' */ 5 | 6 | // This is a small wrapper around `xhr`. 7 | // It has two improvements: 8 | // 1. Omit the `cb`, and a Promise will be returned instead 9 | // 2. Pass a `qs` option for query string support 10 | 11 | function buildUrl(uri, options) { 12 | let qsStringify = options.qsStringify || qs.stringify; 13 | if (options.qs) { 14 | let stringified = qsStringify(options.qs, options.qsStringifyOptions); 15 | if (stringified[0] !== '?') { 16 | stringified = `?${stringified}`; 17 | } 18 | uri += stringified; 19 | } 20 | return uri; 21 | } 22 | 23 | export default function request(uri, options, cb) { 24 | let params = {}; 25 | // This handles the `xhr(options, cb)` syntax 26 | if (typeof uri === 'object') { 27 | params = uri; 28 | } else if (typeof uri === 'string' && typeof options === 'object') { 29 | // This handles the `xhr(uri, options, cb)` syntax 30 | params = options; 31 | params.uri = uri; 32 | } else { 33 | // This handles the `xhr(uri, cb)` syntax 34 | params.uri = uri; 35 | } 36 | 37 | // This adds support for the `qs` option 38 | const urlString = params.uri ? params.uri : params.url; 39 | params.uri = buildUrl(urlString, params); 40 | 41 | if (params.url) { 42 | delete params.url; 43 | } 44 | 45 | let callback; 46 | if (typeof options === 'function') { 47 | callback = options; 48 | } else if (typeof cb === 'function') { 49 | callback = cb; 50 | } 51 | 52 | // Return the `xhr` if a callback was passed. Otherwise, a Promise is returned 53 | if (callback) { 54 | return xhr(params, callback); 55 | } else { 56 | return new Promise((resolve, reject) => { 57 | xhr(params, (err, res) => { 58 | if (err) { 59 | reject(err); 60 | } else { 61 | resolve(res); 62 | } 63 | }); 64 | }); 65 | } 66 | } 67 | 68 | // Also grabbed from xhr's source. This adds the convenience APIs; 69 | // `xhr.get()`, for instance. 70 | // Supported signatures: 71 | // 72 | // xhr[method](url, callback) 73 | // xhr[method](url, options, callback) 74 | // xhr[method](options, callback) 75 | // 76 | ['get', 'put', 'post', 'patch', 'head', 'delete'].forEach(method => { 77 | request[method === 'delete' ? 'del' : method] = function( 78 | uri, 79 | options, 80 | callback 81 | ) { 82 | let opts, cb; 83 | if (typeof uri === 'object') { 84 | opts = uri; 85 | cb = options; 86 | } else if (typeof options === 'object') { 87 | opts = Object.assign({ uri }, options); 88 | cb = callback; 89 | } else if (typeof uri === 'string' && typeof options !== 'object') { 90 | opts = { uri }; 91 | cb = options; 92 | } 93 | opts.method = method.toUpperCase(); 94 | return request(opts, cb); 95 | }; 96 | }); 97 | -------------------------------------------------------------------------------- /docs/recipes/caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | Caching server responses can improve the responsiveness of your application. 4 | 5 | Because the main Redux Resource library does not provide tools to make HTTP requests, 6 | it is not possible for the main library to provide a caching mechanism. 7 | 8 | With that said, bindings for view libraries, such as React, are the perfect place for 9 | caching to be implemented. Official React bindings for Redux Resource are in the works, and 10 | they will be built using [React Request](https://github.com/jamesplease/react-request), a powerful, 11 | declarative HTTP library for React. 12 | 13 | This recipe contains tips that could help you if you're interested in writing your own 14 | caching implementation, either by using React Request or by writing your own system. 15 | 16 | ### Caching using requests 17 | 18 | How can you know if a response has already been returned for a given request? The way 19 | that we recommend doing it is by using [request objects](../requests/request-objects.md). 20 | 21 | Here's how it works with React Request: 22 | 23 | React Request implements its own 24 | [caching system](https://github.com/jamesplease/react-request/blob/master/docs/guides/response-caching.md). 25 | Its caching is powered by a string called a 26 | ["request key"](https://github.com/jamesplease/react-request/blob/master/docs/guides/request-keys.md) based 27 | on the request configuration you pass to it. Two requests with the same key are considered identical. 28 | 29 | This automatically-generated "request key" will be used as the Redux Resource requestKey, which is 30 | what ties the two libraries together. 31 | 32 | ### Accessing data from a dynamic key 33 | 34 | Tools like `getStatus` are more difficult to use directly when using a dynamic request name. 35 | 36 | In the official React bindings for Redux Resource, the solution to this problem will be a wrapping 37 | component _around_ React Request will automatically pull the details of that request from the Redux 38 | store, and pass it to you in a render prop function. It will also pull the resources, resource meta, 39 | and lists that the request affected. 40 | 41 | Because of this, you will rely a lot less on directly using 42 | `getStatus` and `getResources` when using React Redux Resource, although they will still be 43 | there should you need them. 44 | 45 | ### The role of lists 46 | 47 | Sometimes, requests contribute to a list. For instance, if you fetch a user's favorite books, 48 | and then they create a new favorite book, you may have a list called `"favoriteBooks"`, which 49 | is what your component renders out. 50 | 51 | What do you do in this situation? Simply continue to cache at the request level. 52 | 53 | By using a cached response, you are making the claim that a response from the server for that 54 | request could not provide any more information for the list that you already have. 55 | 56 | As a result, you do not make the request, and you continue to render out the locally-cached list. 57 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/upsert-resources.js: -------------------------------------------------------------------------------- 1 | import warning from './warning'; 2 | 3 | // Add or update resources 4 | export default function upsertResources( 5 | resources, 6 | newResources, 7 | mergeResources 8 | ) { 9 | if (!newResources) { 10 | return resources; 11 | } 12 | 13 | const mergeResourcesOption = 14 | typeof mergeResources !== 'undefined' ? mergeResources : true; 15 | const shallowClone = Object.assign({}, resources); 16 | 17 | if (Array.isArray(newResources)) { 18 | newResources.forEach(resource => { 19 | const resourceIsObject = typeof resource === 'object'; 20 | const id = resourceIsObject ? resource.id : resource; 21 | 22 | // If a resource doesn't have an ID, then it cannot be tracked 23 | if (!id && id !== 0) { 24 | if (process.env.NODE_ENV !== 'production') { 25 | warning( 26 | `You attempted to update or add a resource without an ID attribute. ` + 27 | `Redux Resource requires that all resources have an ID. You should ` + 28 | `double-check your Action Creators to make sure that all entries in ` + 29 | `are either an ID or an object with an "id" attribute. ` + 30 | `For more information, refer to the documentation on resource objects at: ` + 31 | `https://redux-resource.js.org/docs/resources/resource-objects.html`, 32 | 'MISSING_ID_UPSERT' 33 | ); 34 | } 35 | 36 | return; 37 | } 38 | 39 | const resourceObj = resourceIsObject ? resource : { id: resource }; 40 | 41 | const resourceAlreadyExists = Boolean(resources[id]); 42 | 43 | // If there is no existing resource, we just add it to the resources object 44 | if (!resourceAlreadyExists) { 45 | shallowClone[id] = resourceObj; 46 | return shallowClone; 47 | } 48 | 49 | let resourceToInsert; 50 | if (mergeResourcesOption) { 51 | const currentResource = shallowClone[id]; 52 | resourceToInsert = Object.assign({}, currentResource, resourceObj); 53 | } else { 54 | resourceToInsert = resourceObj; 55 | } 56 | 57 | shallowClone[id] = resourceToInsert; 58 | }); 59 | } else { 60 | for (let id in newResources) { 61 | const resource = newResources[id]; 62 | 63 | const resourceAlreadyExists = Boolean(resources[id]); 64 | 65 | // If there is no existing resource, we just add it to the resources object 66 | if (!resourceAlreadyExists) { 67 | shallowClone[id] = resource; 68 | continue; 69 | } 70 | 71 | let resourceToInsert; 72 | if (mergeResourcesOption) { 73 | const currentResource = shallowClone[id]; 74 | resourceToInsert = Object.assign({}, currentResource, resource); 75 | } else { 76 | resourceToInsert = { 77 | ...resource, 78 | }; 79 | } 80 | 81 | shallowClone[id] = resourceToInsert; 82 | } 83 | } 84 | 85 | return shallowClone; 86 | } 87 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | * [Home](../INDEX.md) 4 | * [Introduction](introduction/README.md) 5 | * [Motivation](introduction/motivation.md) 6 | * [Core Concepts](introduction/core-concepts.md) 7 | * [Similar Projects](introduction/similar-projects.md) 8 | * [Examples](introduction/examples.md) 9 | * [Resources](resources/README.md) 10 | * [Resource Reducers](resources/resource-reducers.md) 11 | * [Resource Objects](resources/resource-objects.md) 12 | * [Meta](resources/meta.md) 13 | * [Lists](resources/lists.md) 14 | * [Modifying Resources](resources/modifying-resources.md) 15 | * [Requests](requests/README.md) 16 | * [Request Objects](requests/request-objects.md) 17 | * [Keys](requests/request-keys.md) 18 | * [Names](requests/request-names.md) 19 | * [Statuses](requests/request-statuses.md) 20 | * [Request Actions](requests/request-actions.md) 21 | * [Updating Lists](requests/updating-lists.md) 22 | * [Reading Resources](requests/reading-resources.md) 23 | * [Updating Resources](requests/updating-resources.md) 24 | * [Creating Resources](requests/creating-resources.md) 25 | * [Deleting Resources](requests/deleting-resources.md) 26 | * [Other Guides](other-guides/README.md) 27 | * [Usage With React](other-guides/usage-with-react.md) 28 | * [Tracking Request Statuses](other-guides/tracking-request-statuses.md) 29 | * [Using Request Statuses](other-guides/using-request-statuses.md) 30 | * [Custom Action Types](other-guides/custom-action-types.md) 31 | * [Migration Guides](other-guides/migration-guides.md) 32 | * [Recipes](recipes/README.md) 33 | * [Forms](recipes/forms.md) 34 | * [Canceling Requests](recipes/canceling-requests.md) 35 | * [Unauthorized Responses](recipes/unauthorized-responses.md) 36 | * [User Feedback](recipes/user-feedback.md) 37 | * [Related Resources](recipes/related-resources.md) 38 | * [Caching](recipes/caching.md) 39 | * [Ecosystem Extras](extras/README.md) 40 | * [Redux Resource Action Creators](extras/redux-resource-action-creators.md) 41 | * [Redux Resource XHR](extras/redux-resource-xhr.md) 42 | * [Redux Resource Prop Types](extras/redux-resource-prop-types.md) 43 | * [Redux Resource Plugins](extras/redux-resource-plugins.md) 44 | * [HTTP Status Codes](extras/http-status-codes-plugin.md) 45 | * [Selection](extras/selection-plugin.md) 46 | * [Reset](extras/reset-plugin.md) 47 | * [Included Resources](extras/included-resources-plugin.md) 48 | * [FAQ](faq/README.md) 49 | * [General](faq/general.md) 50 | * [State Tree](faq/state-tree.md) 51 | * [Actions](faq/actions.md) 52 | * [Lists](faq/lists.md) 53 | * [API Reference](api-reference/README.md) 54 | * [resourceReducer](api-reference/resource-reducer.md) 55 | * [getStatus](api-reference/get-status.md) 56 | * [getResources](api-reference/get-resources.md) 57 | * [upsertResources](api-reference/upsert-resources.md) 58 | * [setResourceMeta](api-reference/set-resource-meta.md) 59 | * [actionTypes](api-reference/action-types.md) 60 | * [requestStatuses](api-reference/request-statuses.md) 61 | -------------------------------------------------------------------------------- /packages/redux-resource/docs/migration-guides/1-to-2.md: -------------------------------------------------------------------------------- 1 | # Migrating 2 | 3 | This guide outlines how to migrate from `resourceful-redux@1.x` to `redux-resource@2.0.0`. 4 | 5 | ### Getting Started 6 | 7 | First, install the latest version of redux-resource. 8 | 9 | ``` 10 | npm install redux-resource@2.0.0 11 | ``` 12 | 13 | ### Unchanged Utilities 14 | 15 | Several utilities are unchanged between these releases. The first thing that you can do is 16 | update your application to use these instead of the Resourceful Redux versions. 17 | 18 | ```diff 19 | - import { getStatus, actionTypes, requestStatuses } from 'resourceful-redux'; 20 | + import { getStatus, actionTypes, requestStatuses } from 'redux-resource'; 21 | ``` 22 | 23 | Your application should continue working as expected after these changes. 24 | 25 | ### The New Reducer 26 | 27 | There are two breaking changes in the Resource Reducer: 28 | 29 | - `labels` have been renamed to be `requests` 30 | - `labels` are on longer used to track ordered lists of resources. Instead, use `lists`. 31 | 32 | Because Redux Resource is configured per-slice, you can migrate one slice at a time, 33 | rather than moving your entire application over at once. 34 | 35 | First, use the new `resourceReducer` on a single slice. 36 | 37 | ```diff 38 | - import { resourceReducer } from 'resourceful-redux'; 39 | + import { resourceReducer } from 'redux-resource'; 40 | 41 | export default resourceReducer('books'); 42 | ``` 43 | 44 | Next, update your Action Creators to specify a `request` rather than a `label`. 45 | 46 | ```diff 47 | readResources({ 48 | resourceName: 'books', 49 | - label: 'readBooks' 50 | + request: 'readBooks' 51 | }); 52 | ``` 53 | 54 | Update all usages of `getStatus` to reference the new `requests` section of the slice, 55 | rather than `labels`. 56 | 57 | ```diff 58 | - getStatus(state, 'books.labels.readBooks.status'); 59 | + getStatus(state, 'books.requests.readBooks.status'); 60 | ``` 61 | 62 | If you were using `getResources` to render out the response from this request, you will want to 63 | now also use a [list](https://redux-resource.js.org/docs/resources/lists.html). Now is also a 64 | good opportunity to switch to the new `getResources` API. The old API still works, but it will 65 | be removed in v3.0.0 sometime in early 2018. 66 | 67 | ```diff 68 | readResources({ 69 | resourceName: 'books', 70 | request: 'readBooks', 71 | + list: 'usersBooks' 72 | }); 73 | 74 | - getResources(state, 'books', 'readBooks'); 75 | + getResources(state.books, 'usersBooks'); 76 | ``` 77 | 78 | If you were using `mergeLabelIds`, be sure to change that to be `mergeListIds`: 79 | 80 | 81 | ```diff 82 | readResources({ 83 | resourceName: 'books', 84 | request: 'readBooks', 85 | list: 'usersBooks', 86 | - mergeLabelIds: false, 87 | + mergeListIds: false, 88 | }); 89 | ``` 90 | 91 | ### Conclusion 92 | 93 | That's the extent of the changes in the main library. You will want to make sure that any 94 | plugins or libraries that you are using have also been updated to support these changes. 95 | -------------------------------------------------------------------------------- /packages/redux-resource-action-creators/src/index.js: -------------------------------------------------------------------------------- 1 | import { actionTypes } from 'redux-resource'; 2 | import warning from './utils/warning'; 3 | 4 | const _createAction = function( 5 | actionProperties = {}, 6 | actionDefaults = {}, 7 | type 8 | ) { 9 | if (process.env.NODE_ENV !== 'production') { 10 | if (actionProperties.type || actionDefaults.type) { 11 | warning( 12 | 'It looks like you provided a `type` property for an action created using redux-resource-' + 13 | 'action-creators. This library sets the `type` for you; your value has been ignored. ' + 14 | 'For more, refer to the documentation: ' + 15 | 'https://redux-resource.js.org/docs/extras/redux-resource-action-creators.html' 16 | ); 17 | } 18 | } 19 | 20 | return { 21 | ...actionDefaults, 22 | ...actionProperties, 23 | type, 24 | }; 25 | }; 26 | 27 | const createActionCreators = (crudType, actionDefaults = {}) => { 28 | const { resourceName, resourceType } = actionDefaults; 29 | const typeToUse = resourceType || resourceName; 30 | const uppercaseCrud = 31 | typeof crudType === 'string' ? crudType.toUpperCase() : ''; 32 | const isValidCrudType = 33 | uppercaseCrud === 'UPDATE' || 34 | uppercaseCrud === 'DELETE' || 35 | uppercaseCrud === 'READ' || 36 | uppercaseCrud === 'CREATE'; 37 | 38 | if (process.env.NODE_ENV !== 'production') { 39 | if (!isValidCrudType) { 40 | warning( 41 | `You supplied an invalid crudType of ${crudType} to createActionCreators.` + 42 | `Please verify you've supplied one of: ['read', 'update', 'delete', 'create'].` + 43 | 'For more, refer to the documentation: ' + 44 | 'https://redux-resource.js.org/docs/extras/redux-resource-action-creators.html' 45 | ); 46 | } 47 | 48 | if (typeof typeToUse !== 'string') { 49 | warning( 50 | `The value of "resourceType" that you passed to createActionCreators was ` + 51 | `not a string. The resourceType must be a string. You should check ` + 52 | `your redux-resource-action-creators configuration.` + 53 | 'For more, refer to the documentation: ' + 54 | 'https://redux-resource.js.org/docs/extras/redux-resource-action-creators.html' 55 | ); 56 | } 57 | } 58 | 59 | return { 60 | pending: actionProperties => 61 | _createAction( 62 | actionProperties, 63 | actionDefaults, 64 | actionTypes[`${uppercaseCrud}_RESOURCES_PENDING`] 65 | ), 66 | idle: actionProperties => 67 | _createAction( 68 | actionProperties, 69 | actionDefaults, 70 | actionTypes[`${uppercaseCrud}_RESOURCES_IDLE`] 71 | ), 72 | succeeded: actionProperties => 73 | _createAction( 74 | actionProperties, 75 | actionDefaults, 76 | actionTypes[`${uppercaseCrud}_RESOURCES_SUCCEEDED`] 77 | ), 78 | failed: actionProperties => 79 | _createAction( 80 | actionProperties, 81 | actionDefaults, 82 | actionTypes[`${uppercaseCrud}_RESOURCES_FAILED`] 83 | ), 84 | }; 85 | }; 86 | 87 | export default createActionCreators; 88 | -------------------------------------------------------------------------------- /docs/requests/request-statuses.md: -------------------------------------------------------------------------------- 1 | # Request Statuses 2 | 3 | A request has a "status" associated with it, which represents the state that 4 | the request is in. There are four statuses: 5 | 6 | - `IDLE`: The request hasn't started yet 7 | - `PENDING`: The request is in flight, and hasn't finished 8 | - `FAILED`: The request finished, and failed 9 | - `SUCCEEDED`: The request finished, and was successful 10 | 11 | A request has exactly one status at any given time. Here is an example request 12 | object that is in a pending state: 13 | 14 | ```js 15 | { 16 | requestKey: 'createBook', 17 | status: 'PENDING' 18 | } 19 | ``` 20 | 21 | The request status values are exported as 22 | [`requestStatuses`](../api-reference/request-statuses.md). 23 | 24 | > Note: the request status is different from an HTTP status code. A request that 25 | represents an HTTP request could _also_ have a status code associated with it, such 26 | as 200 or 404. 27 | 28 | ### Why Use Request Statuses 29 | 30 | The status of a request is useful for showing feedback to users of the application. 31 | When a request is in the pending state, you can display a loading indicator, and when it is 32 | failed, you can show an error message. 33 | 34 | ### The Status Lifecycle 35 | 36 | The request status can be thought of as a cyclical lifecycle. Every request starts as `IDLE`. 37 | When the request begins, it moves to `PENDING`. 38 | 39 | When it resolves, it becomes `SUCCEEDED` or `FAILED` depending on the outcome. 40 | 41 | And lastly, there may be a time when you no longer need to display to the user that the 42 | request has succeeded or failed. At this time, you can make the request `IDLE` again. 43 | 44 | If the application makes the same request again, the process will repeat itself. 45 | 46 | Not every request needs to go through the entire lifecycle. Sometimes, requests will 47 | sit indefinitely in the `SUCCEEDED` state after they resolve. Other times, you 48 | may need to "reset" the state back to `IDLE`. 49 | 50 | > Note: when a request is aborted, we recommend that you move it back to the `IDLE` 51 | > state. Webapps typically abort requests because the user no longer needs to know 52 | > if the request succeeds or fails. 53 | 54 | ### Providing More Detailed Information 55 | 56 | The request status provides a coarse representation of the status of the request, 57 | but you will frequently need more granular information. Why did the request fail? 58 | Was the resource not found, or did the backend have an error? Is the user logged out, 59 | or did they lose their network connection? 60 | 61 | You can store additional data on a request to capture information like this. 62 | 63 | For instance, if your requests represent RESTful HTTP calls, then you could add the 64 | HTTP status code to the request. Or, if your requests represent gRPC calls, then 65 | you could place the gRPC error code onto the request, too. 66 | 67 | The request definition is intentionally flexible. It allows you store any additional 68 | information that you need. This enables Redux Resource's requests to work with any 69 | networking layer: RESTful HTTP endpoints, GraphQL, gRPC, web sockets, or whatever else 70 | you may be using to transfer data. 71 | -------------------------------------------------------------------------------- /docs/extras/included-resources-plugin.md: -------------------------------------------------------------------------------- 1 | # Included Resources Plugin 2 | 3 | APIs frequently support returning multiple resource types in a single request. For 4 | instance, an endpoint may allow you to fetch an author as well as that author's 5 | books. In this example, `authors` and `books` are two different resource types. 6 | 7 | This plugin is designed to allow you to dispatch a single action that includes 8 | multiple types. It is optimized to receive normalized data, such as what is 9 | returned from [`normalizr`](https://github.com/paularmstrong/normalizr). 10 | 11 | ### Usage 12 | 13 | Add this plugin when you call 14 | [`resourceReducer`](../api-reference/resource-reducer.md). Be sure to add it to 15 | the "primary" resource slice, as well as to the included resource slices. 16 | 17 | ```js 18 | import { resourceReducer } from 'redux-resource'; 19 | import { includedResources } from 'redux-resource-plugins'; 20 | 21 | const authorReducer = resourceReducer('authors', { 22 | plugins: [includedResources] 23 | }); 24 | 25 | const booksReducer = resourceReducer('books', { 26 | plugins: [includedResources] 27 | }); 28 | ``` 29 | 30 | This plugin doesn't come with any custom action types. Instead, it changes the 31 | way the state is transformed with the built-in successful create, update or read CRUD 32 | [action type](../api-reference/action-types.md): `CREATE_RESOURCES_SUCCEEDED`, `UPDATE_RESOURCES_SUCCEEDED` and 33 | `READ_RESOURCES_SUCCEEDED`. 34 | 35 | When your actions have an `includedResources` object, they will be added to the 36 | appropriate slices. 37 | 38 | An example of using `includedResources` is the following: 39 | 40 | ```js 41 | import { actionTypes } from 'redux-resource'; 42 | import store from './store'; 43 | 44 | store.dispatch({ 45 | type: actionTypes.READ_RESOURCES_SUCCEEDED, 46 | resourceType: 'authors', 47 | resources: [{ 48 | id: 10, 49 | name: 'Sarah' 50 | }], 51 | includedResources: { 52 | // Resources are key'd off by their name. 53 | // The value can be an Object or an Array. 54 | 55 | // Here we pass books as an Object. `normalizr` returns 56 | // a shape like this. 57 | books: { 58 | 23: { 59 | id: 23, 60 | name: 'Some book' 61 | }, 62 | 100: { 63 | id: 50, 64 | name: 'Another book' 65 | } 66 | }, 67 | 68 | // Notice here that comments is an Array. This format works, too. 69 | comments: [ 70 | { 71 | id: 23, 72 | name: 'Great author!' 73 | }, 74 | { 75 | id: 100, 76 | name: 'One of my favorite authors' 77 | } 78 | ] 79 | } 80 | }); 81 | ``` 82 | 83 | Be sure to use `includedResources` on _every_ resource slice that can appear in `includedResources`. 84 | In the above example, we would want to include the plugin for our `authors`, `books`, and `comments`. 85 | 86 | This plugin will respect the `mergeResources` and `mergeMeta` action properties. 87 | 88 | ### Related Reading 89 | 90 | Not every API returns included resources in a normalized manner, so a different plugin may be more 91 | appropriate for certain backends. As an example, JSON API does not provide included resources in a 92 | format that can be interpreted by this plugin. 93 | 94 | For more on this subject, refer to the [Related Resources recipe](../recipes/related-resources.md). 95 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jamesplease2@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /packages/redux-resource/test/unit/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { resourceReducer } from '../../../src'; 2 | import { resetCodeCache } from '../../../src/utils/warning'; 3 | 4 | describe('reducer', function() { 5 | beforeEach(() => { 6 | resetCodeCache(); 7 | }); 8 | 9 | it('should be a function', () => { 10 | const reducer = resourceReducer('hellos'); 11 | expect(reducer).to.be.a('function'); 12 | }); 13 | 14 | it('should handle an action type that does not exist', () => { 15 | stub(console, 'error'); 16 | const reducer = resourceReducer('hellos'); 17 | 18 | const state = { 19 | sandwiches: true, 20 | hungry: [], 21 | }; 22 | 23 | const reduced = reducer(state, { type: 'does_not_exist' }); 24 | expect(reduced).to.equal(state); 25 | expect(console.error.callCount).to.equal(0); 26 | }); 27 | 28 | it('should warn when no resourceType is passed', () => { 29 | stub(console, 'error'); 30 | resourceReducer(); 31 | expect(console.error.callCount).to.equal(1); 32 | }); 33 | 34 | it('should warn when a non-string resourceType is passed', () => { 35 | stub(console, 'error'); 36 | resourceReducer({}); 37 | expect(console.error.callCount).to.equal(1); 38 | }); 39 | 40 | it('should warn when a resourceName is passed', () => { 41 | stub(console, 'error'); 42 | const reducer = resourceReducer('books'); 43 | expect(console.error.callCount).to.equal(0); 44 | 45 | reducer( 46 | {}, 47 | { 48 | type: 'UPDATE_RESOURCES_PENDING', 49 | resources: [1], 50 | resourceName: 'books', 51 | } 52 | ); 53 | expect(console.error.callCount).to.equal(1); 54 | }); 55 | 56 | describe('reserved action types', () => { 57 | it('should warn with REQUEST_IDLE', () => { 58 | stub(console, 'error'); 59 | const reducer = resourceReducer('books'); 60 | expect(console.error.callCount).to.equal(0); 61 | 62 | reducer( 63 | {}, 64 | { 65 | type: 'REQUEST_IDLE', 66 | } 67 | ); 68 | expect(console.error.callCount).to.equal(1); 69 | }); 70 | 71 | it('should warn with REQUEST_PENDING', () => { 72 | stub(console, 'error'); 73 | const reducer = resourceReducer('books'); 74 | expect(console.error.callCount).to.equal(0); 75 | 76 | reducer( 77 | {}, 78 | { 79 | type: 'REQUEST_PENDING', 80 | } 81 | ); 82 | expect(console.error.callCount).to.equal(1); 83 | }); 84 | 85 | it('should warn with REQUEST_SUCCEEDED', () => { 86 | stub(console, 'error'); 87 | const reducer = resourceReducer('books'); 88 | expect(console.error.callCount).to.equal(0); 89 | 90 | reducer( 91 | {}, 92 | { 93 | type: 'REQUEST_SUCCEEDED', 94 | } 95 | ); 96 | expect(console.error.callCount).to.equal(1); 97 | }); 98 | 99 | it('should warn with REQUEST_FAILED', () => { 100 | stub(console, 'error'); 101 | const reducer = resourceReducer('books'); 102 | expect(console.error.callCount).to.equal(0); 103 | 104 | reducer( 105 | {}, 106 | { 107 | type: 'REQUEST_FAILED', 108 | } 109 | ); 110 | expect(console.error.callCount).to.equal(1); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /docs/resources/lists.md: -------------------------------------------------------------------------------- 1 | # Lists 2 | 3 | Applications frequently need to keep track of groupings of a resource type. 4 | That's what lists are for. A list is an array of resource IDs. You can 5 | make as many lists as you would like for each resource type. 6 | 7 | Here are two situations when you might find lists useful: 8 | 9 | - Keeping track of a server-side sorted array of resources 10 | - If your UI allows user to "select" resources on your interface by clicking a 11 | checkbox, then you could store the "selected" IDs in a list 12 | 13 | The above isn't meant to be exhaustive; it is simply to give you an idea of 14 | the kinds of situations where lists can be useful. Any time that you need a 15 | subsection of your resources (whether it needs to be ordered or not), you 16 | should use lists. 17 | 18 | The following shows what two lists look like in a resource slice: 19 | 20 | ```js 21 | { 22 | resourceType: 'books', 23 | lists: { 24 | favorites: [1, 100, 52, 1230], 25 | selected: [24, 13, 1] 26 | }, 27 | // ...there are other things in a resource slice, as well. But this guide will 28 | // focus on lists. 29 | } 30 | ``` 31 | 32 | ### List Names 33 | 34 | A good list name is short and descriptive. Here are some examples: 35 | 36 | - mostRecent 37 | - searchResults 38 | - favorites 39 | - selected 40 | 41 | You could include the resource type in the name, too (i.e. "selectedBooks"). 42 | 43 | Sometimes, you may need to use a dynamic list name. A dynamic list name is a string that has 44 | a variable component to it, such as: 45 | 46 | ```js 47 | `authorsBooks:${authorId}` 48 | ``` 49 | 50 | It is okay to use dynamic names, but keep in mind that static names are simpler to manage, so 51 | you might find them preferable to use over dynamic list names. 52 | 53 | Sometimes, you may think that you need a dynamic list name when another approach is better. 54 | 55 | For instance, the above list, `authorsBooks:${authorId}` may represent the list of books that 56 | were written by the author with the ID `authorId`. In this situation, you could instead 57 | store that list of IDs on the author object itself, like a [foreign key](https://en.wikipedia.org/wiki/Foreign_key) 58 | in a relational database. 59 | 60 | This might look like: 61 | 62 | ```js 63 | // An author 'resource object' 64 | { 65 | id: 'a39cva22', 66 | name: 'Jane Austen', 67 | books: ['103', '10', '129903'] 68 | } 69 | ``` 70 | 71 | If the backend does not return this data with the author's primary data, then you may choose to store 72 | the list of IDs on the author's [resource metadata](meta.md) instead: 73 | 74 | ```js 75 | // An author metadata object 76 | { 77 | readStatus: 'SUCCEEDED', 78 | updateStatus: 'IDLE', 79 | createStatus: 'IDLE', 80 | deleteStatus: 'IDLE', 81 | books: ['103', '10', '129903'] 82 | } 83 | ``` 84 | 85 | ### Accessing the Resources in a List 86 | 87 | To retrieve the resources in a list, you can use 88 | [`getResources`](../api-reference/get-resources.md). 89 | 90 | ```js 91 | import { getResources } from 'redux-resource'; 92 | import store from './my-redux-store'; 93 | 94 | const state = store.getState(); 95 | const favoriteBooks = getResources(state.books, 'favorites'); 96 | ``` 97 | 98 | ### Updating Lists 99 | 100 | In the [next guide](modifying-resources.md), we will cover how you can 101 | modify data within a resource slice, including lists. 102 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/get-resources.js: -------------------------------------------------------------------------------- 1 | import warning from './warning'; 2 | 3 | function resourceArrayToObject(arr) { 4 | return arr.reduce((memo, r) => { 5 | memo[r.id] = r; 6 | return memo; 7 | }, {}); 8 | } 9 | 10 | // Returns a list of resources by IDs or list name 11 | export default function(resourceSlice, filter, options) { 12 | if (process.env.NODE_ENV !== 'production') { 13 | // We use duck typing to try and differentiate between a user passing the entire state tree 14 | // in as `resourceSlice` (which was the original getResources API). 15 | // eslint-disable-next-line no-inner-declarations 16 | function validateSlice(slice = {}) { 17 | return ( 18 | // eslint-disable-next-line no-prototype-builtins 19 | slice.hasOwnProperty('resources') && 20 | // eslint-disable-next-line no-prototype-builtins 21 | slice.hasOwnProperty('meta') && 22 | // eslint-disable-next-line no-prototype-builtins 23 | slice.hasOwnProperty('requests') && 24 | // eslint-disable-next-line no-prototype-builtins 25 | slice.hasOwnProperty('lists') 26 | ); 27 | } 28 | 29 | if ( 30 | arguments.length === 3 && 31 | !validateSlice(resourceSlice) && 32 | // We assume that undefined means that a slice is undefined, rather than the entire state tree being undefined. 33 | typeof resourceSlice !== 'undefined' 34 | ) { 35 | warning( 36 | `You called getResources with an argument signature that was removed in ` + 37 | `v3.0.0 of Redux Resource. The old signature accepted three arguments, with the first being the all of the store's ` + 38 | `state. The new signature of this function requires passing a resource slice as the first argument. Please ` + 39 | `update your code to use the new signature. For more information, reference the documentation at ` + 40 | `https://redux-resource.js.org/docs/api-reference/get-resources.html\n\n` + 41 | `Also, the migration guide to Redux Resource v3 can be found at: ` + 42 | `https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource/docs/migration-guides/2-to-3.md`, 43 | 'DEPRECATED_GET_RESOURCES_SIGNATURE' 44 | ); 45 | } 46 | } 47 | 48 | let byId; 49 | if (options && options.byId) { 50 | byId = options.byId; 51 | } 52 | 53 | const noResultsReturn = byId ? {} : []; 54 | 55 | if (!resourceSlice) { 56 | return noResultsReturn; 57 | } 58 | 59 | const resources = resourceSlice.resources; 60 | let idsList; 61 | 62 | if (typeof filter === 'function' || !filter) { 63 | const appliedFilter = filter ? filter : () => true; 64 | const resourcesList = Object.values(resources).filter(resource => 65 | appliedFilter(resource, resourceSlice.meta[resource.id], resourceSlice) 66 | ); 67 | 68 | return byId ? resourceArrayToObject(resourcesList) : resourcesList; 69 | } else if (typeof filter === 'string') { 70 | // This conditional handles the situation where `filter` is an list name 71 | const list = resourceSlice.lists[filter]; 72 | if (!list) { 73 | return noResultsReturn; 74 | } 75 | 76 | idsList = list; 77 | } else { 78 | idsList = filter; 79 | } 80 | 81 | if (!(idsList && idsList.length)) { 82 | return noResultsReturn; 83 | } 84 | 85 | const resourcesList = idsList.map(id => resources[id]).filter(Boolean); 86 | 87 | return byId ? resourceArrayToObject(resourcesList) : resourcesList; 88 | } 89 | -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/docs/old-versions/1.md: -------------------------------------------------------------------------------- 1 | # Resourceful Prop Types (v1.0.0) 2 | 3 | [![gzip size](http://img.badgesize.io/https://unpkg.com/resourceful-prop-types/dist/resourceful-prop-types.min.js?compression=gzip)](https://unpkg.com/resourceful-prop-types/dist/resourceful-prop-types.min.js) 4 | 5 | A collection of [prop-types](https://github.com/facebook/prop-types) objects. 6 | 7 | ### Installation 8 | 9 | Install `resourceful-prop-types` with npm: 10 | 11 | `npm install resourceful-prop-types --save` 12 | 13 | Then, import the prop types that you need: 14 | 15 | ```js 16 | import { resourcesPropType } from 'resourceful-prop-types'; 17 | ``` 18 | 19 | ### Usage 20 | 21 | There are four prop types. If you're using React, refer to the [Typechecking with 22 | PropTypes](https://facebook.github.io/react/docs/typechecking-with-proptypes.html) 23 | guide on how to use them. If you're not using React, refer to the documentation 24 | of the [prop-types](https://github.com/facebook/prop-types) library. 25 | 26 | #### `slicePropType` 27 | 28 | Validates the state slice that a 29 | [`resourceReducer`](/docs/api-reference/resource-reducer.md) is associated with. 30 | It's called a "slice" because the most common usage of Resourceful Redux 31 | involves using 32 | [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html). 33 | 34 | This checks that the object has a shape like: 35 | 36 | ```js 37 | { 38 | resources: {}, 39 | meta: {}, 40 | labels: {} 41 | } 42 | ``` 43 | 44 | ```js 45 | import { slicePropType } from 'resourceful-prop-types'; 46 | 47 | MyComponent.propTypes = { 48 | books: slicePropType 49 | }; 50 | 51 | mapStateToProps(state) { 52 | return { 53 | books: state.books 54 | }; 55 | } 56 | ``` 57 | 58 | #### `resourceIdsPropType` 59 | 60 | Validates an Array of resource IDs. Sometimes, it's convenient to store a 61 | reference to a subset of resources as an array of IDs. For instance, Labels 62 | store their associated resources as an array of IDs. In your own application 63 | code, you might choose to represent a list of "selected" resources as an array 64 | of IDs. 65 | 66 | > Tip: This prop type requires that your IDs be either strings or numbers. 67 | 68 | ```js 69 | import { resourceIdsPropType } from 'resourceful-prop-types'; 70 | 71 | MyComponent.propTypes = { 72 | selectedBookIds: resourceIdsPropType 73 | }; 74 | 75 | mapStateToProps(state) { 76 | return { 77 | selectedBookIds: state.books.selectedBookIds 78 | }; 79 | } 80 | ``` 81 | 82 | #### `resourcesPropTypes` 83 | 84 | Validates an array of resources. Resources are JavaScript objects that have an 85 | `id` attribute. For more, see 86 | [the Resource objects guide](/docs/resources/resource-objects.md). 87 | 88 | ```js 89 | import { resourcesPropType } from 'resourceful-prop-types'; 90 | 91 | MyComponent.propTypes = { 92 | books: resourcesPropType 93 | }; 94 | 95 | mapStateToProps(state) { 96 | return { 97 | books: state.books.resources 98 | }; 99 | } 100 | ``` 101 | 102 | #### `statusPropType` 103 | 104 | Validates the object returned by [`getStatus`](/docs/api-reference/get-status.md). 105 | 106 | ```js 107 | import { getStatus } from 'resourceful-redux'; 108 | import { statusPropType } from 'resourceful-prop-types'; 109 | 110 | MyComponent.propTypes = { 111 | booksReadStatus: statusPropType 112 | }; 113 | 114 | mapStateToProps(state) { 115 | return { 116 | bookReadStatus: getStatus(state, 'books.meta.23.readStatus'); 117 | }; 118 | } 119 | ``` -------------------------------------------------------------------------------- /packages/redux-resource-prop-types/docs/old-versions/2.md: -------------------------------------------------------------------------------- 1 | # Redux Resource Prop Types (v2.0.0) 2 | 3 | > This is the documentation for an old version of this library. We encourage you to upgrade 4 | > to the latest version. 5 | > 6 | > Migration guides: 7 | > - [v2 to v3](https://github.com/jamesplease/redux-resource/blob/master/packages/redux-resource-prop-types/docs/migration-guides/2-to-3.md) 8 | 9 | A collection of [prop-types](https://github.com/facebook/prop-types) objects. 10 | 11 | ### Installation 12 | 13 | Install `redux-resource-prop-types` with npm: 14 | 15 | `npm install redux-resource-prop-types --save` 16 | 17 | Then, import the prop types that you need: 18 | 19 | ```js 20 | import { resourcesPropType } from 'redux-resource-prop-types'; 21 | ``` 22 | 23 | ### Usage 24 | 25 | There are four prop types. If you're using React, refer to the [Typechecking with 26 | PropTypes](https://facebook.github.io/react/docs/typechecking-with-proptypes.html) 27 | guide on how to use them. If you're not using React, refer to the documentation 28 | of the [prop-types](https://github.com/facebook/prop-types) library. 29 | 30 | #### `slicePropType` 31 | 32 | Validates the state slice that a 33 | [`resourceReducer`](/docs/api-reference/resource-reducer.md) is associated with. 34 | It's called a "slice" because the most common usage of Redux Resource 35 | involves using 36 | [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html). 37 | 38 | This checks that the object has a shape like: 39 | 40 | ```js 41 | { 42 | resources: {}, 43 | meta: {}, 44 | requests: {}, 45 | lists: {} 46 | } 47 | ``` 48 | 49 | ```js 50 | import { slicePropType } from 'redux-resource-prop-types'; 51 | 52 | MyComponent.propTypes = { 53 | books: slicePropType 54 | }; 55 | 56 | mapStateToProps(state) { 57 | return { 58 | books: state.books 59 | }; 60 | } 61 | ``` 62 | 63 | #### `resourceIdsPropType` 64 | 65 | Validates an Array of resource IDs. Sometimes, it's convenient to store a 66 | reference to a subset of resources as an array of IDs. For instance, lists 67 | store their associated resources as an array of IDs. In your own application 68 | code, you might choose to represent a list of "selected" resources as an array 69 | of IDs. 70 | 71 | > Tip: This prop type requires that your IDs be either strings or numbers. 72 | 73 | ```js 74 | import { resourceIdsPropType } from 'redux-resource-prop-types'; 75 | 76 | MyComponent.propTypes = { 77 | selectedBookIds: resourceIdsPropType 78 | }; 79 | 80 | mapStateToProps(state) { 81 | return { 82 | selectedBookIds: state.books.selectedBookIds 83 | }; 84 | } 85 | ``` 86 | 87 | #### `resourcesPropTypes` 88 | 89 | Validates an array of resources. Resources are JavaScript objects that have an 90 | `id` attribute. For more, see 91 | [the Resource objects guide](/docs/resources/resource-objects.md). 92 | 93 | ```js 94 | import { resourcesPropType } from 'redux-resource-prop-types'; 95 | 96 | MyComponent.propTypes = { 97 | books: resourcesPropType 98 | }; 99 | 100 | mapStateToProps(state) { 101 | return { 102 | books: state.books.resources 103 | }; 104 | } 105 | ``` 106 | 107 | #### `statusPropType` 108 | 109 | Validates the object returned by [`getStatus`](/docs/api-reference/get-status.md). 110 | 111 | ```js 112 | import { getStatus } from 'redux-resource'; 113 | import { statusPropType } from 'redux-resource-prop-types'; 114 | 115 | MyComponent.propTypes = { 116 | booksReadStatus: statusPropType 117 | }; 118 | 119 | mapStateToProps(state) { 120 | return { 121 | bookReadStatus: getStatus(state, 'books.meta.23.readStatus'); 122 | }; 123 | } 124 | ``` -------------------------------------------------------------------------------- /packages/redux-resource/test/unit/reducers/plugins.js: -------------------------------------------------------------------------------- 1 | import { resourceReducer, actionTypes } from '../../../src'; 2 | import { resetCodeCache } from '../../../src/utils/warning'; 3 | 4 | describe('reducer', function() { 5 | beforeEach(() => { 6 | resetCodeCache(); 7 | }); 8 | 9 | it('should warn when a bad plugin is initialized', () => { 10 | stub(console, 'error'); 11 | 12 | resourceReducer('hellos', { 13 | plugins: [ 14 | () => { 15 | // Intentionally blank 16 | }, 17 | ], 18 | }); 19 | 20 | expect(console.error.callCount).to.equal(1); 21 | }); 22 | 23 | it('should handle a plug-in on a built-in type', () => { 24 | const reducer = resourceReducer('hellos', { 25 | plugins: [ 26 | () => (state, action) => { 27 | if (action.type === actionTypes.READ_RESOURCES_SUCCEEDED) { 28 | return { 29 | ...state, 30 | pizza: 'yum', 31 | }; 32 | } 33 | 34 | return state; 35 | }, 36 | ], 37 | }); 38 | 39 | const reduced = reducer(undefined, { 40 | type: 'READ_RESOURCES_SUCCEEDED', 41 | resourceType: 'hellos', 42 | resources: [3], 43 | }); 44 | 45 | expect(reduced).to.deep.equal({ 46 | resourceType: 'hellos', 47 | resources: { 48 | 3: { id: 3 }, 49 | }, 50 | meta: { 51 | 3: { 52 | createStatus: 'IDLE', 53 | readStatus: 'SUCCEEDED', 54 | updateStatus: 'IDLE', 55 | deleteStatus: 'IDLE', 56 | }, 57 | }, 58 | lists: {}, 59 | requests: {}, 60 | pizza: 'yum', 61 | }); 62 | }); 63 | 64 | it('should handle a plug-in on a custom type', () => { 65 | const reducer = resourceReducer('hellos', { 66 | plugins: [ 67 | () => (state, action) => { 68 | if (action.type === 'SANDWICHES_ARE_GOOD') { 69 | return { 70 | ...state, 71 | tastiness: action.tastiness, 72 | }; 73 | } 74 | 75 | return state; 76 | }, 77 | ], 78 | }); 79 | 80 | const reduced = reducer(undefined, { 81 | type: 'SANDWICHES_ARE_GOOD', 82 | resourceType: 'hellos', 83 | tastiness: 'quite', 84 | }); 85 | 86 | expect(reduced).to.deep.equal({ 87 | resourceType: 'hellos', 88 | resources: {}, 89 | meta: {}, 90 | lists: {}, 91 | requests: {}, 92 | tastiness: 'quite', 93 | }); 94 | }); 95 | 96 | it('should handle multiple plug-ins on a custom type, from right to left', () => { 97 | const reducer = resourceReducer('hellos', { 98 | plugins: [ 99 | () => (state, action) => { 100 | if (action.type === 'SANDWICHES_ARE_GOOD') { 101 | return { 102 | ...state, 103 | tastiness: true, 104 | }; 105 | } 106 | 107 | return state; 108 | }, 109 | () => (state, action) => { 110 | if (action.type === 'SANDWICHES_ARE_GOOD') { 111 | return { 112 | ...state, 113 | tastiness: false, 114 | }; 115 | } 116 | 117 | return state; 118 | }, 119 | ], 120 | }); 121 | 122 | const reduced = reducer(undefined, { 123 | type: 'SANDWICHES_ARE_GOOD', 124 | resourceType: 'hellos', 125 | }); 126 | 127 | expect(reduced).to.deep.equal({ 128 | resourceType: 'hellos', 129 | resources: {}, 130 | meta: {}, 131 | lists: {}, 132 | requests: {}, 133 | tastiness: true, 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /packages/redux-resource-plugins/src/http-status-codes.js: -------------------------------------------------------------------------------- 1 | import { actionTypes, setResourceMeta } from 'redux-resource'; 2 | 3 | // End actions can be failed, succeeded, or idle. Idle should be dispatched 4 | // when the request is aborted (with a status code of 0). 5 | const createEndActions = [ 6 | actionTypes.CREATE_RESOURCES_FAILED, 7 | actionTypes.CREATE_RESOURCES_SUCCEEDED, 8 | actionTypes.CREATE_RESOURCES_IDLE, 9 | ]; 10 | 11 | const readEndActions = [ 12 | actionTypes.READ_RESOURCES_FAILED, 13 | actionTypes.READ_RESOURCES_SUCCEEDED, 14 | actionTypes.READ_RESOURCES_IDLE, 15 | ]; 16 | 17 | const updateEndActions = [ 18 | actionTypes.UPDATE_RESOURCES_FAILED, 19 | actionTypes.UPDATE_RESOURCES_SUCCEEDED, 20 | actionTypes.UPDATE_RESOURCES_IDLE, 21 | ]; 22 | 23 | const deleteEndActions = [ 24 | actionTypes.DELETE_RESOURCES_FAILED, 25 | actionTypes.DELETE_RESOURCES_SUCCEEDED, 26 | actionTypes.DELETE_RESOURCES_IDLE, 27 | ]; 28 | 29 | // This sets a new meta property on resource and request metadata: `statusCode`. 30 | // This will be equal to the last status code for a request 31 | export default function httpStatusCodes(resourceType) { 32 | return function(state, action) { 33 | const typeToCheck = action.resourceType || action.resourceName; 34 | if (typeToCheck !== resourceType) { 35 | return state; 36 | } 37 | 38 | const isCreateEndAction = createEndActions.indexOf(action.type) !== -1; 39 | const isReadEndAction = readEndActions.indexOf(action.type) !== -1; 40 | const isUpdateEndAction = updateEndActions.indexOf(action.type) !== -1; 41 | const isDeleteEndAction = deleteEndActions.indexOf(action.type) !== -1; 42 | 43 | if ( 44 | !isCreateEndAction && 45 | !isReadEndAction && 46 | !isUpdateEndAction && 47 | !isDeleteEndAction 48 | ) { 49 | return state; 50 | } 51 | 52 | const statusCode = 53 | typeof action.statusCode === 'number' ? action.statusCode : null; 54 | const resources = action.resources; 55 | 56 | let request; 57 | 58 | const naiveKey = action.requestKey || action.request; 59 | if (naiveKey && typeof naiveKey === 'string') { 60 | request = naiveKey; 61 | } 62 | 63 | let newRequests, newMeta, idList; 64 | if (resources) { 65 | idList = resources.map(r => { 66 | if (typeof r === 'object') { 67 | return r.id; 68 | } else { 69 | return r; 70 | } 71 | }); 72 | } else { 73 | idList = []; 74 | } 75 | 76 | if (request) { 77 | const existingRequest = state.requests[request] || {}; 78 | 79 | newRequests = { 80 | ...state.requests, 81 | [request]: { 82 | ...existingRequest, 83 | statusCode, 84 | }, 85 | }; 86 | } else { 87 | newRequests = { ...state.requests }; 88 | } 89 | 90 | if (idList.length) { 91 | let metaPrefix; 92 | if (isCreateEndAction) { 93 | metaPrefix = 'create'; 94 | } else if (isReadEndAction) { 95 | metaPrefix = 'read'; 96 | } else if (isUpdateEndAction) { 97 | metaPrefix = 'update'; 98 | } else if (isDeleteEndAction) { 99 | metaPrefix = 'delete'; 100 | } 101 | 102 | newMeta = setResourceMeta({ 103 | meta: state.meta, 104 | newMeta: { 105 | [`${metaPrefix}StatusCode`]: statusCode, 106 | }, 107 | resources: idList, 108 | mergeMeta: true, 109 | }); 110 | } else { 111 | newMeta = state.meta; 112 | } 113 | 114 | return { 115 | ...state, 116 | requests: newRequests, 117 | meta: newMeta, 118 | }; 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /packages/redux-resource/src/utils/reducer-generator.js: -------------------------------------------------------------------------------- 1 | import setResourceMeta from '../utils/set-resource-meta'; 2 | import initialResourceMetaState from './initial-resource-meta-state'; 3 | import warning from './warning'; 4 | 5 | // This helper is used to simplify non-success reducers. Because non-success 6 | // reducers don't modify the data – ever – it simplifies the scope of what can 7 | // change. 8 | // Basically, two things can change: 9 | // 10 | // 1. Request status for resource IDs in `meta`, if IDs are passed in 11 | // 2. Request status for a named request 12 | // 13 | // A named request's IDs don't change, and neither does the resource. Consequently, 14 | // this helper completely defines all of the ways in which the non-success reducers 15 | // can change the state. 16 | export default function(crudAction, requestStatus) { 17 | return function(state, action, { initialResourceMeta } = {}) { 18 | const resources = action.resources; 19 | const mergeMeta = action.mergeMeta; 20 | 21 | let requestKey, requestName; 22 | if (action.request && typeof action.request === 'string') { 23 | requestKey = requestName = action.request; 24 | } 25 | if (action.requestKey && typeof action.requestKey === 'string') { 26 | requestKey = action.requestKey; 27 | } 28 | if (action.requestName && typeof action.requestName === 'string') { 29 | requestName = action.requestName; 30 | } 31 | 32 | let idList; 33 | if (resources) { 34 | idList = resources.map(r => { 35 | if (typeof r === 'object') { 36 | return r.id; 37 | } else { 38 | return r; 39 | } 40 | }); 41 | } else { 42 | idList = []; 43 | } 44 | 45 | const statusAttribute = `${crudAction}Status`; 46 | let newRequests, newMeta, newLists; 47 | 48 | if (!requestKey && !idList.length) { 49 | if (process.env.NODE_ENV !== 'production') { 50 | warning( 51 | `A Redux Resource action of type ${action.type} was dispatched ` + 52 | `without a "requestKey" or "resources" array. Without one of these ` + 53 | `values, Redux Resource cannot track the request status for this ` + 54 | `CRUD operation. You should check your Action Creators. Read more about ` + 55 | `request tracking at: https://redux-resource.js.org/docs/other-guides/tracking-request-statuses.html`, 56 | 'NO_OP_NON_SUCCESS_ACTION' 57 | ); 58 | } 59 | 60 | return state; 61 | } 62 | 63 | if (requestKey) { 64 | const existingRequest = state.requests[requestKey] || {}; 65 | 66 | const newRequest = { 67 | ...existingRequest, 68 | ...action.requestProperties, 69 | requestKey, 70 | resourceType: action.resourceType || action.resourceName, 71 | status: requestStatus, 72 | }; 73 | 74 | if (requestName) { 75 | newRequest.requestName = requestName; 76 | } 77 | 78 | newRequests = { 79 | ...state.requests, 80 | [requestKey]: newRequest, 81 | }; 82 | } else { 83 | newRequests = state.requests; 84 | } 85 | 86 | // Lists only change when a request succeeds 87 | newLists = { ...state.lists }; 88 | 89 | if (idList.length) { 90 | newMeta = setResourceMeta({ 91 | meta: state.meta, 92 | newMeta: { [statusAttribute]: requestStatus }, 93 | resources: idList, 94 | mergeMeta, 95 | initialResourceMeta: { 96 | ...initialResourceMetaState, 97 | ...initialResourceMeta, 98 | }, 99 | }); 100 | } else { 101 | newMeta = state.meta; 102 | } 103 | 104 | return { 105 | ...state, 106 | requests: newRequests, 107 | lists: newLists, 108 | meta: newMeta, 109 | }; 110 | }; 111 | } 112 | --------------------------------------------------------------------------------