Modular Reducers and Selectors

Recently, we’ve been talking about encapsulating the Redux state tree. In the previous post, we looked at the asymmetry that arises between reducers and selectors when using combineReducers. We came up an approach that with works well when using a Rails-style project organization. But if we try to use it in a modular (domain-style) project structure, we run into issues.

What’s a Modular Project Structure?

As I mentioned last time, the Redux FAQ talks about a few different ways to structure projects:

Domain-style: separate folders per feature or domain, possibly with sub-folders per file type

“Ducks”: similar to domain style, but explicitly tying together actions and reducers, often by defining them in the same file

For this post, I’ll treat Domain-style and “Ducks” as one style, because there’s no relevant difference when it comes to reducers and selectors. I typically refer to these styles as “modular”, because they break an application into separate modules.

Organize by feature: Each domain or feature in your application should have its own “module” or folder within the project. The related actions, reducer, components, and selectors all live in this module.

Create strict module boundaries: Each module defines its public interface explicitly. There is a top-level index.js file that exports only the parts of the module that should be exposed to other modules. Modules never do “deep imports” from other modules (i.e., import Component from 'modules/foo/components/Component'). If you can’t do import { Component } from 'modules/foo', then the component isn’t meant to be used outside of the module.

Avoid circular dependencies: If module A depends on module B (that is, it imports something from B’s public interface), then module B cannot depend on anything in module A. This is Uncle Bob Martin’s Acyclic Dependencies Principle (ADP) which I’ve written about before.

What Do We Have So Far?

In the last post, we came up with an approach where we wrote selectors as peers of the corresponding reducer. That is, the selectors assumed they were working on the same slice of the state tree as the reducer.

We then provided a “globalized” version of the selectors that worked from the root of the state tree.

The local selector lives with its reducer, and the globalized selectors live with the main app reducer.

The thunk action creators and container components use the globalized selectors, while the reducer and its tests use the localized selector. Everyone’s happy.

This approach works well in a Rails-style project organization.

So What’s the Problem?

In a modular project structure, the reducer and localized selectors live within the module (todos in our example from the last post). That part is fine.

The reducer is imported from todos by a top-level module (we’ll call that app here) and combined with the reducers from other modules using combineReducers. So far, so good.

The thunk action creators and container components also live within the todos module. But remember, those need the globalized selectors.

So where do the globalized selectors live? The globalized selectors need two things:

the localized selectors

the path from the root of the state tree to the state slice that the localized selectors expect

The localized selectors are within todos; the path information lives in app alongside the main app reducer.

Since app has part of the information we need, and already depends on todos for its reducer, it seems logical that the globalized selectors should also live in app. Problem solved, right?

Not so fast. Remember that the thunk action creators and containers need those globalized selectors, and they live in todos.

If the globalized selectors live in app, then todos would have to import them in order to use them in thunk action creators and containers. But app already depends on todos, so this would create a dependency cycle (app -> todos -> app). We can’t do this if we’re following Jack’s third rule and the Acyclic Dependencies Principle (ADP).

What Do We Do About It?

Spoiler alert: I still haven’t found a solution I really like for this problem.

But I can talk about some options I’ve considered and what I’m doing for now.

Forget About the ADP

We could just forget about the ADP and allow dependency cycles just for this case. The problem is that, once we allow one exception, it’s easy to allow other exceptions “just this once”. We end up with a big mess of spaghetti code that we can’t maintain any more, which is what the modular structure was trying to avoid in the first place.

More practically, most build/bundling tools we might use don’t handle cyclic dependencies well. I’ve accidentally created cyclic dependencies with both Webpack and the React Native packager, and neither of them are happy about it.

This doesn’t seem like a good answer.

Forget About Modular Structure

We could just give up on the modular structure and go back to the Rails-style structure. Maybe this problem is an indication that the modular structure isn’t really a good approach after all.

I’ve found that I’m much happier with the modular structure. It’s easier to find things, it’s easier to reason about the application, and the code ends up cleaner because I’m forced to think about where things really belong.

And again, the point of the modular structure is to avoid a big mess of spaghetti.

Apply the Dependency Inversion Principle. We’d have to extract something within todos that could be used by both app and todos to break the cycle. We’ll see an example of this in the next section.

Create a new module that both app and todos depend on.

I’ve thought about these approaches several times, and I haven’t found an extraction I like.

One idea is to pull out some kind of description of the shape of the state tree. The main reducer in app would use this description to combine the reducers, and todos would use it to globalize its selectors.

This seems overly complex, so I haven’t gone this direction yet. But I’m open to other ideas about how to break this dependency cycle.

Move the Globalized Selectors Into the Module

The final solution I’ve considered is to move the globalized selectors into the todos module.

In order to do this, todos now has to know the path from the root of the state tree to its local sub-section.

This is knowledge it really shouldn’t have, and is also a form of duplication because the app reducer is what creates this path.

There’s a couple of ways to do this.

Let the Module Control Its Mount Point

In Jack Hsu’s article, he says:

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

In this approach, each module defines a constant, say reducerKey or moduleName or something like that.

That constant is used to create the globalized selectors:

Globalizing a Selector

import{moduleName}from'./constants'

import*asfromTodosfrom'./localSelectors'

exportconstallTodos=state=>fromTodos.allTodos(state[moduleName])

It’s also used in the main app reducer when combining reducers:

Combining Reducers

import{combineReducers}from'redux'

import{reducerastodosReducer,moduleName}from'./todos'

exportdefaultcombineReducers({

[moduleName]:todosReducer

})

Live With the Duplication

Another option is to just live with some duplication. This is my current approach, but I’m considering switching to Jack’s method instead.

I keep my selectors in a separate file from my reducers. In the selectors file, I individually export the localized selectors by name, and then I default export an object containing the globalized selectors.