8.0.0-beta.0 • Published 6 years ago

redux-pathspace v8.0.0-beta.0

Weekly downloads
5
License
MIT
Repository
github
Last release
6 years ago

redux-pathspace

Quickly & easily create path-based namespaces to add actions that map to reducers

(live demo)

Usage

import { createStore, applyMiddleware } from 'redux';
import { promiseMiddleware } from 'some-promise-middleware';
import { createNamespace, createReducer } from 'redux-pathspace';
import { api } from './my-api';

const initialState = { foo: 'bar', baz: [] };

const store = createStore(
  createReducer(initialState),
  initialState,
  applyMiddleware(promiseMiddleware),
);

store.getState(); // -> { foo: 'bar', baz: [] }

// no side effects:
const foo = createNamespace('foo');

// use the default reducer (just returns the payload)
const fooAnyActionCreator = foo.mapActionToReducer('ANY');
fooAnyActionCreator('xyz'); // -> { type: 'foo:ANY', payload: 'xyz', meta: {} }
store.dispatch(fooAnyActionCreator('xyz'));
store.getState(); // -> { foo: 'xyz', baz: [] }
foo.examine(store.getState()); // -> 'xyz'

// specify your own reducer...this one ignores the payload and alwyays returns 'hello'
const fooHelloActionCreator = foo.mapActionToReducer('HELLO', () => 'hello');
fooHelloActionCreator() // -> { type: 'foo:HELLO', payload: undefined, meta: {} }
store.dispatch(fooHelloActionCreator());
store.getState(); // -> { foo: 'hello', baz: [] }
foo.examine(store.getState()); // -> 'hello'

// a duplicate action name wihtin a namespace will throw
foo.mapActionToReducer('HELLO'); // -> ERROR: action type already exists for `foo` namespace

// manage side-effects
const baz = createNamespace('baz');

function getBazItems() {
  // some promise
  return api.getBazItems.then(items => items);
}

const getBazItemsActionCreator = baz.mapActionToReducer('GET_ITEMS').withSideEffect(getBazItems);
store.dispatch(getBazItemsActionCreator());
store.getState(); // -> { foo: 'hello', baz: ['Item 1', 'Item 2', 'Item 3'] }
baz.examine(store.getState()); // ['Item 1', 'Item 2', 'Item 3' ]

// paths can also be created for array indexes
const bazIndex1 = createNamespace(1, baz); // passing a namespace as a second argument will create a sub-path
bazIndex1.examine(store.getState()); // -> 'Item 2'
bazIndex1ActionCreator = bazIndex1.mapActionToReducer('FOO');
bazIndex1ActionCreator(); // -> { type: baz[1]:FOO, payload: undefined, meta: {} }

API

import { createNamespace, createReducer, mapNamespaces, setStore, createPathspace } from 'redux-pathspace';

createNamespace(path: string|array|number|func, parentPath: path);

Returns a new namespace object. You can think of a namespace as a self-contained unit (closure) that keeps track of action/reducer pairs and any additional meta data.

  • path - The path to target on the redux state. If it's a string, it can be dot notation such as foo.bar.baz. If it's an array it can consist of strings and numbers without dot notated strings ['foo', 'bar', 0]. If it's a number (whether standalone or in an array), then it specifies an array index within your given path. The specified path will be used as a prefix for the action.type.
  • parentPath - If supplied, this must be a valid path returned from a previous createNamespace call, which will act as the parent path (behind the scenes, this accesses the previous Ramda lens and composes those lenses together to create sub-paths).
  • returns - (object) - A new namespace.

const namespace = createNamespace(...);

