diff --git a/README.md b/README.md index bb971c9..463c666 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -# Entities reducer - -Redux high order reducer for normalized `flux-standard-action`s +# Entities Reducer [![Build Status][build-badge]][build] [![Code Coverage][coverage-badge]][coverage] @@ -10,11 +8,27 @@ Redux high order reducer for normalized `flux-standard-action`s [![Watch on GitHub][github-watch-badge]][github-watch] [![Star on GitHub][github-star-badge]][github-star] +This package is a high-order reducer that updates state using entities from [normalizr](https://github.com/paularmstrong/normalizr). +By default, it expects the action to have a property of `entities` or to follow the [`flux-standard-action` spec](https://github.com/acdlite/flux-standard-action). +However, it is possible to pass a data resolver ([see Parameters](#parameter-data-resolver)) if your case doesn't match either of those. + +## Why +**Why does this package exist?** -Allows for updates to entities in state. Accepts custom reducers to further control. +One of the best things to store in redux is data from server requests. Additionally, working with items stored in redux is best done when the data is normalized. +To simplify the storing of the data, this package will handle updating state with fresh normalized data. + +Entities Reducer is a high-order reducer, which means it will accept more reducers for further customizations. +The custom reducers are passed directly to `combineRecuders` from redux and should be passed into `entities-reducers` with the same format. -## Usage +## Installation +``` +npm install --save entities-reducer +--- or --- +yarn add entities-reducer +``` +Then add to your root reducer: ```javascript import { combineReducers } from 'redux'; import entitiesReducer from 'entities-reducer'; @@ -28,6 +42,29 @@ const rootReducer = combineReducers({ export default rootReducer; ``` +## Parameters + +```javascript +entitiesReducer(reducers, { dataResolver }) +``` + +### (#parameters-reducers) Reducers +Reducers are passed directly into `combineReducers` from redux, after the entities have been updated in state. It is called with the updated state and immediately returned. + +### (#parameters-data-resolver) dataResolver +The data resolver is a lookup function that is passed the action and returns the entities object to use while updating. +If the data resolver returns a falsy value the `entities-reducer` will skip process and move directly to handling the custom reducers. +Below is a customer dataResolver example, or you can checkout the [default resolver](src/index.js). +**Example** +```javascript +const customDataResolver = (action) => { + if (action.error) { + return false; + } + return action.data.extra.normalized; +} +``` + [build]: https://travis-ci.org/kwelch/entities-reducer [build-badge]: https://img.shields.io/travis/kwelch/entities-reducer.svg?style=flat-square diff --git a/package.json b/package.json index 498fbcc..8925dd8 100644 --- a/package.json +++ b/package.json @@ -45,5 +45,12 @@ "es6": true, "jest": true } + }, + "peerDependencies": { + "redux": "^3.3.0" + }, + "dependencies": { + "flux-standard-action": "^1.1.0", + "redux": "3.0.3" } } diff --git a/src/__snapshots__/index.spec.js.snap b/src/__snapshots__/index.spec.js.snap index 6f3f5d5..29881b2 100644 --- a/src/__snapshots__/index.spec.js.snap +++ b/src/__snapshots__/index.spec.js.snap @@ -4,6 +4,32 @@ Object { } `; +exports[`entitiesReducer should allow for custom data resolver 1`] = ` +Object { + "users": Object { + "1": Object { + "firstName": "Kyle", + "id": 1, + "lastName": "Welch", + "middleName": "Ryan", + }, + }, +} +`; + +exports[`entitiesReducer should allow for non-fsa actions 1`] = ` +Object { + "users": Object { + "1": Object { + "firstName": "Kyle", + "id": 1, + "lastName": "Welch", + "middleName": "Ryan", + }, + }, +} +`; + exports[`entitiesReducer should reduce entities from payload 1`] = ` Object { "users": Object { diff --git a/src/index.js b/src/index.js index 538509a..4b5e587 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,6 @@ +import { isFSA } from 'flux-standard-action'; +import { combineReducers } from 'redux'; + const updateEntity = (state, entities) => { return Object.assign({}, state, entities); }; @@ -6,16 +9,25 @@ const updateEntities = (state, key, entities) => { return Object.assign({}, state, { [key]: updateEntity(state[key], entities[key]) }); }; -export default (reducers) => (state = {}, action) => { - const { payload } = action; +const defaultDataResolver = (action) => { + if (isFSA(action)) { + const { payload } = action; + if (payload && !payload.error && payload.entities) { + return payload.entities; + } + return null; + } + return action.entities; +}; + +export default (reducers, { dataResolver } = { dataResolver: defaultDataResolver }) => (state = {}, action) => { let newState = state; - if (payload && !payload.error && payload.entities) { - const { entities } = payload; + const entities = dataResolver(action); + if (entities && typeof entities === 'object') { newState = Object.keys(entities).reduce((acc, key) => updateEntities(acc, key, entities), newState); } - newState = Object.keys(reducers).reduce((acc, key) => { - const reducerReturn = reducers[key](acc[key], action); - return Object.assign({}, acc, { [key]: reducerReturn }); - }, newState); + if (reducers && typeof reducers === 'object' && Object.keys(reducers).length > 0) { + return combineReducers(reducers)(newState, action); + } return newState; }; diff --git a/src/index.spec.js b/src/index.spec.js index 3b02cc2..80ff127 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -110,13 +110,14 @@ describe('entitiesReducer', () => { it('should allow customer reducers ', () => { const customUserReducer = (state = {}, action) => { - switch(action.type) { + switch (action.type) { case "ENTITY/DELETE": { const newState = Object.assign({}, state); delete newState[action.payload.result]; return newState; } } + return state; }; let newState = entitiesReducer({ users: customUserReducer, })(newState, { payload: { @@ -139,4 +140,46 @@ describe('entitiesReducer', () => { }); expect(newState).toMatchSnapshot(); }); + + it('should allow for non-fsa actions', () => { + let newState = entitiesReducer({})(initialState, { + entities: { + users: { + 1: { + firstName: "Kyle", + id: 1, + lastName: "Welch", + middleName: "Ryan", + }, + }, + }, + result: 1, + type: "ENTITY/ENTITY_NORMALIZE", + }); + expect(newState).toMatchSnapshot(); + }); + + + it('should allow for custom data resolver', () => { + const dataResolver = (action) => { + return action.deeplyNested.notEntities; + }; + let newState = entitiesReducer({}, { dataResolver })(initialState, { + deeplyNested: { + notEntities: { + users: { + 1: { + firstName: "Kyle", + id: 1, + lastName: "Welch", + middleName: "Ryan", + }, + }, + }, + result: 1, + }, + type: "ENTITY/ENTITY_NORMALIZE", + }); + expect(newState).toMatchSnapshot(); + }); }); diff --git a/yarn.lock b/yarn.lock index 9f8138d..039a5a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1388,6 +1388,14 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" +flux-standard-action@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/flux-standard-action/-/flux-standard-action-1.1.0.tgz#114a014e70c8830505129511e043880f0961fb38" + dependencies: + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.issymbol "^4.0.1" + for-in@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8" @@ -2322,6 +2330,18 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + +lodash.issymbol@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.issymbol/-/lodash.issymbol-4.0.1.tgz#04ad41d96f3f4f399c37dd4fcf3c1b6901e16116" + lodash.keys@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" @@ -2933,6 +2953,10 @@ redeyed@~1.0.0: dependencies: esprima "~3.0.0" +redux@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.0.3.tgz#cf60cc323ca00fcd15fe76197232df3dc32f568f" + regenerate@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260"