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
- Create a new project in heroku.
- Upload/fork my project in github.
- Connect github to heroku.
- Select the project repo and deploy.
Also refer
<Happy Coding />