Scoped Selectors for Redux Modules

Tandem.ly
tandemly
Published in
8 min readDec 4, 2016

--

Randy Coulman had a recent series of posts related to organizing Redux related code which was a great read and you should really go take a moment to look them over:

That series ended up covering a topic that has been very poignant for me recently. Namely, the reusability of selectors when using Redux in applications with more complex state and decomposed reducers (using combineReducers() vs a single, top-level reducer).

Specifically, I’ve been trying to determine how to have selectors, defined parallel with their related reducer, that work on both the localized state and the global state.

Selectors

Selectors are functions that help decouple view components, specifically container components, from the shape of the state they access. They keep us from having to modify our view components any time we decide to change the shape and structure of our reducers.

For example, for a redux module that works with todo items, we might define the following:

// redux-modules/todos/reducer.js 
const initialState = {
allIds: [],
todosById: {}
};
// state will be the slice of state for this reducer export default (state = initialState, action) { /* ... */ }// redux-modules/todos/selectors.js
export const allTodoIds = (state) => state.allIds;
export const allTodos = (state) => state.allIds.map(id => state.todosById[id]);
export const getTodo = (state, id) => state.todosById[id];

That assumption for the state being passed to our selectors is the same slice of state received by the reducer — it’s localized to the slice of state dealing with todos.

Scoping Selectors

The problem: Since our selectors are written to be localized, like our reducer, we have to couple our application’s overall state shape to our components when using them in thunk action creators or in mapStateToProps() since both of those cases only receives the global application state.

For example,

// containers/TodoListContainer 
import * as selectors from 'redux-modules/todos/selectors';
//...
@connect((state) => ({
// we're coupling our view to our state shape here
// if the application change's where 'todo's are mounted
// we have to change all uses of our selector across components
ids: selectors.allTodoIds(state.todos)
})) class TodoListContainer extends Component {
render() {
return <TodoList {...this.props} />;
}
}

or, in asynchronous thunk action creators:

// redux-modules/todos/actions.js 
export const updateTodo = (id) => (dispatch, getState) => {
dispatch(/* REQUEST_ACTION */);
fetch(/*...*/).then(result => {
// state may have change by this point
const ids = allTodoIds(getState().todos);
//...
});
}

In the first example above, the component has to know where todos are mounted in the application state; and in the async action creator example, our action creator has to know that information, too.

This seems like fairly tight coupling between the application our components and our redux modules.

  1. Our redux module shouldn’t have to know where the application is mounting it within the global state tree.
  2. Components shouldn’t have to know that information, either.

Our application is responsible for determining the overall shape of our redux state when creating the store. It is also responsible for knowing where each reducer is mounted in the state tree.

import todoReducer from 'redux-modules/todos'; 
// ...other reducer imports
// App creates store shape by mounting reducers at specific points
const store = createStore(combineReducers({
todos: todoReducer,
other: otherReducer,
//...
}));

So,…

  1. How can we keep from coupling our view to our state shape and keep from specifically passing nested state objects directly.
  2. How do we write selectors that can work locally; but also operate on global state when needed.
  3. Who should own the knowledge of state shape and where specific reducers are mounted within a decomposed state? The redux modules? The application? Some new third-party module?

Decoupling

Decoupling is good. We want to define clear module boundaries across the application. We’d like to be able to reorganize our Redux state shape without affecting the components that access it using selectors.

To decouple the selectors, we would like the following:

  • localized selectors should expect to receive just their local slice of state to operate on.
  • our components, using mapStateToProps() or async thunk actions shouldn't have to know about the overall application state shape
  • we want to be able to just pass the global state object to selectors keeping us from hard coding explicit paths to various state mount points.

Let’s take a simple selector from our above example:

const getTodo = (state, id) => state.todosById[id];

We don’t want to have to write a global version of every selector we have just to address this problem.

// localized selector 
const getTodo = (state, id) => state.todosById[id];
// global selector
const getTodoGlobal = (state, id) => state.todos.todosById[id];

In his examples, Dan Abramov pulls those global selectors up to the application reducer level, defining them using delegation:

// appReducer.js 
import * as fromTodos from './todosReducer'
export const allTodos = state => fromTodos.allTodos(state.todos)

The advantages of the delegation approach are:

  • it keeps localized selectors decoupled from the application state
  • encapsulates the knowledge about where those localized states are mounted in the appropriate location (with the app reducer), and
  • keeps global selectors decoupled from the internal shape of the localized state slice of its various reducers by delegating to the localized selectors.

The disadvantages come at scale, where:

  • you end up defining lots of globalized selectors when you use more reducers to create a decomposed application state tree.
  • you end up with circular dependencies when you need to pull in a globalized selector for async thunk style action creators at the level of the reducers.

We can easily solve disadvantage #1 by providing a utility to generate the globalized selectors using the localized selectors (which we’ll cover in the next section)

