Technical Blog


Building a React-Redux App for Music Composition

Because my current version of a music composition management system is some recordings on my cell phone here, some lyrics written in a journal there, and chord progressions jotted in the margins of that book I lost, I decided to create something that would help me record and access everything in one place.

After completing the first feature set and submitting it as a Flatiron School project, I'd like to walk you through how I built this thing and what I see for its future.

I started with the backend - the task of setting up a Rails API that my frontend could request data from to render to the browser. Creating the database and model relationships was a helpful excercise in translating the concept of song and lyric creation to 1s and 0s. The basic structure is: there are two models, a lyric and a song. A song has_many lyrics, and a lyric has_many songs.

  
               
                   # /models/song.rb
                   
                   class Song < ApplicationRecord
                        has_many :lyrics, dependent: :destroy
                        validates :name, :artist, :chords, :presence => :true
                   end
               
               
               
                  # /models/lyric.rb
                  
                  class Lyric < ApplicationRecord
                    belongs_to :song
                    validates :text, :chord, :presence => :true
                  end
                   
                  
                  


Next I built some CRUD actions for my lyrics and songs controllers and configured the routes that I want to client side to have access to. I left the API as a simple implementation so I could focus on integrating some newly earned knowledge: React and Redux!

When I first dove in to this javascript package, I was overwhelmed by React's unfamiliar modular style of structuring render data and confused by how it interacts with Redux and what functionality that provides us with. After much learning, here is what I've ascertained:

The modular style of React, specifically the structure of building a series of components and containers, that will interact to display data, became very intuitive and made easier seeing the parallels between backend and frontend data.

Redux is used to manage the entirety of the state in your application via a javascript object called store, which individual components can subscribe to via the connect() function and assign certain state values to designated component props.

It is also through connect() that our components can access our action creators that will dispatch actions and change the state of our store.

The basic beauty of Redux is that it provides a central location for us to manage our state, and does so in a consistent, immutable and communicative fashion.

Here's a visual representation of how Redux flows:



To begin with the frontend, I decided how I would structure the component tree. I built a container component (a component that talks to the store) for each of the lyric and song models. These components would be responsible for subscribing to the store to allow for state to be update and actions to be dispatched.

<App />
---<SongsContainer />
------<SongForm />
------<Songs />
---------<SongCard />
---<LyricsContainer/>
------<LyricForm />
------<Lyrics />
---------<LyricCard />


As mentioned, lyrics and songs each have a container to house their related components. Each of these containers connect to the store and then pass down some props to their sub-components. The Songs and SongCard as well as Lyrics and LyricCard are presentational components (responsible primarily for displaying data) that are passed important data via props. The Form components are a hybrid between container and presentational, as they subscribe to the store in order to keep form fields up to date as well as display the form itself.

I built action creator functions that would be responsible for dispatching actions, and reducers that would be responsible for receiving these actions and updating the store. Here is how a new song will be added to the store:

                    
                        # /actions/songs.js
                        
                        const addSong = song => {
                            return {
                                type: 'CREATE_SONG_SUCCESS',
                                song
                            }
                         }
                    
                    


This action creator, when called, will be dispatched to the corresponding reducer:

                    
                    # /reducers/songsReducer.js
                    
                    export default (state = [], action) => {
                        switch(action.type) {

                            case 'GET_SONGS_SUCCESS':
                                return action.songs

                            case 'CREATE_SONG_SUCCESS':
                                return state.concat(action.song) // Change store's state!!!
            
                        case 'DELETE_SONG':
                            return state.filter(song => action.id !== song.id)
       
            
                        default:
                            return state;
                        }
                    }
                    
                    


In the event that we need to make a request to our backend to get or post some data, we can use a package called Thunk that will allow us to make an asynchronous fetch() request to our API and upon completion dispatch() an action that is a function, not just a plain object. And what is this action? Our action creator we previously created!



             
             # actions/songs.js
             
             export const createSong = song => {
                return dispatch => {
                    dispatch(postRequest())
                    return fetch('http://192.168.1.31:3000/api/v1/songs', {
                        method: 'POST',
                        headers: {
                            "Content-Type": "application/json"
                        },
                        body: JSON.stringify({song: song})
                    })
                    .then(response => response.json())
                    .then(song => {
                        dispatch(addSong(song)) // Action Creator function from before!!!
                            dispatch(resetSongForm())
                    })
                    .catch(error => console.log(error))
                }
            }
             
             


Here's what's happening: The user clicks a submit button after entering data into a "New Song" form. A click handler attached to this event calls the createSong() funtion, which makes a fetch() request to the API, upon completion passing that response to our action creator function addSong(), which itself is passed in as an argument to Thunk's dispatch() function, and then the response is sent to the reducer, which updates the store to include the newly created song!

I built this type of
[event] => [async action] => [action creator] => [reducer] => [store]
pattern for the songs index and create actions and even implemented full client-side CRUD functionality for lyrics. Woohoo!

As I continue working, I'd like to add a Chord model to the family that possesses a has_many_through relationship to Song through Lyric so that users can interact with chord composition on a more detailed level. I'd also like to link to an external API to allow users to search for existing compositions and import them into their own library. Finally, I want to build two fun "modes" available to the user. "Muse Mode" will display all lyrics across songs in a shuffled manner and allow users to edit the lyric order and subcategorize lyrics to create fresh songs, and "Training Mode" Pitch Training will take in and process a user's pitch and evaluate how flat or sharp it is relative to a designated note and Harmony Training, will take in a tune and output a harmonized version. Gosh knows I need help with my harmonizing.

If you're interested, here's my github repo and video demo.

Thanks for listening!