1

Problem

My component renders every second instead of only rendering if the state passed into setCurrentlyPlaying() is different. How can I fix this behavior?

Background

I would like to check an API every 1 second. Aside from the first render, I only want my component to rerender when the incomingSong differs from the currentlyPlayingSong. I'm banking on the State hook's ability to only trigger a rerender if the passed in state is different than the previous based on the Object.is comparison algorithm. Even if I only return(<div></div>); in my component it continues to render every 1 second, so it seems like the problem stems from this logic. I test this by playing NOTHING on my Spotify so it always goes into the first if statement and it still rerenders every second (prints the console.log("RENDERED") statement).

    const [currentlyPlayingSong, setCurrentlyPlayingSong] = useState({ type: 'Now Playing', name: 'No Song Playing', artists: [], imageUrl: ''});

    console.log("RENDERED")

    useEffect(() => {
        console.log("initial effect including updateSongs");

        const updateSongs = async () => {
            const result = await getCurrentlyPlaying();

            var incomingSong = {};

            // no song is playing
            if(result.data === "") {
                incomingSong = { type: 'Now Playing', name: 'No Song Playing', artists: [], imageUrl: ''};
            }
            // some song is playing
            else {
                var songJSONPath = result.data.item;
                incomingSong = {
                    type: 'Now Playing', 
                    name: songJSONPath.name,
                    id: songJSONPath.id,
                    artists: songJSONPath.artists.map(artist => artist.name + " "),
                    imageUrl: songJSONPath.album.images[songJSONPath.album.images.length - 2].url,
                };
            }

            // console.log("currentlyPlayingSong: ", currentlyPlayingSong)
            // console.log("incomingSong: ", incomingSong);

            // this line!!!!
            setCurrentlyPlayingSong(incomingSong);
        }

        updateSongs();

        const timer = setInterval(updateSongs, 1000);

        return () => clearInterval(timer);
    },[]);
sj123
  • 105
  • 2
  • 9
  • Console logging in the body of a functional component ***is not actually*** an accurate measure for how often a component is rendered ***to the DOM***, you should do this in an `useEffect` hook. Also, `updateSongs` creates a new `incomingSong` object reference each time it's invoked, and since react uses shallow reference equality it will trigger a rerender every time you `setCurrentlyPlayingSong(incomingSong);`. You should do this conditional check manually *before* updating state with a new value. How you determine equality between the objects is up to you. `id` seems a good choice. – Drew Reese Dec 29 '20 at 06:40
  • @sj123 but you're always passing a new object to `setCurrentlyPlayingSong` so, there's no way React can understand that it got the same song. – Ramesh Reddy Dec 29 '20 at 06:43

2 Answers2

2

Console logging in the body of a functional component is not actually an accurate measure for how often a component is rendered to the DOM, you should do this in an useEffect hook.

Also, updateSongs creates a new incomingSong object reference each time it's invoked, and since react uses shallow reference equality it will trigger a rerender every time you setCurrentlyPlayingSong(incomingSong);. You should do this conditional check manually before updating state with a new value. How you determine equality between the objects is up to you. id seems a good choice.

Check if the song id is different and only update state when it is actually a different song.

if (currentlyPlayingSong.id !== incomingSong.id) {
  setCurrentlyPlayingSong(incomingSong);
}

code

const [currentlyPlayingSong, setCurrentlyPlayingSong] = useState({ 
  type: 'Now Playing',
  name: 'No Song Playing',
  artists: [],
  imageUrl: '',
});

useEffect(() => {
  console.log("RENDERED"); // <-- logs when component is rendered to DOM
});

useEffect(() => {
  console.log("initial effect including updateSongs");

  const updateSongs = async () => {
    const result = await getCurrentlyPlaying();

    let incomingSong = {};

    // no song is playing
    if (result.data === "") {
      ...
    }
    // some song is playing
    else {
      ...
    }

    // check if song id is different and only update if true
    if (currentlyPlayingSong.id !== incomingSong.id) {
      setCurrentlyPlayingSong(incomingSong);
    }
  }

  updateSongs();

  const timer = setInterval(updateSongs, 1000);

  return () => clearInterval(timer);
},[]);
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thank you very much! I did not notice that it was a new object every time. It now works how I want it to. I ended up using the lodash _.isEqual() method. Also, didn't realize that console.log() outside the useEffect() is not accurate. – sj123 Dec 29 '20 at 07:06
2

Assuming a song's name indicates the identity of a song. You can add an if statement before updating the state.

if (currentlyPlayingSong.name !== incomingSong.name) {
 setCurrentlyPlayingSong(incomingSong);
}

This will make sure to update the state only when the song has changed. If name isn't the correct property to check then replace it with the correct one.

React has no way of understanding that it can prevent the state update as you're passing a new object every time. See this to know more about react compares different values.

Ramesh Reddy
  • 10,159
  • 3
  • 17
  • 32