21

In this Redux: Colocating Selectors with Reducers Egghead tutorial, Dan Abramov suggests using selectors that accept the full state tree, rather than slices of state, to encapsulate knowledge of the state away from components. He argues this makes it easier to change the state structure as components have no knowledge of it, which I completely agree with.

However, the approach he suggests is that for each selector corresponding to a particular slice of state, we define it again alongside the root reducer so it can accept the full state. Surely this implementation overhead undermines what he is trying to achieve... simplifying the process of changing the state structure in the future.

In a large application with many reducers, each with many selectors, won't we inevitably run into naming collisions if we're defining all our selectors in the root reducer file? What's wrong with importing a selector directly from its related reducer and passing in global state instead of the corresponding slice of state? e.g.

const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, todo(undefined, action)];
    case 'TOGGLE_TODO':
      return state.map(t => todo(t, action));
    default:
      return state;
  }
};

export default todos;

export const getVisibleTodos = (globalState, filter) => {
  switch (filter) {
    case 'all':
      return globalState.todos;
    case 'completed':
      return globalState.todos.filter(t => t.completed);
    case 'active':
      return globalState.todos.filter(t => !t.completed);
    default:
      throw new Error(`Unknown filter: ${filter}.`);
  }
};

Is there any disadvantage to doing it this way?

Community
  • 1
  • 1
pjivers
  • 1,283
  • 12
  • 20
  • 1
    Yeah, I just watched that video and I can see how once an app starts growing, having 3 sources of truth for one action seems, well - terrible. From that one action in the jsx file, we call to the reducer/index file, which now has a reference to the reducer file which holds the state. I don't know, this to me - seems like a lot of overhead and to instantiate one method that needs a slice of data, we now have to have its presence felt in 3 different files. Now, multply that by 50, or 100... Perhaps in a very base application like "todos", its fine. But its' not real world. – james emanon Dec 22 '16 at 02:08
  • A lot of things in Redux are suggested best practices, but that doesn't necessarily mean they're best for all cases or for your use case. I do things the same way you do: place selectors with the reducer they correspond with. I think this makes more sense as the knowledge for accessing a part of the state is located alongside the functions that define that part of the state. It's really about what works best for you though, and many people in the Redux community take the attitude that that's what you should be going for. – Mark Chandler Dec 22 '16 at 15:30
  • 1
    In a large application I don't think that the structure proposed in the tutorials works anymore. You need to split your reducers / selectors / actions based on the specific domain object they are targeting. To answer your question, there are not disadvantages to that, other than the fact that you introduce a lot of dependencies between your components and the specific reducers – Alex Moldovan Dec 23 '16 at 10:57
  • 1
    I've found a good post on this problem at: http://www.datchley.name/scoped-selectors-for-redux-modules/ – pjivers Jan 03 '17 at 00:19

1 Answers1

13

Having made this mistake myself (not with Redux, but with a similar in-house Flux framework), the problem is that your suggested approach couples the selectors to the location of the associated reducer's state in the overall state tree. This causes a problem in a few cases:

  • You want to have the reducer in multiple locations in the state tree (e.g. because the related component appears in multiple parts of the screen, or is used by multiple independent screens of your application).
  • You want to reuse the reducer in another application, and the state structure of this application is different from your original application.

It also adds an implicit dependency on your root reducer to each module's selectors (since they have to know what key they are under, which is really the responsibility of the root reducer).

If a selector needs state from multiple different reducers, the problem can be magnified. Ideally, the module should just export a pure function that transforms the state slice to the required value, and it's up to the application's root module files to wire it up.

One good trick is to have a file that only exports selectors, all taking the state slice. That way they can be handled in a batch:

// in file rootselectors.js
import * as todoSelectors from 'todos/selectors';
//...
// something like this:
export const todo = shiftSelectors(state => state.todos, todoSelectors); 

(shiftSelectors has a simple implementation - I suspect the reselect library already has a suitable function).

This also gives you name-spacing - the todo selectors are all available under the 'todo' export. Now, if you have two todo lists, you can easily export todo1 and todo2, and even provide access to dynamic ones by exporting a memoized function to create them for a particular index or id, say. (e.g. if you can display an arbitrary set of todo lists at a time). E.g.

export const todo = memoize(id => shiftSelectors(state => state.todos[id], todoSelectors)); 
// but be careful if there are lot of ids!

Sometimes selectors need state from multiple parts of the application. Again, avoid wiring up except in the root. In your module, you'll have:

export function selectSomeState(todos, user) {...}

and then your root selectors file can import that, and re-export the version that wires up 'todos' and 'user' to the appropriate parts of the state tree.

So, for a small, throwaway application, it's probably not very useful and just adds boilerplate (particularly in JavaScript, which isn't the most concise functional language). For a large application suite using many shared components, it's going enable a lot of reuse, and it keeps responsibilities clear. It also keeps the module-level selectors simpler, since they don't have to get down to the appropriate level first. Also, if you add FlowType or TypeScript, you avoid the really bad problem of all your sub-modules having to depend on your root state type (basically, the implicit dependency I mentioned becomes explicit).

TomW
  • 3,725
  • 1
  • 20
  • 26
  • 2
    I was looking to solve this exact problem and this answer inspired a great solution. For anyone looking for an example of what shiftSelectors might look like, check out bindSelectors in this gist: https://gist.github.com/jslatts/1c5d4d46b6e5b0ac0e917fa3b6f7968f – jslatts Mar 29 '17 at 17:57