selector-action
v1.2.0
Published
State-aware Redux actions with Reselect syntax.
Downloads
94
Readme
State-aware Redux actions with Reselect syntax
Basic Usage
import selectorAction from 'selector-action';
// ...
export const reloadActiveItem = selectorAction(
activeIdSelector,
(activeId) => ({
type: 'RELOAD_ACTIVE_ITEM',
promise: fetch(`//website.com/items/${activeId}`),
}),
);
Background
selector-action simplifies a common Redux pattern: actions that depend on the current Redux state. For example, when you're reloading an "active" item's data with an API call:
// The active ID is in the Redux state, but we need it to make our API call. Sad!
export function reloadActiveItem(activeId) {
return {
type: 'RELOAD_ACTIVE_ITEM',
promise: fetch(`//website.com/items/${activeId}`),
};
}
In this example, we have to pass activeId
to the action creator, even though it’s actually
part of the Redux state! This pollutes our React components with unnecessary props, and makes
the action creators more complicated than they need to be. We could try using redux-thunk
to eliminat the argument, but that adds a lot of boilerplate:
export function reloadActiveItem() {
// We can access the current state by returning a thunk.
return (dispatch, getState) => {
const state = getState();
const activeId = activeIdSelector(state);
// Now we just have to dispatch the action...
return dispatch({
type: 'RELOAD_ACTIVE_ITEM',
promise: fetch(`//website.com/items/${activeId}`),
});
};
}
We have to get the state, call selectors on it, and finally dispatch
the resulting action.
And if we wanted to test this action creator, there's even more boilerplate - we'd have to mock
getState
, then spy on the dispatch
function...it's not pretty.
Using selector-action
selectorAction
makes this pattern a breeze. Instead of doing the dispatching
and the action-creating ourselves, we’ll pass in a selector and an action creator function that uses
the selector's results. The end result is a reloadActiveItem
function that's exactly equivalent
to the previous example.
import selectorAction from 'selector-action';
// ...
export const reloadActiveItem = selectorAction(
activeIdSelector,
(activeId) => ({
type: 'RELOAD_ACTIVE_ITEM',
promise: fetch(`//website.com/items/${activeId}`),
}),
);
As you can see, this looks a lot like the syntax of Reselect. You pass in one or more selectors, then an action creator that takes the selectors’ return values as its arguments. The result of this action creator function is what's dispatched.
Testing
selectorAction
exposes the original action creator as an
originalActionCreator
property of the generated function. This allows for testing the
underlying action creator without using a fake state or stubbing dispatch
.
describe('reloadActiveItem', () => {
it('returns an action with the correct type', () => {
const activeId = 1234;
const action = reloadActiveItem.originalActionCreator(activeId);
expect(action.type).to.eql('RELOAD_ACTIVE_ITEM');
});
});
middleware
If you're already using redux-thunk
, you don't need to
do anything to start using selectorAction
- it’s fully compatible with the thunk middleware.
If you don’t want to use redux-thunk
(and there are some
good
reasons
to not want to
), then selector-action
provides a middleware for you to use instead:
import { createStore, applyMiddleware } from 'redux';
import selectorActionMiddleware from 'selector-action/middleware';
import rootReducer from './reducers/index';
const store = createStore(
rootReducer,
applyMiddleware(selectorActionMiddleware),
);
selector-action
's middleware only runs when selectorAction
s are dispatched. Similar to
redux-thunk
, it plays nice with other common middlewares like
redux-pack
.
Advanced features
state
as an argument
In addition to the results of the selectors, state
is passed in as the last argument to the action
creator. If you don't specify any selectors, then state
is the only argument. This can be useful
for converting actions that already use the getState
trick shown above.
export const reloadActiveItem = selectorAction((state) => {
const activeId = activeIdSelector(state);
return {
type: 'RELOAD_ACTIVE_ITEM',
promise: fetch(`//website.com/items/${activeId}`),
};
});
Arrays of selectors
Like Reselect, you can pass in an array of selectors instead of passing them as separate arguments. Here is a contrived example demonstrating this feature.
export const awesomeAction = selectorAction(
[fooSelector, barSelector],
(foo, bar) => ({ type: 'AWESOME!', payload: { foo, bar } }),
);
selectorAction
with arguments
You'll probably run across a case where an action creator needs a mix of selector results and
regular arguments to compute an action. For instance, let’s say that the user has entered a new name
for the current active item. Your reducer will need both the new name AND the active item’s ID
to compute a new state. This can be done by wrapping selectorAction
in a higher-order function.
export function setActiveItemName(newName) {
return selectorAction(activeIdSelector, (activeId) => ({
type: 'SET_ITEM_NAME',
payload: { activeId, newName },
}));
}
License
MIT