Search code examples
javascriptreactjsreact-nativereact-propsreact-component

How to pass react-native props from functional component to class component


I am creating a music player for a streaming app and I want to make a dynamic playlist. I am using Expo AV and Firebase to store music and information.

I already have everything configured but I can't pass the "cancionesPlaylist" prop (which is already an array) from the parent functional component to the child class component. This is the code I have:

import { StyleSheet, TouchableOpacity, View, Image } from "react-native";
import { Title, Text } from "react-native-paper";
import { LinearGradient } from "expo-linear-gradient";
import { Button } from "../components/Button";
import { Audio, Video } from "expo-av";
import firebase from "../utils/firebase";
import "firebase/firestore";
import { Ionicons } from "@expo/vector-icons";

export default function ReproductorAudio(props) {
  const { route } = props;
  const { canciones } = route.params;
  const cancionesPlaylist = canciones;

  return <ReproductorMusica cancionesPlaylist={cancionesPlaylist} />;
}

class ReproductorMusica extends React.Component {
  constructor(props) {
    super(props);
    const cancionesPlaylist = props.cancionesPlaylist;
    console.log(cancionesPlaylist);
  }

  state = {
    isPlaying: false,
    playbackInstance: null,
    currentIndex: 0,
    volume: 1.0,
    isBuffering: false,
  };

  async componentDidMount() {
    try {
      await Audio.setAudioModeAsync({
        allowsRecordingIOS: false,
        interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
        playsInSilentModeIOS: true,
        interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS,
        shouldDuckAndroid: true,
        staysActiveInBackground: true,
        playThroughEarpieceAndroid: true,
      });

      this.loadAudio();
    } catch (e) {
      console.log(e);
    }
  }

  async loadAudio() {
    const { currentIndex, isPlaying, volume } = this.state;

    try {
      const playbackInstance = new Audio.Sound();
      const source = {
        uri: cancionesPlaylist[currentIndex].uri,
      };

      const status = {
        shouldPlay: isPlaying,
        volume,
      };

      playbackInstance.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
      await playbackInstance.loadAsync(source, status, false);
      this.setState({ playbackInstance });
    } catch (e) {
      console.log(e);
    }
  }

  onPlaybackStatusUpdate = (status) => {
    this.setState({
      isBuffering: status.isBuffering,
    });
  };

  handlePlayPause = async () => {
    const { isPlaying, playbackInstance } = this.state;
    isPlaying
      ? await playbackInstance.pauseAsync()
      : await playbackInstance.playAsync();

    this.setState({
      isPlaying: !isPlaying,
    });
  };

  handlePreviousTrack = async () => {
    let { playbackInstance, currentIndex } = this.state;
    if (playbackInstance) {
      await playbackInstance.unloadAsync();
      currentIndex < cancionesPlaylist.length - 1
        ? (currentIndex -= 1)
        : (currentIndex = 0);
      this.setState({
        currentIndex,
      });
      this.loadAudio();
    }
  };

  handleNextTrack = async () => {
    let { playbackInstance, currentIndex } = this.state;
    if (playbackInstance) {
      await playbackInstance.unloadAsync();
      currentIndex < cancionesPlaylist.length - 1
        ? (currentIndex += 1)
        : (currentIndex = 0);
      this.setState({
        currentIndex,
      });
      this.loadAudio();
    }
  };

  renderFileInfo() {
    const { playbackInstance, currentIndex } = this.state;
    return playbackInstance ? (
      <View style={styles.trackInfo}>
        <Text style={[styles.trackInfoText, styles.largeText]}>
          {cancionesPlaylist[currentIndex].name}
        </Text>
        <Text style={[styles.trackInfoText, styles.smallText]}>
          {cancionesPlaylist[currentIndex].author}
        </Text>
        <Text style={[styles.trackInfoText, styles.smallText]}>
          {cancionesPlaylist[currentIndex].source}
        </Text>
      </View>
    ) : null;
  }

