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?

Made with Slides.com