Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Working with Audio objects in React/ Redux

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!

like image 522
red house 87 Avatar asked Jun 07 '18 12:06

red house 87


2 Answers

Do not store it in redux' state.

  1. The whole audio object is redundant information (name/path to file, play/pause state, a current time information is sufficient).
  2. Redux and immutable approach are not really suitable for this type of objects in performance point of view.
  3. And, this type of objects is not suitable to work with redux dev tools/logger/human eyes.

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.

like image 36
amankkg Avatar answered Oct 03 '22 10:10

amankkg


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.

  1. What actions are available: See actionCreator
  2. What is your state structure: See initialState in reducer
  3. How is your state updated: See musicPlayer in reducer
  4. What does your app look like: Your presentational concerns and side-effects will be taken care of in your React component. It will be wrapped with connect to subscribe to Redux state.
  5. How does your component interpret the Redux state?: See mapStateToProps

Your Redux state will have 2 keys:

  1. audioTrack: keeps track of active audio track's file name.
  2. 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

  1. Flexibility to add more attributes: If you want to add more attributes, you can store them in state but do remember to store only those attributes which actually affect your component state. For every change in your Redux state, your Reducer will be called and so will your mapStateToProps. This will lead to use-less re-rendering of your components if not used carefully.
  2. Presentation is independent of state logic: In the future, if you want to change just the presentation logic of your component (like using another class instead of Audio), you don't have to worry about changing your reducer code.
  3. Reducer is independent of how data is being consumed: The reducer is independent of the structure of the audio file. It gives you the flexibility of using the same Reducer code in another audio component you may write (for example you need to write a separate component for mobile apps). It's always a good practice to separate your data and state concerns from your presentation concerns.

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

like image 154
yeshashah Avatar answered Oct 03 '22 10:10

yeshashah