Typescript class as React state, rerender

Viewed 1191

I have the following function component:

const Player = () => {
  const [spotifyPlayer, setSpotifyPlayer] = React.useState<SpotifyPlayer | null>(null);

  return (
    <SongControls togglePlay={togglePlay} isPlaying={spotifyPlayer.isPlaying} />
  );
};

the SongControls component is responsible for showing the correct control button (start/pause):

const SongControls: React.FC<Props> = ({ isPlaying }: Props) => {
  return (
    {isPlaying ? (
      <Pause style={iconBig} onClick={togglePlay} />
    ) : (
      <PlayArrow style={iconBig} onClick={togglePlay} />
    )}
  );
};

the spotifyPlayer state in the Player component is a class that has a field isPlaying of type boolean. It also has a function defined togglePlay() which toggles isPlaying.

The problem is that when togglePlay() is called React doesn't rerender the components. I understand why (since the instance of SpotifyPlayer is not changing, thats why you shouldn't update react state directly).

I can't call setSpotifyPlayer() to update the state because of 2 reasons:

  • the instance of SpotifyPlayer is responsible for toggling isPlaying
  • when I change the state use js deconstructing I lose the prototype of the spotifyPlayer state and the function won't be available anymore.

I am not able to figure this out.

1 Answers

Your understanding of the problem is correct. You cannot rely on changes to the internal state of the SpotifyPlayer object to trigger a re-render. I recommend using a local state to store the value of isPlaying and update the SpotifyPlayer based on changes to that value.

If the player object has some sort of onChange listener, then I would use the player to set the local state rather than the other way around.

I'm not sure what package the player comes from, so I am assuming this interface:

class SpotifyPlayer {
  isPlaying: boolean = false;
  play(): void {
    this.isPlaying = true;
  }
  pause(): void {
    this.isPlaying = false;
  }
}

We can use useRef to store a consistent reference to a player object.

const spotifyPlayer = React.useRef<SpotifyPlayer>(new SpotifyPlayer()).current;

We will store the value of isPlaying in local state. We can get the initial value from the player.

const [isPlaying, setIsPlaying] = React.useState(spotifyPlayer.isPlaying);

In order to keep isPlaying in sync with the player, we could use useEffect.

const togglePlay = () => setIsPlaying(!isPlaying);

React.useEffect(() => {
  if ( isPlaying && ! spotifyPlayer.isPlaying ) {
    spotifyPlayer.play();
  } else if ( ! isPlaying && spotifyPlayer.isPlaying ) {
    spotifyPlayer.pause();
  }
}, [spotifyPlayer, isPlaying]);

But if togglePlay is the only function which will change the value, then we could handle the changes in that function.

const togglePlay = () => {
   if (isPlaying ) {
       spotifyPlayer.pause();
       setIsPlaying(false);
   } else {
      spotifyPlayer.play();
      setIsPlaying(true);
   }
}
Related