The following example shows the usual implementation for the selector pattern. Then I'll discuss the problems of this implementation. After that I'll suggest another implementation that may prove useful.
Usual implementation:
Here is how the root reducer looks like along with public selectors:
// reducers/index.js
import { combineReducers } from 'redux';
import * as Items from './items';
import * as Login from './login';
export default combineReducers({
login: Login.reducer,
items: Items.reducer
// more slices ...
});
// PUBLIC SELECTORS
export const getToken = state => Login.getToken(state.login);
export const getLoginError = state => Login.getError(state.login);
export const isLoginLoading = state => Login.isLoading(state.login);
export const getItems = state => Items.getToken(state.items);
export const getCurrentItem = state => Items.getError(state.items);
export const isItemsLoading = state => Items.isLoading(state.items);
// Selectors for data from other slices
// ...
// Selectors for derived data from multiple slices
export const getOwnedItems = (state) => {
// ...
};
Here is how a slice reducer look like along with its private selectors:
// reducers/login.js
import {
// actions ...
} from '../actions';
const defaultState = {
// ...
};
export const reducer = (state = defaultState, action = {}) => {
// Reducer ...
};
// PRIVATE SELECTORS
export const getToken = state => state.token;
export const getError = state => state.error;
export const isLoading = state => state.loading;
Another slice reducer and its private selectors:
// reducers/items.js
import {
// actions ...
} from '../actions';
const defaultState = {
// ...
};
export const reducer = (state = defaultState, action = {}) => {
// Reducer ...
};
// PRIVATE SELECTORS
export const getItems = state => state.items;
export const getCurrentItem = state => state.currentItem;
export const isItemsLoading = state => state.isLoading;
The usage of this implementation would be like this:
import { getLoginError, isLoginLoading } from '../reducers';
const mapStateToProps = state => {
return {
error: getLoginError(state),
loading: isLoginLoading(state)
};
};
The Problem:
The previous implementation of selectors requires all public selectors to be defined in reducers/index.js
. Notice that public selectors receives the full state which is managed by the root reducer defined in reducers/index.js
.
Public selectors can be divided into two types. Extraction selectors just for extracting data from the state. And Derived information Selectors which computes derived information from the state. For the users all selectors are the same and their purpose is to separate the client code from the shape of the state.
First problem with the previous implementation is that all extraction selectors are written twice. Once as a public selector and another as a private selector where the public selector is calling the private one.
Second problem is that all private selectors for specific slice can only receive that slice but it is passed many times, once for each usage instance of a private selector for that slice which seems a good case for refactoring.
The following is another implementation for selectors that could prove useful:
Another implementation
The root reducer file will only provide one select()
function which takes the full state and provide the public interface from which client code can retrieve data from the state.
The interface may consist of functions or collections of functions grouped under a name. This structure allow us to provide an interface that will make implementing extraction selectors trivial in addition to the ability to provide more customized public selectors.
Please don't confuse the structure of the selector with the shape of the state. There is not coupling between the two. The public selector can still implement the same interface for the application even if the shape of the state changed.
Here is the public selectors implemented in select(state)
function:
// reducers/index.js
import { combineReducers } from 'redux';
import * as Items from './items';
import * as Login from './login';
export default combineReducers({
login: Login.reducer,
items: Items.reducer
// more slices ...
});
// PUBLIC SELECTORS
export const select = (state) => {
return {
login: Login.select(state.login),
items: Items.select(state.items),
// Selectors for drived data from multiple slices
getOwnedItems: () => {
// ...
}
};
};
Here is private selectors inplemented in the same way:
// reducers/login.js
import {
// actions ...
} from '../actions';
const defaultState = {
// ...
};
export const reducer = (state = defaultState, action = {}) => {
// Reducer ...
};
// PRIVATE SELECTORS
export const select = (state) => {
return {
getToken: () => state.token,
getError: () => state.error,
isLoading: () => state.loading
};
};
Again for another slice:
// reducers/items.js
import {
// actions ...
} from '../actions';
const defaultState = {
// ...
};
export const reducer = (state = defaultState, action = {}) => {
// Reducer ...
};
// PRIVATE SELECTORS
export const select = (state) => {
return {
getItems: () => state.items,
getCurrentItem: () => state.currentItem,
isLoading: () => state.loading
};
};
The usage of this implementation looks like this:
import { select } from '../reducers';
const mapStateToProps = state => {
return {
error: select(state).login.getError(),
loading: select(state).login.isLoading()
};
};
Question 1:
What are the drawbacks of this implementation?
Question 2:
Is there another way to address the problems described above?
Thank you