Solving the issue of acyclic dependencies in #2 is another problem. If the application depends on the todos module to define the global selectors but then our todos module depends on the application’s globalized selectors for use in async thunk actions we end up with a dependency cycle: application -> todos -> application, which we'd like to avoid.

Generating global selectors: composing a solution

We can use a higher order function and composition to create globally scoped selectors.

// Given a `path` and `selector`, return a version of that 
// selector that works with the global state
const fromRoot = (path, selector) => (state, ...args) => selector(_.get(state, path), ...args);

fromRoot() is a higher order function, in that it takes a localized selector and the path location in the global state where that local state slice is mounted and returns a new version of that selector that can be passed the global state. We also allow for selectors that take multiple arguments as well.

We’re using lodash#get here to access the given string path within the global state.

For example:

// our local selector in redux module 
const getTodo = (state, id) => state.todosById[id];
// version of getTodo that works using global state const globalGetTodo = fromRoot('todos', getTodo);

It would be nice if we could do this for an entire set of selectors, rather than individual selectors. Let’s create a function that will “globalize” a set of selectors for a given reducer.

const globalizeSelectors = (selectors, path) => { 
return Object.keys(selectors).reduce((final, key) => {
final[key] = fromRoot(path, selectors[key]);
return final;
}, {});
};
// elsewhere...
import { allTodoIds, allTodos, getTodo } from 'redux-modules/todos/selectors';
const selectors = globalizeSelectors({ allTodoIds, allTodos, getTodo }, 'todos');

Our globalizeSelectors() takes advantage of our fromRoot() function and loops through an object containing local selectors, along with a path string, and returns a new object of selectors that work on global state instead of local.

This makes it easy to generate global selectors from local selectors and reduces the amount of code we have to write at the application reducer level.

I put together a short codepen for this so you can see it in action.

But we still have the problem of acyclic dependencies for redux modules that have thunk style async action creators that rely on importing the globalized selectors we define at the application reducer level.

Removing Acyclic Dependencies

Randy Coulman’s series references a great article from Jack Hsu, Rules for Structuring Redux Applications, in which Jack brings up an approach to resolving this dependency cycle:

“We can solve this issue by giving control to the todos module on where it should be mounted in the state atom."

This could be accomplished by having the todo module export some module name or key which the application reducer would use to then name the mount point in the global state tree.

// redux-modules/todos/index.js 
export moduleKey = 'todos';
//...
// appReducer.js
import reducer, { moduleKey } from 'redux-modules/todos';
const store = createStore({ [moduleKey]: reducer });
//...

It’s an interesting approach with a lot of merit. I think it rubs me the wrong way, because if I’m pulling in third-party redux modules, it feels wrong that that module would dictate my application’s state tree for me. My application might use that redux module in ways the author hadn’t considered.

Given that I didn’t want to go this route, there are two other possible solutions here:

  • live with some duplication of the mount point at the redux module level. Moving the definition of our global selectors into the redux-module and out of the application itself.
// redux-modules/todos/selectors.js 
import { globalizeSelectors } from 'utils';
const mountPath = 'todos';
// duplication from app level
// localized selectors as named exports
export const allTodoIds = state => state.allIds;
export const allTodos = state => state.allIds.map(id => state.todosById[id]);
export const getTodo = (state, id) => state.todosById[id];
// globalized selectors as default export
export default globalizeSelectors({ allTodoIds, allTodos, getTodo }, mountPath);
  • or, pull the mount points and paths out into another module that both the application and todos redux module would depend upon.
// mountpoints.js 
export default { 'todos': 'todos', //... };
// appReducer.js
import mounts from 'mountpoints';
import todoReducer from 'redux-modules/todos';
const store = createStore({ [mounts.todos]: todoReducer });
// redux-modules/todos/selectors.js
import { globalizeSelectors } from 'utils';
import mounts from 'mountpoints';
// localized selectors as named exports
export const allTodoIds = state => state.allIds;
export const allTodos = state => state.allIds.map(id => state.todosById[id]);
export const getTodo = (state, id) => state.todosById[id];
// globalized selectors as default export
export default globalizeSelectors({ allTodoIds, allTodos, getTodo }, mounts.todos);

Conclusion

Much like Randy Coulman, I haven’t decided on a clear path here. I’ve used both approaches in my own applications; each with their own drawbacks and advantages.

I think because of the nature of Redux managing a single, global state atom, selectors are the one piece that become very hard to decouple from the application space since that’s where the store is defined. This hinders us from creating truly “reusable” redux modules in the sense that they can be bundled and used in any application.

For those of you using a modular approach to defining your redux code, I’d love to hear what you’ve done in this area and if this problem has even been a concern. Let me know in the comments section!

Originally published at www.datchley.name on December 4, 2016.

--

--