Let the Music Render: a sample app demonstrating React component hierarchy & interaction — Part 3 of 3
Now I have my MusicPlayer app, with its two child container components, SongCatalog and YourPlaylist. The last component on my list is SongCard. SongCard is good example of reusable component — both the SongCatalog and YourPlaylist components will call on SongCard. However, how SongCard looks and behaves could be totally different depending on the container.
I think of SongCard as being like a template for a form letter. The basic template is always the same, but the data is different for each individual letter. In this case, both the SongCatalog and the YourPlaylist components are using the same template, but they are populating that template with different data.
Whenever my SongCatalog renders, its createSongCards() function will iterate through each song in the allSongs array and use the map() method to produce a card from this SongCard template. It is that map() method that populates this card with data: the image, title, artist, and year for each song, as well as a unique key we will use to identify this specific card. In that same function, SongCatalog passed each card a function called clickSong, and I defined that function as being the addSongToPlaylist() function from MusicPlayer.
Remember that YourPlaylist also has a CreateSongCards() function, which is nearly identical to SongCatalog but with two important differences:
- YourPlaylist maps over playlistSongs, instead of allSongs
- YourPlaylist passes the function removeSongFromPlaylist() instead of addSongToPlaylist()
Essentially, SongCatalog and YourPlaylist are using the same form letter template, but filling it in with different data. In this case, the clickCard() function works like any other piece of data. Every SongCard will have a function called clickSong() that will be invoked when the card is clicked. However, the behaviour of that function is different depending on whether the card was created by the SongCatalog component as opposed to the YourPlaylist component.
Now my program is complete! It has everything I need for my MusicPlayer:
- The MusicPlayer component owns the state of the application, which holds two arrays: allSongs and playlistSongs
- The MusicPlayer component also defines two functions that are capable of changing state: addSongToPlaylist and removeSongFromPlaylist
MusicPlayer renders two container components:
- SongCatalog, which is passed the props of allSongs and addSongToPlaylist()
- YourPlaylist, which is passed the props of playListSongs and removeSongFromPlaylist()
- Both SongCatalog and YourPlaylist use map() methods to transform their list of songs into SongCard components.
- Both SongCatalog and YourPlaylist pass a function called clickCard() but even though they have the same name, the functions are not the same.
SongCatalog and YourPlaylist both render the SongCard component
- For SongCards in SongCatalog, the clickCard() function invokes the addSongToPlaylist() function
- For SongCards in YourPlaylist, the clickCard() function invokes the removeSongFromPlaylist() function
- SongCard has an onClick handler that always invokes the clickCard() function appropriate for the container it is in.
With this sample playlist app, I have demonstrated how to:
- create a nested hierarchy of React components
- initialize and set state
- pass down data and functions to child components as props
- use if(), filter() and the spread operator to write functions that change state
- use map() to transform an array of data into a collection of components
- create an onClick handler for a reusable component that will behave differently depending on its container