  render() {
    return (
      <View style={styles.container}>
        <Image
          style={styles.albumCover}
          source={{
            uri:
              "http://www.archive.org/download/LibrivoxCdCoverArt8/hamlet_1104.jpg",
          }}
        />
        <View style={styles.controls}>
          <TouchableOpacity
            style={styles.control}
            onPress={this.handlePreviousTrack}
          >
            <Ionicons name="arrow-back-circle-outline" size={48} color="#444" />
          </TouchableOpacity>
          <TouchableOpacity
            style={styles.control}
            onPress={this.handlePlayPause}
          >
            {this.state.isPlaying ? (
              <Ionicons name="ios-pause" size={48} color="#444" />
            ) : (
              <Ionicons name="ios-play-circle" size={48} color="#444" />
            )}
          </TouchableOpacity>
          <TouchableOpacity
            style={styles.control}
            onPress={this.handleNextTrack}
          >
            <Ionicons
              name="arrow-forward-circle-outline"
              size={48}
              color="#444"
            />
          </TouchableOpacity>
        </View>
        {this.renderFileInfo()}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#aaa",
    alignItems: "center",
    justifyContent: "center",
  },
  albumCover: {
    width: 250,
    height: 250,
  },
  trackInfo: {
    padding: 40,
    backgroundColor: "#aaa",
  },
  trackInfoText: {
    textAlign: "center",
    flexWrap: "wrap",
    color: "#550088",
  },
  largeText: {
    fontSize: 22,
  },
  smallText: {
    fontSize: 16,
  },
  control: {
    margin: 20,
  },
  controls: {
    flexDirection: "row",
  },
});

