React Patterns:
Normalizing React/Redux Data

1960s Database

Users
Id Name Town Population ClubName ClubTown
1 Matthew 'Matt' Holloway Wellington 15 people MMM Auckland
2 Stella Wellington 15 people
Users
Id Name TownId ClubId
1 Matthew 'Matt' Holloway 1 1
2 Stella 1
Towns
TownId Name Population
1 Wellington 15 people
2 Auckland 16 people
Clubs
ClubId Name TownId
1 MMM 2

Denormalized (fewer tables, more repetition)

versus

Normalized (more tables, fewer repeats)

{ users: [ { id: 1, name: 'Matthew "Matt" Holloway', town: 'Wellington', population: '15 People', clubName: 'MMM', clubTown: 'Auckland' }, { id: 2, name: 'Stella', town: 'Wellington', population: '15 People', } ] }
const selectUsers = state => state.users const selectUser = (state, id) => state.users.find(user => user.id === id)
const updateTown = (previousState, action) => ({ ...previousState, users: previousState.users.map(previousUser => { if (previousUser.townName !== action.payload.previousTownName) { return previousUser; } return { ...previousUser, townName: action.payload.newTownName, population: action.payload.newPopulation }; }), clubs: previousState.clubs.map(previousClub => { if (previousUser.clubTown !== action.payload.previousTownName) { return previousClub; } return { ...previousClub, clubTown: action.payload.newTownName }; } })

Denormalized

Easy to select.
Hard to update.
Easy to introduce bugs.

Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time:
premature optimization is the root of all evil.
Yet we should not pass up our opportunities in that critical 3%.
- Don Knuth

Normalized

{ users: { 1: { name: 'Matthew "Matt" Holloway', townId: 1, club: 1 }, 2: { name: 'Stella', townId: 1 } }, towns: { 1: { name: 'Wellington', population: '15 People' } }, clubs: { 1: { name: 'MMM', townId: 1 } }, userView: [1, 2] }
const selectUsers = state => state.userView.map( userId => selectUser(state, userId) ) const selectUser = (state, userId) => { const user = state.users[userId]; return { ...user, town: selectTown(state, user.townId) } }; const selectTown = (state, townId) => state.towns[townId]; const updateTown = (previousState, action) => ({ ...previousState, towns: { ...previousState.towns, [action.payload.newTown.id]: action.payload.newTown, } })
const mapStateToProps = state => state.userView; const HomePage = ({ userIds }) => ( <div> {userIds.map(userId => <User id={userId} /> )} </div> ) const mapStateToProps = (state, ownProps) => state.users[ownProps.id]; const UserComponent = ({ name, townId, clubId }) => ( <div> {name} <Town id={townId} /> {clubId && <Club id={clubId} /> </div>; ) const mapStateToProps = (state, ownProps) => state.towns[ownProps.id]; const TownComponent = ({ name }) => ( <div> {name} </div>; )
ReSelect

Normalizing Denormalized Responses

{ users: [ { id: 1, name: 'Matthew "Matt" Holloway', town: { id: 1, name: 'Wellington', population: '15 People', }, club: { id: 1, name: 'MMM', townId: 2, population: '16 People' } }, { id: 2, name: 'Stella', town: { townId: 1, name: 'Wellington', population: '15 People', } ]}
const userViewReducer = (previousState, action) => { if (action.type === NEW_DATA) { return action.payload.users.map(user => user.id ) } return previouState; }
const userReducer = (previousState, action) => { if(action.type === NEW_DATA) { return { ...previouState, ...action.payload.users.reduce((users, user) => { users[user.id] = { id: user.id, name: user.name, } return towns; }, {}) } } return previouState; }
const townReducer = (previousState, action) => { if(action.type === NEW_DATA) { return { ...previouState, ...action.payload.users.reduce((towns, user) => { towns[user.town.id] = user.town; return towns; }, {}) } } return previouState; }
const clubReducer = (previousState, action) => { if(action.type === NEW_DATA) { return { ...previouState, ...users.reduce((clubs, user) => { clubs[user.club.id] = user.club; return clubs; }, {}) } } return previouState; }

In conclusion

  • Normalize your Redux state (flatten, deduplicate)
  • Use ReSelect and Redux-ORM to help
  • Only denormalize for speed reasons if necessary