Integrating React Functional Components with a SoundCloud Widget

Published

Earlier this month, I was asked by a friend to help add music playback to a fitness platform he was developing. After some research, we concluded that the easiest option would be to use the SoundCloud Widget API to embed a player inside of the platform web app.

The web app was built using ReactJS, along with its relatively new Functional Components and Hooks. Performing the integration via Hooks was challenging at first, but very educational. Check out the completed feature and the key integration points below!

The Completed Feature

The SoundCloud music player I developed fit into a larger web app that shared data via an Apollo Client and GraphQL data backend. Play/Pause events are initiated by instructors, and broadcast out to clients. The React app running on the client machines receives the events, and updates it’s local SoundCloud player instance to match.

Simplified Code is Available on GitHub

In the fitness platform app, events are received from the backend and passed into the SoundCloud player component as props. For demonstration purposes, I’ve taken the same integration logic that examined these props, and simplified it work off of local state instead.

Sample code has been posted on GitHub. It also shows how useEffects can control an embedded SoundCloud player based on updates occurring from within a React app.

Integration Points

The two main areas enabling integration between React and the SoundCloud Widget were:

  1. Setting events listeners on the SoundCloud Player PLAY and PAUSE events to detect if the user clicked play, changed the song, etc.
  2. Defining useEffect functions to update the SoundCloud Player based changes occuring within the React app

1. Setting SoundCloud Event Handlers

Since the end user controlled music playback via the SoundCloud Widget itself, I needed to monitor the player events (e.g. play button click, playlist selection, etc.) in order to keep the React app state in sync.

I set event handlers on lines 24-31 and 33-38 in code below. Oddly, these callback functions establish a closure that captures a snapshot of the react state and props at the time of creation (despite the use of lambda expressions). Luckily, functions that set React state work as expected when used with parameters made available by the SoundCloud Widget API (e.g. playerPlaylistIndex on line 29).

// state hooks
const [isPlaying, setIsPlaying] = useState(false)
const [playlistIndex, setPlaylistIndex] = useState(0)
const [player, setPlayer] = useState(false)

// ref for iframe element - https://reactjs.org/docs/refs-and-the-dom.html
const iframeRef = createRef()

// initialization - load soundcloud widget API and set SC event listeners

useEffect(() => {

  // use load-script module to load SC Widget API
  loadscript('https://w.soundcloud.com/player/api.js', () => {
    
    // initialize player and store reference in state
    const player = window.SC.Widget(iframeRef.current)
    setPlayer( player )
  
    const { PLAY, PLAY_PROGRESS, PAUSE, FINISH, ERROR } = window.SC.Widget.Events

    // NOTE: closures created - cannot access react state or props from within and SC callback functions!!

    player.bind( PLAY, () => {
      // update state to playing
      setIsPlaying(true)
      // check to see if song has changed - if so update state with next index
      player.getCurrentSoundIndex( (playerPlaylistIndex) => {            
        setPlaylistIndex(playerPlaylistIndex)
      })
    })

    player.bind( PAUSE, () => {
      // update state if player has paused - must double check isPaused since false positives
      player.isPaused( (playerIsPaused) => {
        if (playerIsPaused) setIsPlaying(false)
      })
    })

  })

}, [])

2. Defining useEffect Hooks to Update the SoundCloud Player

The second integration point deals with keeping the SoundCloud Widget in sync with updates that originate within the React app. For example, in the fitness platform use-case, client applications receive play/pause/seek events from the backend as props, and update their local SoundCloud players as needed. For demonstration purposes, this logic has been simplified to monitor local app state instead.

// state referenced below
const [player, setPlayer] = useState(false)
const [isPlaying, setIsPlaying] = useState(false)
const [playlistIndex, setPlaylistIndex] = useState(0)

// adjust playback in SC player to match isPlaying state
useEffect(() => {
  
  if (!player) return // player loaded async - make sure available

  player.isPaused( (playerIsPaused) => {
    if (isPlaying && playerIsPaused) {
      player.play()
    } else if (!isPlaying && !playerIsPaused) {
      player.pause()
    }
  })
  
},[isPlaying])

// adjust seleted song in SC player playlist if playlistIndex state has changed
useEffect(() => {
  
  if (!player) return // player loaded async - make sure available

  player.getCurrentSoundIndex( (playerPlaylistIndex) => {            
    if (playerPlaylistIndex !== playlistIndex) player.skip(playlistIndex)
  })
  
},[playlistIndex])

Wrap Up and Reflection

React Functional Components took some getting use to, however I did develop an appreciation for the use of hooks to manage “side-effects”, or integrations with other libraries in my case. I particularly like how a list of dependencies is included when defining useEffects – this ensures the logic is only executed when needed. I am wondering if this can be used as a direct replacement for using the shouldComonentUpdate() lifecycle function to cut down on excess renders. Will have to stay tuned!

Subscribe by Email

Enter your email address below to be notified about updates and new posts.


Comments

Loading comments..

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *