I decided to buy the Creative React and Redux course by Ed, a developer and YouTuber.

The first project was a music app which “hot links” songs from chillhop.com, a music site which creates free music for content creators to add music to their videos or streams.

Getting the data

The song library gets imported into the app using a JavaScript file, data.js, which exports a function, ChillHop(), which returns an array of objects. Each object in the array represents 1 song.

Playing the music

The main App imports a Nav, a Song, a Player, and a Library. The main App also renders the HTML <audio> tag. The useRef React hook to add a ref attribute, audioRef, so that we can reference the <audio> node in the DOM… I think.

useRef is like a “box” that can hold a mutable value in its .current property

Source: reactjs.org

<audio
    onEnded={songEndHandler}
    onLoadedMetadata={timeUpdateHandler}
    onTimeUpdate={timeUpdateHandler}
    ref={audioRef}
    src={currentSong.audio}
    volume={songInfo.volume}
></audio>

The audioRef state gets passed as a prop to the Player and the Library components: both of these components can change which song is currently playing, and when the current song changes or gets paused, audioRef.current.pause() or audioRef.current.play() needs to fire.

What I added on beyond the tutorial

The tutorial did not include a volume control on the song. Every music player needs volume control! So I set about creating it.

The new feature did not require any new components: the UI was done in Player.jsx. It also did not create new state; instead, I added volume: 0.5 to the existing songInfo state.

The next few steps included creating the markup, adding styling, and adding a JavaScript event handler to the onChange event on the volume input slider.

Volume control display

The first item in the container div is an <input> with type range. It uses the songInfo.volume state to know whats its value should be. The min and max properties restrict the input from 0 to 1 because these are the expected values for <audio>. The step was set to 0.1 because at 1, the dragging animation did not feel very nice.

<div className="volume-control">
    <input
        type="range"
        min={0}
        max={1}
        step={0.1}
        onChange={volumeDragHandler}
        value={songInfo.volume}
    />
    <div
    className="animate-volume"
    style={transformVolumeRange}
    ></div>
</div>

There is an illusory second div, animate-volume, which is used to hide or show the input. Its style property is set to a JavaScript object, which uses CSS to move the div in the x-plane. Because translateX uses percentage, and <audio volume> uses decimals (0.0 to 1.0), the songInfo.volume state needs to be multiplied by 100.

const transformVolumeRange = {
    transform: `translateX(${songInfo.volume * 100}%)`
};

The animate-volume div gets placed absolutely within the main div.

  .volume-control {
    background-color: $track_color;
    border-radius: $volume_height;
    height: $volume_height;
    overflow: hidden;
    position: relative;
    margin-top: $volume_height;
    width: 20%;
    input[type="range"]::-webkit-slider-thumb {
      height: $volume_height;
      width: $volume_height;
    }
    .animate-volume {
      background: $shadow_color;
      height: 100%;
      left: 0%;
      position: absolute;
      pointer-events: none;
      top: 0%;
      width: 100%;
    }
  }

Volume Drag Event Handler

When the volume input gets dragged to adjust the volume, it fires the volumeDragHandler. This event handler gets the input volume, updates the volume on the <audio> element, and changes the songInfo state, preserving all other values, but setting the volume equal to the volume received from the input.

const volumeDragHandler = (event) => {
    let volume = event.target.value;
    audioRef.current.volume = volume;
    setSongInfo({...songInfo, volume: volume});
}

Favorite Snippet of Code

Here is my favorite piece of code from this project: Death by ternary operator because the ternary operator is great in my opinion.

This is the event handler for when either the fast-forward or rewind buttons get clicked, deciding which song in the array of song objects should be played next.

  const skipHandler = async (direction) => {
    const curIdx = songs.indexOf(currentSong);
    let newIdx = 0;
    direction === 'forward'                 // you want to skip forward?
      ? curIdx === songs.length - 1         // true: you're on the last song?
        ? newIdx = 0                        // true: go to first song
        : newIdx = curIdx + 1               // false: go to next song
      : curIdx === 0                        // not forward. you're on the first song?
        ? newIdx = songs.length - 1         // true: go to last song
        : newIdx = curIdx - 1               // false: go to previous song
    await setCurrentSong(songs[newIdx]);    // pause JS run-time until setCurrentSong is done
    if (isPlaying) audioRef.current.play(); // play song if applicable
  }