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

  1. Can now display full name anywhere in the app without having to call a function.
  2. Can now display and sort by teamName and divisionName

Relationships between objects?

  1. Belongs To:
    1. We can model a belongs to by using the derived attribute reselect. A member belongs to a division. 
    2. Shows up by division_id or team_id on member
    3. Can easily select from
      selectTeams(state)[member.team_id]
  2. Has Many?
    1. The member has the division_id so how do you figure out given a division, which members belong to it?
    2. 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