I'm currently building a music app and have a question on the correct way to approach audio objects in terms of storing them and their current state in React/ Redux.
I'm currently dispatching an action in one of my components that's going to the following reducer to set an audio object as part of the state:
Reducer.js
import { fromJS } from 'immutable';
const initialState = fromJS({
audioTrack: false
});
export const musicPlayer = (state = initialState, action) => {
switch (action.type) {
case 'musicPlayer/PLAY_TRACK': {
const mergeObj = {};
const audioTrack = state.get('audioTrack');
mergeObj.audioTrack = audioTrack;
if (!audioTrack) {
mergeObj.audioTrack = new Audio('../../public/music/test.mp3');
mergeObj.audioTrack.play();
} else if (state.get('audioTrack').paused) {
mergeObj.audioTrack.play();
} else {
mergeObj.audioTrack.pause();
}
return state.merge(mergeObj);
}
default: return state
}
}
Basically here if audioTrack
is false
i'm creating a new audio track when someone clicks a play button. I'm then adding the audioTrack
object to the reducers state. From there if the track is set then I can access the audioTrack
object from the reducers state and pause it if I need to as well as call whatever other audio methods I need to.
My issue here is that i'm pretty sure storing the audio object in a reducer is not the correct way to approach something like this. The audio object has some deep nested objects and I would like to keep my reducers as flat as possible for obvious performance reasons.
What would be a better way to approach this? I've though of adding the audio object to the window object and just store its' state there but again not sure if this is the most reasonable approach. Would like to keep any audio elements out of the dom to prevent users from doing a quick inspect element and find the node along with its' source.
Thanks and please let me know if anythings unclear!
Do not store it in redux' state.
You have to store a minimal amount of information about audio. So, any component can subscribe to this state. For example, instantiate the audio object on componentDidMount
and store it in local state, switch audio on getDerivedStateFromProps/componentWillReceiveProps
, destroy audio object on componentWillUnmount
, etc.
Also, if you want to hide the information about the audio, then it is yet another reason to use component's local state.
First, the approach to change the type of a state key (audioObject
from boolean
to class Audio
) is not a recommended approach. You should refrain from changing the type of a state key as it can lead to many unforeseen bugs. It is also difficult for a human reader to figure out its use while skimming through the code.
Now, seeing your use-case (keeping track of only a single audio at a time), I feel that separation of concerns is not really clear in your current code which is why the confusion whether audioTrack should be maintained in a component or reducer state. Let's improve upon that using Redux.
We will apply Separation of Concerns in following way.
actionCreator
initialState
in reducermusicPlayer
in reducerconnect
to subscribe to Redux state.mapStateToProps
Your Redux state will have 2 keys:
audioTrack
: keeps track of active audio track's file name.isPlaying
: keeps track of whether track is playing or paused.Your actionCreator will look like this:
// sets active track
export const setActiveTrack (activeTrack) => ({
type: 'musicPlayer/SET_ACTIVE_TRACK',
payload: activeTrack,
});
// plays active track
export const playTrack () => ({
type: 'musicPlayer/PLAY_TRACK',
payload: true,
});
// pauses active track
export const pauseTrack () => ({
type: 'musicPlayer/PLAY_TRACK',
payload: false,
});
Your reducer will work as follows:
//always use null as an indicator of empty.
const initialState = {
audioTrack: null,
isPlaying: false,
};
export const musicPlayer = (state = initialState, action) => {
switch (action.type) {
case 'musicPlayer/SET_ACTIVE_TRACK':
return {
...state,
audioTrack: action.payload,
}
case 'musicPlayer/PLAY_TRACK':
return {
...state,
isPlaying: action.payload,
}
default: return state
}
}
Your mapStateToProps will look like so:
mapStateToProps(state) {
return {
audioTrack: createSelector(state.audioTrack, audioTrack => new Audio(audioTrack)), // `createSelector` will return the same old Audio instance unless the `audioTrack` value changes
isPlaying: state.isPlaying
}
}
Finally your presentational component:
// Take care of side-effects in componentDidMount (for first render) or componentDidUpdate (for all other renders)
componentDidUpdate() {
if(this.props.audioTrack) {
const audioTrack = this.props.audioTrack;
if(this.props.isPlaying) audioTrack.play();
else audioTrack.pause();
}
}
render() {
return ...; // your presentation logic
}
Playing/pausing an audio file is a side-effect which should be taken care of in componentDidUpdate
and componentDidMount
according to React 16 guidelines. You can check that out in the Notes
section here: https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops
Wrapping an audio file with an Audio Class is done in mapStateToProps
using reselect/createSelector
because createSelector
will return the older Audio file if the value of state.audioTrack
has not changed. This is required for pausing after playing an audio file.
createSelector: https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc
(I have assumed here that the user will work only on a single file at a time. Once, he changes the audio file, it can be discarded. In case, you want to maintain a list of active audio files, you can use a generator function which generates the audio given the file name.)
Performance
As long as perf is concerned, you need not worry about the large Audio object. Its reference is not stored anywhere except by createSelector
. When the user changes the active audio track, the older track will be free to be collected in the next GC cycle.
Scalability
mapStateToProps
. This will lead to use-less re-rendering of your components if not used carefully.I will recommend checking out this Video series by Redux Creator Dan Abramov to understand how to use Redux: https://egghead.io/courses/building-react-applications-with-idiomatic-redux
I have demonstrated how to integrate your use-case using Redux as I got an idea that you are trying to learn Redux. But for such a simple application, you might not need Redux. You can simply use the concept of Container (data/state) and presentational components. Check out this amazing article by Dan Abramov: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With