Skip to content

Instantly share code, notes, and snippets.

@jslatts
Last active November 14, 2024 16:58
Show Gist options
  • Save jslatts/1c5d4d46b6e5b0ac0e917fa3b6f7968f to your computer and use it in GitHub Desktop.
Save jslatts/1c5d4d46b6e5b0ac0e917fa3b6f7968f to your computer and use it in GitHub Desktop.
Utility to bind selectors to a slice of state. Helpful for keeping things DRY when colocating selectors and reducers
// 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),
};
@jslatts
Copy link
Author

jslatts commented Mar 29, 2017

@jabacchetta
Copy link

jabacchetta commented Aug 19, 2017

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),
});

@dawnmist
Copy link

dawnmist commented May 12, 2018

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

@SteveByerly
Copy link

@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),
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment