React+Redux+Soundcloud = Musio

The first thing which pops into my mind when thinking about react and redux is thenewboston infographics. In this tutorial I will walk you through my personal project Musio.  In the previous article, I have covered the basics of react.

[wpi_designer_button text=’Download’ link=’https://github.com/arjunsk/react-redux-audio-player’ style_id=’48’ icon=’github’ target=’_blank’]

This pretty much speaks about the data flow in react redux.

Musio

Musio is an open source web app that fetches tracks from soundcloud api.

Let me give you brief overview of my project.

Folder structure:

Dependency :

npm install --save redux react-redux redux-logger redux-thunk axios

Reducers:

Actions describe the fact that something happened but don’t specify how the application’s state changes in response. This is the job of reducers.

reducer / reducer_settings.js

// it is initial state
const default_state = {
    isPlaying : false,
    trackID : 0,
    playedForOnce : false
};

function settingsReducer(state = default_state, action) {

    switch(action.type){
        case "PLAY_TRACK" :
			// Note: Object.assign() copies the values (of all enumerable own properties) 
			// from one or more source objects to a target object. 
			// This makes it immmutable
            return Object.assign(
                {},
                state,
                {
                    isPlaying : action.payload.isPlaying,
                    trackID : parseInt(action.payload.trackID,10), // parseInt is used to explicitly convert text to int
                    playedForOnce : true
                }
            );

        case "PAUSE_TRACK" :
            return Object.assign(
                {},
                state,
                {
                    isPlaying : action.payload.isPlaying
                }
            );

        default :
            return state
    }
}

export default settingsReducer;

reducer / index.js

import { combineReducers } from 'redux' // Note

import settingsReducer from './reducer_settings' // Note how we imported all the reducers
import tracksReducer from './reducer_tracks'

// we are combining all the reducers to create a massive chunk
export default combineReducers({
     settingsReducer,
     tracksReducer
});

Actions:

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

actions / actions_settings.js

export function play_track(new_id){
    return {
        type:"PLAY_TRACK",
        payload:{
            isPlaying : true,
            trackID : new_id
        }
    }
}

export function pause_track(){
    return {
        type:"PAUSE_TRACK",
        payload:{
            isPlaying : false
        }
    }
}

actions / actions_tracks.js

import axios from "axios" // axios is used for json fetching

export function fetchTracks(){
    return function(dispatch){

		// case1 : fetch started
        dispatch({type:"FETCH_TRACKS_INIT"}); // no need of store.dispatch() here, because of Thunk middleware

        axios.get("https://api.soundcloud.com/tracks?client_id=...")
        .then((response)=>{
				// case3 : fetch completed
                dispatch({type:"FETCH_TRACKS_FULFILLED", payload: response.data})
        })
        .catch((err)=>{
				// case2 : fetch error
                dispatch({type:"FETCH_TRACKS_REJECTED",payload: err})
        })
    }
}

Store :

A store holds the whole state tree of your application. The only way to change the state inside it is to dispatch an action on it.

A store is not a class. It’s just an object with a few methods on it. To create it, pass your root reducing function to createStore.

store.js

import { applyMiddleware, createStore } from "redux"

import logger from 'redux-logger'
import thunk from "redux-thunk"

import rootReducer from "./reducers/index"

/*

Thunk:
Redux Thunk middleware allows you to write action creators that return a function instead of an action. 
The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. 
The inner function receives the store methods dispatch and getState as parameters.
Here it is mainly used for fetchTracks()

Logger:
Gives a beautiful previousstate-action-nextstate logging

createStore:
This function is used to creat the redux store

*/
const middleware = applyMiddleware( thunk, logger);

export default createStore(rootReducer, middleware);

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

import App from './components/App';
import store from "./store" // importing the store from above store.js

import { Provider } from 'react-redux'

ReactDOM.render(\
// we are globalizing the store
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Components :

Components let you split the UI into independent, reusable pieces, and think about each piece in isolation.

components / Grid.js

/*
   Components are dumb
   Containers are smart
   ~This is a container (I guess :P)
*/

import React, { Component } from 'react';
import './Grid.css';

// importing only two functions from actions_settings
import {play_track, pause_track  } from "../actions/actions_settings"
import {connect} from "react-redux"

class Grid extends Component {


    constructor(props) {
        super(props);
		// this is required to access this in the user created function
        this.onItemClick= this.onItemClick.bind(this);
    }

    onItemClick(event) {
        const {id} = event.target;

		// this.props.settingsObj is a part of the global store.
		
        if(this.props.settingsObj.isPlaying) {
            // === equal compares even the type
            if(id == this.props.settingsObj.trackID) {
                this.props.pause_track();
            }else{
                this.props.play_track(id);
            }

        }else{
            this.props.play_track(id);
        }

    };

  render() {

    var track = this.props.track;
    return (
      <div className="card card-2">


          <div className="container">
              <img className="cover_image" alt="" src={track.artwork_url}  style={{width: "100%" , height:"90%"}}  />
              <div className="middle">
                  <div className="text">
                      <i className={ (this.props.settingsObj.isPlaying && track.id==this.props.settingsObj.trackID) ? "fa fa-pause" :  "fa fa-play" } id={track.id} onClick={this.onItemClick} ></i>
                  </div>
              </div>
          </div>


          <div>

              <div style={{float: "left"}} className="song-card-bottom-div">
                <img className="avatar" alt="" src={track.user.avatar_url}/>
              </div>

              <div style={{margin:"2px"}} className="song-card-bottom-div">

                  <a className="song-card-title">
                    {track.title}
                  </a>

                  <a className="song-card-user-username">
                    {track.user.username}
                  </a>

              </div>

          </div>

      </div>
    );
  }
}

// We map dispatch to props so that we can call the function using props.
// For eg:- this.props.pause_track();

function mapDispatchToProps(dispatch) {

    return({
		// Note this syntax
        pause_track: () => {dispatch(pause_track())},
        play_track: (id) => {dispatch(play_track(id))}
    })
}

// we map state to props. Here settingsObj holds a part of global store. 
function mapStateToProps(store){
    return {
        settingsObj : store.settingsReducer
    }
}


export default connect( mapStateToProps,mapDispatchToProps)(Grid);// Note the syntax

Now we will see how a component, re-renders on state change. In our case, we have MusicBar which has to re-render when the track changes.( when play_track(id) action is dispatched )

components / MusicBar.js

import React, { Component } from 'react';
import './MusicBar.css'

var audio_html_dom;

import {play_track, pause_track  } from "../actions/actions_settings"
import {connect} from "react-redux"


class MusicBar extends Component {

	// only after loading, we can get the html dom object
    componentDidMount() {
        audio_html_dom = document.getElementById("audio-player");
    }


	// ie if the component got re-rendered usually after store change
    componentDidUpdate(){
        if(this.props.settingsObj.isPlaying){
            audio_html_dom.play();
        }else{
            audio_html_dom.pause();
        }
    }
	
	// NOTE: very important
	// This check if we need to re-render the component or not.
	// We change the current state against the next state. Only if they are not equal, we re-render
    componentWillReceiveProps(nextProps) {
       if(this.props.settingsObj.isPlaying !== nextProps.settingsObj.isPlaying) {
            return true;
        }else{
            return false;// no re-rendering is required
        }
    }

    render() {

        return (
           <footer style={{display:(this.props.settingsObj.playedForOnce)?'block': 'none'}} className="fixedBottom" >

               <audio
                   onPlay={ () => this.props.play_track(this.props.settingsObj.trackID)   } // Note: calling function with arguments
                   onPause={this.props.pause_track} // Note: calling function with no arguments
                   id="audio-player"
                   width="100%"
                   src={"https://api.soundcloud.com/tracks/"+ this.props.settingsObj.trackID +"/stream?client_id=..."}
                   type="audio/mp3"
                   controls="controls">
               </audio>;

           </footer >
        );
    }
}


function mapDispatchToProps(dispatch) {
    return({
        pause_track: () => {dispatch(pause_track())},
        play_track: (id) => {dispatch(play_track(id))}
    })
}

function mapStateToProps(store){
    return {
        settingsObj : store.settingsReducer
    }
}


export default connect( mapStateToProps,mapDispatchToProps)(MusicBar);

That’s it ! I guess I covered almost everything.

Upload to Heroku

  1. Create a new project in heroku.
  2. Upload/fork my project in github.
  3. Connect github to heroku.
  4. Select the project repo and deploy.

Also refer

  1. The SoundCloud Client in React + Redux

<Happy Coding />

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s