-
-
Save jslatts/1c5d4d46b6e5b0ac0e917fa3b6f7968f to your computer and use it in GitHub Desktop.
// Example of usage | |
import { selectors } from './rootReducer'; | |
import { selectors } from '../../reducers/rootReducer'; | |
const mapStateToProps = (state: State, ownProps: any) => ({ | |
theseObjects: selectors.getTheseObjects(state), | |
thoseObjects: selectors.getThoseObjects(state), | |
showSomethinginUi: selectors.getSomethingFromUiSelectors(state), | |
}); | |
... MORE CODE ... |
// @flow | |
// Helper to wrap a redux selector with a predetermined state slice function | |
'use strict'; | |
export const bindSelectors = (slicer: (any) => any, selectors: *) => { | |
const keys = Object.keys(selectors); | |
const boundMethods = {}; | |
keys.forEach(k => { | |
boundMethods[k] = fullState => selectors[k](slicer(fullState)); | |
}); | |
return boundMethods; | |
}; | |
export default bindSelectors; |
// Example root reducer | |
import { bindSelectors } from './bindSelectors'; | |
import ui, { selectors as uiSelectors } from './ui'; | |
import theseObjects, { selectors as theseObjectSelectors } from './theseObjectsReducers'; | |
import thoseObjects, { selectors as thoseObjectSelectors } from './thoseObjectsReducers'; | |
// State shape looks like | |
const defaultState = { | |
ui, | |
theseObjects, | |
thoseObjects, | |
}; | |
... | |
REDUCER CODE GOES HERE | |
... | |
export default rootReducer; | |
// bindSelectors binds a state slice to the sub-reducer's selectors | |
export const selectors = { | |
...bindSelectors(state => state.theseObjects, theseObjectSelectors), | |
...bindSelectors(state => state.thoseObjects, thoseObjectSelectors), | |
...bindSelectors(state => state.ui, uiSelectors), | |
}; |
// Example theseObjects reducer | |
... | |
REDUCER CODE GOES HERE | |
... | |
export default theseObjects; | |
// Seletors in this file do not have to worry about what the state of the whole state tree looks like | |
// They just operate within the same scope of their corresponding reducers | |
export const selectors = { | |
getFilteredObjects: (state: State) => state.filter(t => t.someFlag === true), | |
}; |
Awesome. Here's my take allowing for additional arguments from the component.
bindSelectors.js
export const bindSelectors = (slicer, selectors) => (
Object.keys(selectors).reduce((boundMethods, methodName) => ({
...boundMethods,
[methodName]: (state, ...args) => selectors[methodName](slicer(state), ...args),
}), {})
);
app.js
const mapStateToProps = (state, ownProps) => ({
theseObjects: selectors.getTheseObjects(state, ownProps.params.someId),
});
Thank you very much for this!
I tried to make a TypeScript version of this, but in redefining the selector functions when wrapping them in the object in the parent reducer it lost all type safety (and editor hinting with it) - there wasn't any way I could find to retain things like the number of parameters in the functions, the parameter types, etc once the selector functions were recreated.
So I ended up reversing the direction - instead of the parent reducer doing the wrapping, the child reducer is given a slicer function by the parent that will give the child the right slice of the parent's state. The child reducer then defines its selectors in terms of the parent's state based on that slicer function.
I've written up the process and created a sample set of reducers demonstrating how it works when there are multiple depths at https://gitlab.com/dawnmist/redux-typescript-selectors
@jslatts the selectors
object that is exported from rootReducer.js
could introduce a subtle bug if two slices of state define selectors with the same name.
Instead of spreading all selectors for each slice into a single object, you might consider namespacing them
export const selectors = {
theseObjects: bindSelectors(state => state.theseObjects, theseObjectSelectors),
thoseObjects: bindSelectors(state => state.thoseObjects, thoseObjectSelectors),
ui: bindSelectors(state => state.ui, uiSelectors),
};
Inspired by http://stackoverflow.com/a/41860269/190766