Redux Reselect Patterns
Dustin McCraw
Simple “selector” library for Redux
- Selectors can compute derived data, allowing Redux to store the minimal possible state.
- Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
- Selectors are composable. They can be used as input to other selectors.
import { createSelector } from 'reselect'
const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent
const subtotalSelector = createSelector(
shopItemsSelector,
items => items.reduce((acc, item) => acc + item.value, 0)
)
const taxSelector = createSelector(
subtotalSelector,
taxPercentSelector,
(subtotal, taxPercent) => subtotal * (taxPercent / 100)
)
export const totalSelector = createSelector(
subtotalSelector,
taxSelector,
(subtotal, tax) => ({ total: subtotal + tax })
)
let exampleState = {
shop: {
taxPercent: 8,
items: [
{ name: 'apple', value: 1.20 },
{ name: 'orange', value: 0.95 },
]
}
}
console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState)) // 0.172
console.log(totalSelector(exampleState)) // { total: 2.322 }
{
divisions: {
1: { id: 1, name: "National Football League", rootId: 1 },
3: { id: 3, name: "American Football Conference", rootId: 1 },
5: { id: 5, name: "National Football Conference", rootId: 1 }
},
teams: {
101: { id: 101, name: "New England Patriots", divisionId: 2 },
102: { id: 102, name: "Denver Broncos", divisionId: 2 },
203: { id: 203, name: "San Francisco 49ers", divisionId: 3 },
204: { id: 204, name: "Philadelphia Eagles", divisionId: 3 }
},
members: {
17: { id: 17, firstName: "Roger", lastName: "Goodell", divisionId: 1, teamId: nil },
19: { id: 19, firstName: "Nick", lastName: "Foles", divisionId: 5, teamId: 204 },
21: { id: 21, firstName: "Jay", lastName: "Ajayi", divisionId: 5, teamId: 204 },
23: { id: 23, firstName: "Jimmy", lastName: "Garoppolo", divisionId: 5, teamId: 203 },
25: { id: 25, firstName: "Matt", lastName: "Breida", divisionId: 5, teamId: 203 },
27: { id: 27, firstName: "Tom", lastName: "Brady", divisionId: 3, teamId: 101 },
29: { id: 29, firstName: "Brandon", lastName: "Bolden", divisionId: 3, teamId: 101 },
31: { id: 31, firstName: "Paxton", lastName: "Lynch", divisionId: 3, teamId: 102 },
33: { id: 33, firstName: "C.J.", lastName: "Anderson", divisionId: 3, teamId: 102 }
}
}
Store items in our redux store like a database
const selectDivisions = state => state.divisions
const selectTeams = state => state.teams
const selectMembers = state => state.members
Basic Redux Selectors
const selectRawMembers = state => state.members
const selectMembers = createSelector(
selectRawMembers,
(rawMembers) => {
let members = {}
Object.values(rawMembers).forEach((member) => {
members[member.id] = {
...member,
fullName: `${member.firstName} ${member.lastName}`
}
})
return members
}
)
Reselect Derived
Attribute Selectors
members: {
17: { id: 17, firstName: "Roger", lastName: "Goodell", ..., fullName: "Roger Goodell" },
19: { id: 19, firstName: "Nick", lastName: "Foles", ..., fullName: "Nick Foles" },
21: { id: 21, firstName: "Jay", lastName: "Ajayi", ..., fullName: "Jay Ajayi" },
23: { id: 23, firstName: "Jimmy", lastName: "Garoppolo", ..., fullName: "Jimmy Garoppolo" },
25: { id: 25, firstName: "Matt", lastName: "Breida", ..., fullName: "Matt Breida" }
27: { id: 27, firstName: "Tom", lastName: "Brady", ..., fullName: "Tom Brady" },
29: { id: 29, firstName: "Brandon", lastName: "Bolden", ..., fullName: "Brandon Bolden" },
31: { id: 31, firstName: "Paxton", lastName: "Lynch", ..., fullName: "Paxton Lynch" },
33: { id: 33, firstName: "C.J.", lastName: "Anderson", ..., fullName: "C.J. Anderson" }
}
Reselect Derived
Attribute Selectors
const selectRawMembers = state => state.members
const selectMembers = createSelector(
selectRawMembers, selectTeams, selectDivisions,
(rawMembers, teams, divisions) => {
let members = {}
Object.values(rawMembers).forEach((member) => {
members[member.id] = {
...member,
fullName: `${member.firstName} ${member.lastName}`,
teamName: teams[member.teamId] ? teams[member.teamId].name : '',
divisionName:
divisions[member.divisionId] ? divisions[member.divisionId].name : '',
}
})
return members
}
)
Reselect Derived
Attribute Selectors x2
members: {
17: { id: 17, ..., teamName: "", divisionName: "National Football League" },
19: { id: 19, ..., teamName: "Philadelphia Eagles", divisionName: "American Football Conference" },
21: { id: 21, ..., teamName: "Philadelphia Eagles", divisionName: "American Football Conference" },
23: { id: 23, ..., teamName: "San Francisco 49ers", divisionName: "American Football Conference" },
25: { id: 25, ..., teamName: "San Francisco 49ers", divisionName: "American Football Conference" },
27: { id: 27, ..., teamName: "New England Patriots", divisionName: "National Football Conference" },
29: { id: 29, ..., teamName: "New England Patriots", divisionName: "National Football Conference" },
31: { id: 31, ..., teamName: "Philadelphia Eagles", divisionName: "National Football Conference" },
33: { id: 33, ..., teamName: "Philadelphia Eagles", divisionName: "National Football Conference" }
}
Reselect Derived
Attribute Selectors x2
- Can now display full name anywhere in the app without having to call a function.
- Can now display and sort by teamName and divisionName
Relationships between objects?
- Belongs To:
- We can model a belongs to by using the derived attribute reselect. A member belongs to a division.
- Shows up by division_id or team_id on member
- Can easily select from
selectTeams(state)[member.team_id]
- Has Many?
- The member has the division_id so how do you figure out given a division, which members belong to it?
- selectAndSort
selectAndSort
const selectAndSortBy = (collection, by) => {
const sortedBy = {}
Object.values(collection, (item) => {
if (item[by] !== undefined) {
if (!sortedBy[item[by]]) {
sortedBy[item[by]] = []
}
sortedBy[item[by]].push(item)
}
})
return sortedBy
}
const selectMembersByDivision = createSelector(
selectMembers,
members => selectAndSortBy(members, 'divisionId')
)
selectAndSort
selectMembersByDivision(state)
{
1: [
{ id: 17, firstName: "Roger", lastName: "Goodell", divisionId: 1, teamId: nil }
],
3: [
{ id: 27, firstName: "Tom", lastName: "Brady", divisionId: 3, teamId: 101 },
{ id: 29, firstName: "Brandon", lastName: "Bolden", divisionId: 3, teamId: 101 },
{ id: 31, firstName: "Paxton", lastName: "Lynch", divisionId: 3, teamId: 102 },
{ id: 33, firstName: "C.J.", lastName: "Anderson", divisionId: 3, teamId: 102 }
],
5: [
{ id: 19, firstName: "Nick", lastName: "Foles", divisionId: 5, teamId: 204 },
{ id: 21, firstName: "Jay", lastName: "Ajayi", divisionId: 5, teamId: 204 },
{ id: 23, firstName: "Jimmy", lastName: "Garoppolo", divisionId: 5, teamId: 203 },
{ id: 25, firstName: "Matt", lastName: "Breida", divisionId: 5, teamId: 203 },
]
}
selectAndSort
const selectMembersByDivisionId = state => divisionId => (
selectMembersByDivision(state)[divisionId] || []
)
// thoughts about providing a default value?
// pass a function that already selects the members
const mapStateToProps = (state, ownProps) => (
return {
members: selectMembersByDivisionId(state)(ownProps.divisionId)
}
)
this.props.members
// or pass a function that allows the component to select the members
const mapStateToProps = (state, ownProps) => (
return {
membersByDivisionId: selectMembersByDivisionId(state)
}
)
this.props.membersByDivisionId(divisionId)
selectAndSort
selectAndSortContactEmailAddressesByMemberId
selectAndSortBroadcastEmailsByMemberId
selectAndSortContactsByMemberId
selectAndSortInvoicesByBatchInvoiceId
The list goes on and on.
Thoughts on ways to most efficiently use these patterns or other patterns that we should use?
ReSelect Patterns
By Dustin McCraw
ReSelect Patterns
- 833