Selectors in Redux are a MUST
Lately, I’ve been shocked by how many problems Redux’s selectors can solve. That’s why I’ve become a big fan and I’m sad to see that pattern treated as an advanced one. They are easy to understand and, at the same time, enable low coupling / high cohesive code at the Redux’s state level.
I got to know selectors a couple of months ago. In fact, I started working on a crazy search page for a project: besides a normal text query, users can select a number of filters. Thing is, filters come in all sorts and shapes: some are a set of checkboxes, some are nested trees of checkboxes, some trees can be filtered with a text box. Not only that, they produce really complex queries with combinations of AND and OR. Not only that, they interact between each other. And the list of “not only that”s goes much longer.
At the beginning I felt overwhelmed by all of that complexity. Eventually, I worked my way towards the light step by step. In this journey, selectors have saved my ass and become my best friend.
What follows is a story from the project on how I got to know selectors and why they are so awesome.
Keeping state flat
At the beginning I had a store shape similar to this
store = {
filters: {
locations: []
...
}
}
where locations
could be
locations = [{
id: 1
value: 'Europe'
checked: false
children: [{
id: 2
value: 'Central Europe'
checked: false
children: [{
id: 3
value: 'Italy'
checked: false
}],
}]}, ...]
Does it look bad enough? Well, imagine the pain of doing an immutable update to check the node with id 3
. In plain JavaScript that would look like this
newLocations = [
...locations.slice(1)
{
...locations[0]
children: [
...locations[0].children.slice(1)
{
...locations[0].children[0]
children: [
...locations[0].children[0].children.slice(1)
{
...locations[0].children[0].children[0]
checked: true
}
]
}
]
}
]
Of course, I could use some library like dot-prop but that would be like sweeping the code smell under the rug.
A better representation could be
locationsById = {
1: {
id: 1
value: 'Europe'
checked: true
children: [2, 3]
}
2: {
id: 2
value: 'Central Europe'
checked: true
children: [3]
},
3: {
id: 3
value: 'Italy'
checked: true
}, ...}
Or the one I ended up with
locationsByPath = {
'1': {
id: 1
value: 'Europe'
checked: true
}
'1/2': {
id: 2
value: 'Central Europe'
checked: true
}
'1/2/3': {
id: 3
value: 'Italy'
checked: true
}, ...}
with the trees’ hierarchies encoded in the paths (eg 1/2
is child of 1
and parent of 1/2/3
).
With locationsByPath
the update looks much nicer
newLocationsByPath = {
...locations
'1/2/3': {
...locationsByPath['1/2/3']
checked: false
}
}
Separating concerns
While working on the feature I’ve noticed something: locationsByPath
knows too much. As a matter of fact, it has both domain knowledge (ie id
, value
and hierarchy) and UI data (ie checked
).
That’s wrong because most of the times domain and UI data have different needs and lifecycles. For example, in my case, once the filters are loaded in the store they are never updated. On the contrary, the checked
property is toggled every time the checkbox’s state changes. Therefore, I decided to separate the two:
locationsByPath = {
1: {
id: 1
value: 'Europe'
}
'1/2': {
id: 2
value: 'Central Europe'
}
'1/2/3': {
id: 3
value: 'Italy'
}, ...}checkedLocationsPaths: ['1', '1/2', '1/2/3']
First of all this makes checking a node incredibly easy
checkedLocationsPaths.includes('1/2/3') ?
checkedLocationsPaths :
checkedLocationsPaths.concat('1/2/3')
Also, checkedLocationsPaths
and locationsByPath
are independent now. That means, they can follow different lifecycles. For example, checkedLocationsPaths
can be present in the store before loading locationsByPath
.
Unfortunately, refactoring to a flat state made my life easier with reducers but harder with components. In fact, the initial nested shape is much easier for components to deal with:
const Component = ({ value, checked, children }) =>
<div>
<input type='checkbox' value={value} checked={checked} />{children.map(({ value, checked, children }) =>
<Component
value={value}
checked={checked}
children={children}/>
}
</div>
Well, I could add some logic to the component to handle the flat state. But is it the right place for that? I don’t think so. In fact, doing so would mean coupling the component with the state’s shape. Also, I believe in really dumb™ components.
Selector to the rescue
Reducers are in charge of writing the state to a specific shape. Therefore, it’s a good idea to have something as close as possible to them to handle the reading part. That’s where selectors come into play.
If you don’t want to take my word, listen to Dan Abramov’s.
As a matter of fact, having the initialState
, write side and read side in the same place is right on spot:
// reducer.jsinitialState = {
locationsByPath: {}
checkedLocationsPaths: []
...
}// write side
const filters = (state = initialState, action) => {
if (action.type === 'LOAD_LOCATIONS') {
return { ...state, locations: action.locations };
} else if (action.type === 'CHECK_LOCATION') {
const checkedLocationsPaths =
state.checkedLocationsPaths.includes('1/2/3') ?
state.checkedLocationsPaths :
state.checkedLocationsPaths.concat('1/2/3')
return { ...state, checkedLocationsPaths }
}
...
}// read side// nests locations
const nest = locationsByPath => ...// adds `checked: true` where appropriate
const withChecks = (locationsByPath, checks) => ...const getNestedLocationsWithChecks = state => {
const {
locationsByPath,
checkedLocationsPaths,
} = state; return nest(
withChecks(locationsByPath, checkedLocationsPaths)
);
}
Let’s see how some the selector works with an example
locationsByPath = {
1: {
id: 1,
value: 'Europe',
},
'1/2': {
id: 2,
value: 'Central Europe'
},
'1/2/3': {
id: 3,
value: 'Italy',
}, ...}checkedLocationsPaths: ['1', '1/2', '1/2/3']state = {
locationsByPath,
checkedLocationsPaths
}nestedLocationsWithChecks(state)
=> [{
id: 1,
value: 'Europe',
checked: true,
children: [{
id: 2,
value: 'Central Europe'
checked: true,
children: [{
id: 3,
value: 'Italy',
checked: true,
}]
}]},
...]
with nestedLocationsWithChecks(state)
ready to be passed to the Component
from the previous section.
In other words, on one side the application works with flat state. Which is more convenient for data management. On the other side, the application works with nested data. Which is more convenient for the UI.
More importantly, they can evolve independently since selectors work as an anti-corruption layer and prevent leaking structural coupling.
Bubbling up a selector
In the previous section I’ve worked top to bottom by refactoring the state first and worked my way down to the component.
Let’s see how the inverse looks like by refactoring some code I’ve written before getting to know selectors.
There’s a page in the application where some articles are shown grouped by their category (ie articles belong to one category only). All the categories must be rendered always besides the last one which should be hidden if empty.
categoriesFrom
performs the group by and translates the title for the current locale. Component
renders each category separately.
const categoriesFrom = resources => {
const byCategory = [
{ id: 1, name: I18n.t('cat1'), resources: resources.cat1 },
{ id: 2, name: I18n.t('cat2'), resources: resources.cat2 },
{ id: 3, name: I18n.t('cat3'), resources: resources.cat3 },
{ id: 4, name: I18n.t('cat4'), resources: resources.cat4 },
]; if (resources.cat5.length !== 0) {
const name = I18n.t('cat5')
return byCategory
.concat({ id: 5, name, resources: resources.cat5 })
} else {
return byCategory
}
}const Component = ({ resources }) =>
categoriesFor(resources).map(category =>
<Category key={category.id} category={category} />)
)
Component
is too smart and is coupled to the state shape (eg resources.cat1
).
Let’s bubble that logic up to a selector:
// reducer.js// selector
const getResources = state => state.resources;// selector
const getResourcesGroupedByCategory = state => {
const resources = getResources(state); const byCategory = [
{ id: 1, name: I18n.t('cat1'), resources: resources.cat1 },
{ id: 2, name: I18n.t('cat2'), resources: resources.cat2 },
{ id: 3, name: I18n.t('cat3'), resources: resources.cat3 },
{ id: 4, name: I18n.t('cat4'), resources: resources.cat4 },
]; if (resources.cat5.length !== 0) {
const name = I18n.t('cat5')
return byCategory
.concat({ id: 5, name, resources: resources.cat5 })
} else {
return byCategory
}
}// Component.jsxconst Component = ({ resources }) =>
resources.map(category =>
<Category key={category.id} category={category} />)
)connect({ resources: getResources(state) })(Component)
Ever heard about dumb components? With selectors you get dumber components™: they just destructure props
and render them without any additional logic.
Outro
In general, the shape of the state is something only reducers should know about. The moment it leaks out of the store, code becomes structurally coupled.
Since reducers decide what’s the shape because they write it. It’s just common sense to make the “read” happen close to them with selectors.
As a rule of thumb, doing state.
is a mistake. The only place where that should be allowed is inside a selector. But only as long as the selector is colocated in the reducer which is responsible for that part of the state.
Want to read more cool JavaScript stuff? Check out how functional programming can make your life easier.
Get the latest content via email from me personally. Reply with your thoughts. Let’s learn from each other. Subscribe to my PinkLetter!