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>;
)
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