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
store
, which individual components can subscribe to via the connect()
function and assign certain state values to designated component props.
connect()
that our components can access our action creators that will dispatch actions and change the state of our store.
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 />
store
in order to keep form fields up to date as well as display the form itself.
# /actions/songs.js
const addSong = song => {
return {
type: 'CREATE_SONG_SUCCESS',
song
}
}
# /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;
}
}
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))
}
}
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!