redux-pathspace
v8.0.0-beta.0
Published
> Quickly & easily create path-based namespaces to add actions that map to reducers
Downloads
9
Maintainers
Readme
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 asfoo.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 specifiedpath
will be used as a prefix for theaction.type
.parentPath
- If supplied, this must be a validpath
returned from a previouscreateNamespace
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 whenactionType
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 callingcreateNamespace
as the first argument--you can think of it as the "slice" of state specified by the givenpath
. All reducers get passed a second argument which is the value supplied toactionCreator(value)
. All reducers will receive the full state supplied bystore.getState()
as the third argument. For example:myReducer(path, payload, fullState) => payload
.meta
- Object to set on theaction.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 returnundefined
. 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 withstore
andactionCreators
(seesetStore
method below). Obviously, if you didn't usesetStore
, then both of those arguments will beundefined
. The function that's returned gets passed the arguments supplied toyourActionCreator(...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 isundefined
.- 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 fromcreateStore
. 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