Solution

  • You almost have it you are just missing one conceptual difference between the class and functional components. Namely how props are used inside the component (docs).

    With a simple functional component, it is simply that, a function, and all the props are accessible anywhere in the component. But with a class component the props are bound to the class instance via this.props.

    So now looking at your code, you are creating a local variable cancionesPlaylist inside of the class constructor but that variable is scoped only to within the constructor function block.

    constructor(props) {
      super(props);
      const cancionesPlaylist = props.cancionesPlaylist;
      console.log(cancionesPlaylist);
    }
    

    So when you try to access that local variable anywhere in your class you are actually referencing a likely nonexistent (i.e. undefined) global variable named cancionesPlaylist. What you need to do instead is just access the props on the class instance via this.props.cancionesPlaylist. You have access to this (aka the component/class instance) anywhere inside the class component, but it may not always be the same 🤭 see this for some general gotchas not specific to react.

    Altogether with a little cleanup...

    import { StyleSheet, TouchableOpacity, View, Image } from "react-native";
    import { Title, Text } from "react-native-paper";
    import { LinearGradient } from "expo-linear-gradient";
    import { Button } from "../components/Button";
    import { Audio, Video } from "expo-av";
    import firebase from "../utils/firebase";
    import "firebase/firestore";
    import { Ionicons } from "@expo/vector-icons";
    
    export default function ReproductorAudio(props) {
      const { route } = props;
      const { canciones } = route.params;
      const cancionesPlaylist = canciones;
    
      return <ReproductorMusica cancionesPlaylist={cancionesPlaylist} />;
    }
    
    class ReproductorMusica extends React.Component {
      state = {
        isPlaying: false,
        playbackInstance: null,
        currentIndex: 0,
        volume: 1.0,
        isBuffering: false,
      };
    
      async componentDidMount() {
        try {
          await Audio.setAudioModeAsync({
            allowsRecordingIOS: false,
            interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
            playsInSilentModeIOS: true,
            interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS,
            shouldDuckAndroid: true,
            staysActiveInBackground: true,
            playThroughEarpieceAndroid: true,
          });
    
          this.loadAudio();
        } catch (e) {
          console.log(e);
        }
      }
    
      async loadAudio() {
        const { currentIndex, isPlaying, volume } = this.state;
    
        try {
          const playbackInstance = new Audio.Sound();
          const source = {
            uri: this.props.cancionesPlaylist[currentIndex].uri,
          };
    
          const status = {
            shouldPlay: isPlaying,
            volume,
          };
    
          playbackInstance.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
          await playbackInstance.loadAsync(source, status, false);
          this.setState({ playbackInstance });
        } catch (e) {
          console.log(e);
        }
      }
    
      onPlaybackStatusUpdate = (status) => {
        this.setState({
          isBuffering: status.isBuffering,
        });
      };
    
      handlePlayPause = async () => {
        const { isPlaying, playbackInstance } = this.state;
        isPlaying
          ? await playbackInstance.pauseAsync()
          : await playbackInstance.playAsync();
    
        this.setState({
          isPlaying: !isPlaying,
        });
      };
    
      handlePreviousTrack = async () => {
        let { playbackInstance, currentIndex } = this.state;
        if (playbackInstance) {
          await playbackInstance.unloadAsync();
          currentIndex < this.props.cancionesPlaylist.length - 1
            ? (currentIndex -= 1)
            : (currentIndex = 0);
          this.setState({
            currentIndex,
          });
          this.loadAudio();
        }
      };
    
      handleNextTrack = async () => {
        let { playbackInstance, currentIndex } = this.state;
        if (playbackInstance) {
          await playbackInstance.unloadAsync();
          currentIndex < this.props.cancionesPlaylist.length - 1
            ? (currentIndex += 1)
            : (currentIndex = 0);
          this.setState({
            currentIndex,
          });
          this.loadAudio();
        }
      };
    
      renderFileInfo() {
        const { playbackInstance, currentIndex } = this.state;
        return playbackInstance ? (
          <View style={styles.trackInfo}>
            <Text style={[styles.trackInfoText, styles.largeText]}>
              {this.props.cancionesPlaylist[currentIndex].name}
            </Text>
            <Text style={[styles.trackInfoText, styles.smallText]}>
              {this.props.cancionesPlaylist[currentIndex].author}
            </Text>
            <Text style={[styles.trackInfoText, styles.smallText]}>
              {this.props.cancionesPlaylist[currentIndex].source}
            </Text>
          </View>
        ) : null;
      }
    
      render() {
        return (
          <View style={styles.container}>
            <Image
              style={styles.albumCover}
              source={{
                uri:
                  "http://www.archive.org/download/LibrivoxCdCoverArt8/hamlet_1104.jpg",
              }}
            />
            <View style={styles.controls}>
              <TouchableOpacity
                style={styles.control}
                onPress={this.handlePreviousTrack}
              >
                <Ionicons name="arrow-back-circle-outline" size={48} color="#444" />
              </TouchableOpacity>
              <TouchableOpacity
                style={styles.control}
                onPress={this.handlePlayPause}
              >
                {this.state.isPlaying ? (
                  <Ionicons name="ios-pause" size={48} color="#444" />
                ) : (
                  <Ionicons name="ios-play-circle" size={48} color="#444" />
                )}
              </TouchableOpacity>
              <TouchableOpacity
                style={styles.control}
                onPress={this.handleNextTrack}
              >
                <Ionicons
                  name="arrow-forward-circle-outline"
                  size={48}
                  color="#444"
                />
              </TouchableOpacity>
            </View>
            {this.renderFileInfo()}
          </View>
        );
      }
    }
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        backgroundColor: "#aaa",
        alignItems: "center",
        justifyContent: "center",
      },
      albumCover: {
        width: 250,
        height: 250,
      },
      trackInfo: {
        padding: 40,
        backgroundColor: "#aaa",
      },
      trackInfoText: {
        textAlign: "center",
        flexWrap: "wrap",
        color: "#550088",
      },
      largeText: {
        fontSize: 22,
      },
      smallText: {
        fontSize: 16,
      },
      control: {
        margin: 20,
      },
      controls: {
        flexDirection: "row",
      },
    });
    

    Removed the constructor as it was not needed and changed global variable lookup of cancionesPlaylist to prop lookup on class via this.props.cancionesPlaylist.