First of all, I have already gone through:
- /react-redux/issues/779
- Stackoverflow / React Redux rerenders when creating arrays
- Stackoverflow / How to deal with relational data in redux?
- Redux / Recipes / Managing normalized data
And have learnt a lot, but still can't figure out how to work out my case.
The Mission
I am doing a music player and I have normalized my store to look like this:
{
albums : {
album1 : {
name : "...",
songs : ["s1, s2"]
},
album2 : {
name : "...",
songs : ["s3, s4"]
}
},
songs : {
s1 : {
id : "s1",
title : "title",
artist : "name"
},
s2 : { ... },
s3 : { ... }
}
}
So, I have a view that loads a bunch of albums and displays each one with its corresponding songs in something like this:
<Album id={albumId} />
And the Album components looks like this:
class Album extends Component {
componentDidMount() {
this.props.loadAlbum(this.props.albumId)
}
render() {
<div> ... </div>
}
}
mapStateToProps = (state, props) => {
const album = state.albums[props.albumId]
return {
album : album,
songs : album.songs.map( id => state.songs[id] )
}
}
(I am omitting some Redux's boilerplate to enhance readability, but if something feels missing please let me know to add it) loadAlbum
is the action
that loads the album with its songs and I have two reducers (albumsReducer
and songsReducer
) that set the album's metadata in store.albums
and "pushes" the new songs of that album into store.songs
, respectively.
This is the simplified songsReducer
:
(state = initialState.songs, action) => {
if( types.PUT_SONGS_RESULT ) {
return { ...state, ...payload.songs }
}
}
The Problem
The thing is that, of course, as I am creating a new "songs" array every time the store gets updated in mapStateToProps
, the component of "album1" is getting re-rendered every time every other album fetches its songs (because even though the result of "album.song.map" is the same by value for that album, it is not the same array. i.e. [{a:1}] !== [{a:1}]
).
I tried moving the mapping to the following selector using reselect
:
const getAlbum = (state, props) => state.albums[props.albumId]
const getSongs = (state, props) => state.songs
export const makeGetSongsOfAlbums = () => {
return createSelector (
[ getAlbum, getSongs ],
(album, songs) => {
return album ? album.songs.map( id => songs[id] ) : []
}
)
}
Which gets connected like this:
mapStateToProps = (state, ownProps) => {
const getSongsOfAlbum = makeGetSongsOfAlbums()
return {
"album" : state.albums[ownProps.albumId],
"songs" : getSongsOfAlbum(state, ownProps)
}
}
But it has the same effect because the value gets computed every time one of the arguments of the selector changes and state.songs
changes every time one album loads its songs (right?).
So, does anyone have an idea on how I can prevent re-rendering the Album
component every time every other album loads? I can implement shouldComponentUpdate
, but I thought it could be achieved "automatically" without having to perform the deep comparison manually, though definitely I am no React expert so if my hunch of staying away from shouldComponentUpdate
as much as possible is wrong, please let me know.
The Journey
You might be wondering why I am normalizing data like this if it'd be simpler to have the store like:
{
albums : {
album1 : {
name : "...",
songs : [{id : "s1"}, {id : "s2"}]
},
album2 : {
name : "...",
songs : [{id : "s3"}, {id : "s4"}]
}
}
}
Or even loading songs in the local state of each Album
.
Well, the thing is that I am also loading playlists and I need to support the feature to label songs as favourites, so this was starting to get messy when I was displaying the same song in several places in the same screen (i.e. the music player and any list of songs) because when I label one song as "favourite", I want it to be labeled everywhere in the app so each component that is showing it says "hey! this song is already a favourite! I will show a little star in here and won't let the user re-favourite because the API fails if I try to (can't do anything about it)" and having to update the song's status in many places in the store and in local states inside components didn't sound like a good thing. Instead, it sounded more like songs
should be in the global store so I just have to update each song by id in state.songs
and it should also increase performance (or that's what I thought).
So, for instance, when I load a playlist I am following the same pattern as an album:
playlists : {
playlist1 : {
name : "...",
songs : ["s1, s2"]
},
playlist2 : {
name : "...",
songs : ["s3, s4"]
}
}
And the playlists puts its songs in state.songs
. Still if this approach sounds like its getting too complicated (because indeed I thought it would be simpler) and you don't think it's the way to go, please let me know!
EDIT replying to Will Jenkins:
The 'songs' array (actually it is an object. I have edited the sample in the code above to make it clearer) in the root of the store is where all the songs that are going to be currently displayed are going to be. For example, if I load view which displays the songs of a playlist, I would clear that object and then put songs coming from the API in there. In the case of the albums, if that view needs to display 5 albums with its songs, then again I would clear that object and put songs from every album in there so then every album can filter its own songs.
'because even though the result of "album.song.map" is the same by value for that album, it is not the same array' this means that [{a:1}] !== [{a:1}]
because they look the same, but they are not the same array (I have edited my original statement to include this example). Even if they held the same object, they would still be different:
const a = {a:1}
[a] === [a] // this would yield false
About the issue with favouriting, yes, there should be only one entry per song in the store (which actually is the purpose of the 'songs' object). I was trying to explain the issue I had before normalizing data where I had many entries of the same song in many places.
shouldComponentUpdate
is intended for performance improvements and usually it is recommended to useReact.PureComponent
beforeshouldComponentUpdate
. – Grey