namespace.mapActionToReducer(actionType: string, reducer: func, meta: object);

  • actionType - The name of your action that will get prefixed with the parent path to avoid collisions.
  • reducer - Reducer to be called when actionType is dispatched. If not supplied it uses a default reducer with the signature: defaultReducer = (state, payload) -> payload. All supplied reducers get passed the value of of the state based on the path specified when calling createNamespace as the first argument--you can think of it as the "slice" of state specified by the given path. All reducers get passed a second argument which is the value supplied to actionCreator(value). All reducers will receive the full state supplied by store.getState() as the third argument. For example: myReducer(path, payload, fullState) => payload.
  • meta - Object to set on the action.meta property. Defaults to {}.
  • returns - (function) - A new actionCreator.

namespace.examine(item: object|array|string);

Function that peers into the specified depth of an object based on the path of the namespace and returns its value.

  • item - Object/array/string to inspect. If there's no matching path, it will return undefined. Otherwise, it will return the value of the specified path.

namespace.lens;

Retrieves the underlying Ramda lens being used by redux-pathspace for the given path should you need it for your own purposes.

const actionCreator = namespace.mapActionToReducer('FOO');

A new action creator that, when called, returns a flux standard action (FSA) that looks like: { type, payload, meta }. Prior to returning the FSA object, the action creator will check for any side-effects, and pass all arguments supplied to actionCreator to the side-effect function you specify when calling actionCreator.withSideEffect(func). The return value of the side-effect function call will get set on the payload of the FSA. More on the side-effect API below.

actionCreator.withSideEffect(sideEffect: func);

Adds a side effect to the actionCreator.

  • sideEffect - This function should return another function. The signature is (store, actionCreator) => (...args) => { ... }. The first function gets called with store and actionCreators (see setStore method below). Obviously, if you didn't use setStore, then both of those arguments will be undefined. The function that's returned gets passed the arguments supplied to yourActionCreator(...args) to execute any side effects before getting set onto action.payload.

The sideEffect you specify gets called like this:

function actionCreator(...args) {
  return {
    type: 'SOME_ACTION',
    payload: sideEffect(store, actionCreators)(...args),
    meta: {},
  };
}

Notice store is passed as the first argument to the function provided to withSideEffect. If you used setStore (see documentation below), store will get passed to all sideEffect functions you pass to withSideEffect. The rationale behind this is that your reducers should be as simple and pure as possible--ideally, simple enough to where they return primitive values in most cases, and are completely unaware of the shape of the rest of your state. When you need to update other parts of the state in order to properly set the portion of the state your reducer is concerned with, then that is a side-effect. Therefore store gets passed as the first argument to sideEffect so any other updates to the state can be completely transparent when you call store.dispatch(actionCreator('foo')).

The actionCreators argument passed after store is usually object of action creators you specified when calling setStore for convenience--although it could technically be anything (i.e. a getter function that retrieves your action creators, etc.). See the setStore documentation below for more information.

Again, if you didn't let redux-pathspace know about your store by using setStore, then store and actionCreators will both be undefined and unavailable to your sideEffect functions (unless you manually pass it in your action creators).

const reducer = createReducer(initialState: string|array|number|object);

  • initialState - Used to store the supplied initial state which is returned whenever the state passed to a reducer is undefined.
  • returns - (function) - A "root" reducer which should get passed to redux's createStore.

mapNamespaces(initialState: object|array|string);

  • initialState - If an object, will deeply map namespaces to each key in your object. If an array, will deeply traverse your array and create a namespace for each index (see more on arrays below). If a string, will create a namespace for each index in the string.

Note: Array values

If any of the values in the initialState passed to mapNamespaces are arrays (or just plain array itself), a new function will be created for that key that takes one argument--the array's index you want to target. When called, it returns a namespace for that specific index. In addition, all the namespace methods/properties are mapped onto the function, so you don't have to target a specific index. You can just use the normal namespace methods/properties as you would for non-arrays.

Additionally, mapNamespaces will recursively walk down arrays provided and if an object is found, it will assume all objects within that array will have the same shape, and create a namespace for any index in that array that matches that shape. If it finds other arrays nested within your array, it will map those arrays (and nested objects) as well.

Here is a usage example:

