10. Colocating Selectors with reducers
Inside of VisibleTodoList
, the mapStateToProps
function uses the getVisibleTodos
function, and it passes the slice of the state
corresponding to the todos
. However, if we ever change the state's structure, we have to remember to update this whole side.
In order to clean this up, we can move the getVisibleTodos
function out of the view layer and place it inside of the file that contains our todos
reducer. We do this because the todos
reducer knows the most about the internal structure of the state's todos
.
VisibleTodoList
Before
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'all':
return todos;
case 'completed':
return todos.filter(t => t.completed);
case 'active':
return todos.filter(t => !t.completed);
default:
throw new Error(`Unknown filter: ${filter}.`);
}
};
const mapStateToProps = (state, { params }) => ({
todos: getVisibleTodos(state.todos, params.filter || 'all'),
});
Updating our Reducer
We are going to move our getVisibleTodos
implementation into the file with the reducers, and make it a named export.
The convention we follow is simple. The default export is always the reducer function, but any named export starting with 'get'
is a function that prepares the data to be displayed by the UI. We usually call these functions selectors because they select something from the current state.
In the reducers, the state
argument corresponds to the state of this particular reducer, so we will follow the same convention for selectors. The state
argument corresponds to the state of the exported reducer in this file.
Inside src/reducers/todos.js
export const getVisibleTodos = (state, filter) => {
switch (filter) {
case 'all':
return state;
case 'completed':
return state.filter(t => t.completed);
case 'active':
return state.filter(t => !t.completed);
default:
throw new Error(`Unknown filter: ${filter}.`);
}
};
Updating the Root Reducer
Inside of VisibleTodoList
we still depend on the state structure because because we read the todos
from the state, but the actual method of reading todos
may change in the future.
With this in mind, we are going to update our root reducer with a named selector export. It will also be called getVisibleTodos
, and like before it also accepts state
and filter
. However, in this case state
corresponds to the state of the combined reducer.
Now we want to be able to call the getVisibleTodos
function defined in the todos
file alongside the reducer, but we can't use a named import because there is a function with exactly the same name in the scope.
To work around this, we will use the name space import syntax that puts all the exports on an object (called fromTodos
in this case).
Now we can use fromTodos.getVisibleTodos()
to call the function we defined in the other file, and pass the slice of the state
corresponding to the todos
.
Root Reducer Update (src/reducers/index.js
)
import { combineReducers } from 'redux';
import todos, * as fromTodos from './todos';
const todoApp = combineReducers({
todos,
});
export default todoApp;
export const getVisibleTodos = (state, filter) =>
fromTodos.getVisibleTodos(state.todos, filter);
Updating VisibleTodoList
Now we can go back to our VisibleTodoList
component and import getVisibleTodos
from the root reducer file.
import { getVisibleTodos } from '../reducers'
getVisibleTodos
encapsulates all the knowledge about the application state shape, so we can just pass it the whole state of our application and it will figure out how to select the visible todos according to the logic described in our selector.