Let the Music Render: a sample app demonstrating React component hierarchy & interaction — Part 1 of 3
When I was first learning React, I was impressed by the power and speed of the framework, but it took time for me to really grasp how the different components worked together. In particular, I struggled to understand how a single React component could have different behavior depending on its container.
In order to illustrate how reusable components work together in React, I have created a sample app for a music player that lets users create their own playlists. Over the course of three posts, I will use this app to demonstrate how to:
- create a nested hierarchy of React components
- initialize and set state
- pass down data and functions to child components as props
- transform an array of data into a collection of components
- create reusable components that will behave differently depending on their container
Let’s say I want to create a music app dedicated to the hit songs of the eighties. I want my app to have two areas or “containers” — a catalog of all the songs at the top, and then an area underneath for the user to create their own playlist. When a user clicks on any song in the top container, it should be added to their playlist at the bottom. If the user clicks on the song in the pink playlist section, it is removed from the playlist.
Because I am using React, I know that my app needs to be built from components — independent and reusable pieces of code that each return one HTML element. Components in a React app are arranged in a nested hierarchy, in this case:
At the top of the hierarchy is the MusicPlayer component; all the other components of my app will be nested within MusicPlayer. Next, my two “container” components — the song catalog and the user’s playlist — which sit next to each other in the hierarchy as “siblings”. At the bottom I have the SongCard component that I will use to display each song, with the image, title, artist, and year. Both SongCatalog and YourPlaylist display SongCards.
To build out my app, I will start at the top of the hierarchy. In React, data always flows down from a “parent” component to its “children”. Since MusicPlayer is my top-level component, it is the only one that is capable of providing data to every other component in my app. This makes it a perfect place to store the state of my application.
In React applications, state is an object where we store property values that belong to a specific component. Anytime the state data changes, React “reacts” by re-rendering the component. Essentially, when we set state for a component, we are telling React “watch this object, and if any of its values ever change, re-render the component”.
In the case of my MusicPlayer, I need to assign two pieces of state: an array which will hold all of the songs in my song catalog, and another array that will hold all of the songs in the user’s playlist. Inside the class definition for the MusicPlayer component, I initialize the state by creating both properties and assigning them to blank arrays.
Next, I need to populate my allSongs array by making a fetch request to my backend database that holds the song catalog. I want to make sure this happens at a specific point in the React “lifecycle” — after I have set the state, but before my app is available for user interaction.
Any time I need to change a piece of state, I use the setState function, because React will only “react” to changes in state that happen through that function. The above function will fetch all the songs from the collection and assign them to the allSongs array in state.
At this point I haven’t actually created anything for my app to render, but I will get there. First, I want to write two functions I know I will need later on:
- a function to add a song from the catalog to the user’s playlist
- a function to remove a song from the user’s playlist
Since playlistSongs is a piece of state that belongs to the MusicPlayer component, all functions that can change that state must also belong to the MusicPlayer component. I start with the function to remove a playlist song:
This function takes in one argument — the song item that I want to remove from the playlistSongs array. To do this, I take the playlistSongs array that is currently held in state and filter it to exclude the song I want to remove. Then, using the setState function, I assign the new filtered array to the playlistSongs in state.
The function to add a song to playlistSongs is similar, but with an additional wrinkle. I want my users to be able to click a song in the catalog and add it to their playlist, but I don’t want any duplicate songs in the playlist if the user clicks the same card twice. My function needs a conditional statement to check if the song is already in playlist before adding it.
This function also takes in one song as an argument. But before I set the state, I use a conditional statement that uses the find method on the current playlistSongs array. If the find method returns a matching song, nothing will happen — the setState function will not be invoked, and the app will not re-render.
If the find method does not find any matches, my function will create a new array that includes songToAdd. I do this by using […] or the “spread operator”. By using the spread operator I am essentially telling the function “take all of the items from this existing array”. Then I append songToAdd, and use the setState function to assign the new array to playlistSongs.
Now I am finally ready to start my render() function. I know from my components diagram that MusicPlayer has two children, SongCatalog and YourPlaylist. Therefore, these are the two components that need to be included in my return value. Since a React component can only return one HTML element, we need to nest our two components in a single <div> element. I also know that these two components will each need to access some of the MusicPlayer data and functions in order to do their jobs.
- SongCatalog is going to need the allSongs array from state
- SongCatalog also needs the addSongToPlaylist function, so that songs in that container can be added to the playlist when clicked
- YourPlaylist needs access to the playlistSongs array from state
- YourPlaylist also needs the removeSongFromPlaylist function, so that songs in that container will be removed when clicked
I give these components the access they need by “passing props”, which means that I will assign the data and functions as properties. The syntax for this is similar to assigning other HTML properties:
Now my MusicPlayer component is passing the correct properties to its two child components — but still nothing is going to happen on my page, because I haven’t defined those components yet. In Part Two, I cover how to set up these two containers and use these properties to generate a card for each song.