import { createNamespace, mapNamespaces, createReducer } from 'redux-pathspace';
import { createStore } from 'redux';

const initialState = { someKey: 'someValue', myArr: ['foo', 'bar'], arrWithObjects: [{ name: 'John' }]};
const namespaces = mapNamespaces(initialState);
const store = createStore(createReducer(initialState), initialState);

console.log(typeof namespaces.someKey); // -> 'object'
console.log(typeof namespaces.myArr); // -> 'function'

namespaces.myArr(1).examine(initialState); // -> bar
namespaces.myArr(10).examine(initialState); // -> undefined
namespaces.arrWithObjects(0).examine(initialState); // -> { name: 'John' }
namespaces.arrWithObjects(0).name.examine(initialState); //  -> 'John'
namespaces.arrWithObjects(42).name.examine(initialState); // -> undefined

const newNameActionCreator = namespaces.arrWithObjects(42).name.mapActionToReducer('NEW_NAME');
store.dispatch(newNameActionCreator('Lizzie');
namespaces.arrWithObjects(42).name.examine(store.getState()); // -> 'Lizzie'

setStore(store: object, actionCreators: any);

This function essentially makes your redux store available to redux-pathspace. The motivation for this API method was to make store.dispatch available to the function you pass to withSideEffect without having to pass it manually each time you create a side-effect that dispatches other actions before updating the state. This gives you the power to focus on small "slices" of the state in your reducers--keeping them simple and pure--while at the same time updating other parts of the state if you need to (by dispatching actions that handle those other parts, thus letting each reducer do its "job" for each part of the state you're concerned with). This way, you can use something like mapNamespaces to create namespaces for each part of your state (no matter how deep) and return simple values in your reducers, without worrying about the larger shape of your state...without limiting your ability to affect other parts of the state in a controlled, predictable way.

Optionally, it takes a second argument--an object containing your action creators. This is added for convenience so your action creators can be passed to your side-effects and used with store.dispatch without having to import or require your action creators everywhere you define your side-effects. This will be undefined if you did not pass a second argument to setStore.

  • object - A redux store returned from createStore. Returns the same redux store you pass it.

Example usage:

import { createStore } from 'redux';
import { createReducer, setStore } from 'redux-pathspace';
import actionCreators from './action-creators';

const initialState = { foo: 'bar', baz: [] };

export const store = setStore(createStore(createReducer(initialState), initialState), actionCreators);

createPathspace();

The redux-pathspace module automatically calls createPathspace() which creates a closure and exports all of the above API methods. If, however, you need to create that closure yourself, you can use this method instead of the exported methods.

This method is typically useful for when you're doing hot module reloading (HMR), which in some cases can call your namespace creators (via createNamespace or mapNamespaces), which will throw an error. With createPathspace, you can ensure you get a fresh closure so your namespace creators re-create your namespaces successfully on HMR.

Install

With npm installed, run

$ npm install --save redux-pathspace

Acknowledgements

As noted above, this library uses Ramda lenses under the hood. The required Ramda functions are bundled with the distribution instead of requiring users of this lib to download the entire Ramda library as a dependency.

License

MIT

8.0.0-beta.0

6 years ago

7.0.1

6 years ago

7.0.0

6 years ago

6.1.0

6 years ago

6.0.0

6 years ago

5.2.0

6 years ago

5.1.1

6 years ago

5.1.0

6 years ago

5.0.3

6 years ago

5.0.2

6 years ago

5.0.1

6 years ago

5.0.0

6 years ago

4.0.2

6 years ago

4.0.1

6 years ago

4.0.0

6 years ago

3.0.0

6 years ago

2.1.1

6 years ago

2.1.0

6 years ago

2.0.4

6 years ago

2.0.3

6 years ago

2.0.2

6 years ago

2.0.1

6 years ago

2.0.0

6 years ago

1.0.4

6 years ago

1.0.3

6 years ago

1.0.2

6 years ago

1.0.1

6 years ago