Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Redux: Colocating Selectors with Reducers

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?

like image 436
pjivers Avatar asked Dec 22 '16 01:12

pjivers


People also ask

Can you use selectors in reducer?

It's not typically possible to use selectors inside of reducers, because a slice reducer only has access to its own slice of the Redux state, and most selectors expect to be given the entire Redux root state as an argument.

What is reselect in Redux?

A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well. Selectors can compute derived data, allowing Redux to store the minimal possible state.

What pattern for single file logic is recommended by Redux?

Within a given feature folder, the Redux logic for that feature should be written as a single "slice" file, preferably using the Redux Toolkit createSlice API. (This is also known as the "ducks" pattern).


1 Answers

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).

like image 118
TomW Avatar answered Oct 12 '22 20:10

TomW