首页 > 解决方案 > Infinite re-renders when updating useState from streaming audio function

问题描述

I'm building an app React Native where I'm sending an array of audio files into an Expo AV Audio.Sound object, loading them, playing them, and then attempting to update the display of the app itself with info about the audio file being played (specifically how far through the file the user is). I'm trying to update the display through the useState hook which is being called by a callback function from the audio player.

The problem I'm running into is that anytime I try to change the state from the audio player callback function I get thrown into an infinite re-render. Simplified code is below:

import React, { useState} from 'react';
import { Audio } from 'expo-av';

const AudioPlayer = ({ user }) => {
    const [currentProgress, setCurrentProgress] = useState(0);

    const soundObject = new Audio.Sound();
    soundObject.setOnPlaybackStatusUpdate(playbackUpdate);
    // sets a function that is called every 500 milliseconds as the audio is played 

    if(user) {
          soundObject.loadAsync({user.message.path});
    }

    const play = () => {
          soundObject.playAsync();
    }

    const playbackUpdate = (playbackObject) => {
          setCurrentProgress(playbackObject.currentMillis);
          // updating state with progress through audio file in milliseconds
    }

    return (
          <View>
             <Text>{currentProgress}</Text>
             <Button title="play" onPress={play} />
          </View>
    )

}

export default AudioPlayer

标签: javascriptreactjsreact-nativeaudioreact-hooks

解决方案


Remember that everything in your function body will run on every render - so in this case you are creating a new soundObject and potentially running the soundObject.loadAsync call on every single render. You'll need to take advantage of other hooks to avoid this - in your case likely useRef and useEffect. I would recommend getting familiar with these through the hooks api reference: https://reactjs.org/docs/hooks-reference.html

Here's a quick stab at how I would avoid the unnecessary effects. You'll probably want to review and tweak the dependency arrays depending on how you want things to function and when you want the various effects to be re-run. I'm not sure if you ever need the Sound object to be re-created for example.

import React, { useState, useRef, useCallback, useEffect} from 'react';
import { Audio } from 'expo-av';
import { Button, View, Text } from 'react-native';

const AudioPlayer = ({ user }) => {
    const [currentProgress, setCurrentProgress] = useState(0);

    const soundObjectRef = useRef(new Audio.Sound());

    useEffect(() => {

      const playbackUpdate = (playbackObject) => {
          setCurrentProgress(playbackObject.currentMillis);
          // updating state with progress through audio file in milliseconds
      }
      soundObjectRef.current.setOnPlaybackStatusUpdate(playbackUpdate);
    }, []); // do this only once per component mount
    // sets a function that is called every 500 milliseconds as the audio is played 

    useEffect(() => {
      if (user) {
        soundObjectRef.current.loadAsync({user.message.path});
      }
    }, [user]); // run this anytime user changes but do not run again if user doesn't change

    const play = () => {
          soundObjectRef.current.playAsync();
    }

    return (
          <View>
             <Text>{currentProgress}</Text>
             <Button title="play" onPress={play} />
          </View>
    )

}

export default AudioPlayer


推荐阅读