In our earlier blog post, we had built a React Native Android app for Video calls using twilio. The main objective of this post is to provide you with tips and some of the best React Native practices which will benefit you and your team in building your React Native projects. Some of these tips will be extremely helpful while building the complex React Native apps.
Assuming that you have followed our earlier blog post and created the app, we will proceed further by improving the same app by suggesting 8 best practice tips for React Native Development.
Tip 1: Check internet connectivity
When you are building your React Native app that needs to pull assets or data from a server, there is a possibility that some users may use the application in an offline environment i.e., without an internet connection. There is a chance that the app might crash. So, for better user experience, we can handle this by checking for an internet connection and notifying the user if not.
This can be done using a package called react-native-offline.
Make the following changes to use react-native-offline within your app.
Step-1:
Create a file TwilioVideo.js within your app. Place the existing code for Video calls from App.js into TwilioVideo.js.
Step-2:
Install the react-native-offline package using the npm package manager. For that execute the following commands within the project directory.
npm install react-native-offline --save
npm install @react-native-community/netinfo --save
cd ios
pod install
Then modify the App.js file like shown below.
App.js:
import React from 'react';
import TwilioVideo from "./TwilioVideo";
import { NetworkProvider } from 'react-native-offline';
const App = () => (
<>
<NetworkProvider>
<TwilioVideo/>
</NetworkProvider>
</>
);
export default App;
In the App.js file, we are importing TwilioVideo component from TwilioVideo.js file.
We must also wrap the twilio component within NetworkProvider as shown.
Step-3:
We also need a presentable component which will be used to notify the user about the internet connection. For that create a file OfflineNotice.js and place the below code in it.
OfflineNotice.js:
import React, { useEffect,useState } from 'react';
import { View, Text, Dimensions, StyleSheet, Platform } from 'react-native';
import { NetworkConsumer } from 'react-native-offline';
const { width } = Dimensions.get('window');
function OfflineNotice(params) {
return (
<>
<NetworkConsumer>
{({ isConnected }) => (
isConnected ? (
null
) : (
<View style={[styles.offlineContainer,{backgroundColor: '#b52424'}]}>
<Text style={styles.offlineText}>No Internet Connection</Text>
</View>
)
)}
</NetworkConsumer>
</>
);
}
const styles = StyleSheet.create({
offlineContainer: {
height: 30,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
width,
position: 'absolute',
zIndex:2,
},
offlineText: { color: '#fff' }
});
export default OfflineNotice;
Now we can use this component in our TwilioVideo component.
Modify the TwilioVideo.js file as shown below.
TwilioVideo.js:
import React, { Component } from "react";
import {
StyleSheet,
Text,
View,
TouchableOpacity,
PermissionsAndroid,
TouchableHighlight,
} from "react-native";
import {
TwilioVideoLocalView,
TwilioVideoParticipantView,
TwilioVideo,
} from "react-native-twilio-video-webrtc";
import MIcon from "react-native-vector-icons/MaterialIcons";
import normalize from "react-native-normalize";
import MCIcon from "react-native-vector-icons/MaterialCommunityIcons";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp,
} from "react-native-responsive-screen";
import InputWithLabel from "./staticComponents/InputWithLabel";
import OfflineNotice from "./staticComponents/OfflineNotice";
export async function getAllPermissions() {
try {
const userResponse = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.CAMERA,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
]);
return userResponse;
} catch (err) {
console.log(err);
}
return null;
}
export default class TwilioVideo extends Component {
state = {
isAudioEnabled: true,
isVideoEnabled: true,
isButtonDisplay: true,
status: "disconnected",
participants: new Map(),
videoTracks: new Map(),
roomName: "",
token: "",
};
componentDidMount() {
getAllPermissions();
}
_onConnectButtonPress = () => {
this.refs.twilioVideo.connect({
roomName: this.state.roomName,
accessToken: this.state.token,
});
this.setState({ status: "connecting" });
};
_onEndButtonPress = () => {
this.refs.twilioVideo.disconnect();
};
_onMuteButtonPress = () => {
this.refs.twilioVideo
.setLocalAudioEnabled(!this.state.isAudioEnabled)
.then((isEnabled) => this.setState({ isAudioEnabled: isEnabled }));
};
_onFlipButtonPress = () => {
this.refs.twilioVideo.flipCamera();
};
_onRoomDidConnect = () => {
this.setState({ status: "connected" });
};
_onRoomDidDisconnect = ({ roomName, error }) => {
this.setState({ status: "disconnected" });
};
_onRoomDidFailToConnect = (error) => {
this.setState({ status: "disconnected" });
};
_onParticipantAddedVideoTrack = ({ participant, track }) => {
this.setState({
videoTracks: new Map([
...this.state.videoTracks,
[
track.trackSid,
{ participantSid: participant.sid, videoTrackSid: track.trackSid },
],
]),
});
};
_onParticipantRemovedVideoTrack = ({ participant, track }) => {
const videoTracks = this.state.videoTracks;
videoTracks.delete(track.trackSid);
this.setState({ videoTracks: { ...videoTracks } });
};
render() {
return (
<View style={styles.container}>
<OfflineNotice />
{this.state.status === "disconnected" && (
<View>
<Text style={styles.headerStyle}>React Native Twilio Video</Text>
<InputWithLabel
label="Room Name"
placeholder="Room Name"
defaultValue={this.state.roomName}
onChangeText={(text) => this.setState({ roomName: text })}
/>
<InputWithLabel
label="Token"
placeholder="Token"
defaultValue={this.state.token}
onChangeText={(text) => this.setState({ token: text })}
/>
<TouchableHighlight
style={[styles.buttonContainer, styles.loginButton]}
onPress={this._onConnectButtonPress}
>
<Text style={styles.Buttontext}>Connect</Text>
</TouchableHighlight>
</View>
)}
{(this.state.status === "connected" ||
this.state.status === "connecting") && (
<View style={styles.callContainer}>
{this.state.status === "connected" && (
<View style={styles.remoteGrid}>
<TouchableOpacity
style={styles.remoteVideo}
onPress={() => {
this.setState({
isButtonDisplay: !this.state.isButtonDisplay,
});
}}
>
{Array.from(
this.state.videoTracks,
([trackSid, trackIdentifier]) => {
return (
<TwilioVideoParticipantView
style={styles.remoteVideo}
key={trackSid}
trackIdentifier={trackIdentifier}
/>
);
}
)}
</TouchableOpacity>
<TwilioVideoLocalView
enabled={true}
style={[
styles.localVideo,
{ bottom: this.state.isButtonDisplay ? "40%" : "30%" },
]}
/>
</View>
)}
<View
style={[
styles.callScreenButtonContainer,
{
display: this.state.isButtonDisplay ? "flex" : "none",
zIndex: this.state.isButtonDisplay ? 2 : 0,
},
]}
>
<TouchableOpacity
style={[
styles.buttonStyle,
{ display: this.state.isButtonDisplay ? "flex" : "none" },
]}
onPress={this._onMuteButtonPress}
>
<MIcon
name={this.state.isAudioEnabled ? "mic" : "mic-off"}
size={24}
color="#fff"
/>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.buttonStyle,
{ display: this.state.isButtonDisplay ? "flex" : "none" },
]}
onPress={this._onEndButtonPress}
>
<MIcon name="call-end" size={28} color="#fff" />
</TouchableOpacity>
<TouchableOpacity
style={[
styles.buttonStyle,
{ display: this.state.isButtonDisplay ? "flex" : "none" },
]}
onPress={this._onFlipButtonPress}
>
<MCIcon name="rotate-3d" size={28} color="#fff" />
</TouchableOpacity>
</View>
</View>
)}
<TwilioVideo
ref="twilioVideo"
onRoomDidConnect={this._onRoomDidConnect}
onRoomDidDisconnect={this._onRoomDidDisconnect}
onRoomDidFailToConnect={this._onRoomDidFailToConnect}
onParticipantAddedVideoTrack={this._onParticipantAddedVideoTrack}
onParticipantRemovedVideoTrack={this._onParticipantRemovedVideoTrack}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "white",
},
buttonStyle: {
width: 60,
height: 60,
marginLeft: 10,
marginRight: 10,
borderRadius: 100 / 2,
backgroundColor: "grey",
justifyContent: "center",
alignItems: "center",
},
callContainer: {
flex: 1,
position: "absolute",
bottom: 0,
top: 0,
left: 0,
right: 0,
minHeight: "100%",
},
headerStyle: {
fontSize: 30,
textAlign: "center",
paddingTop: 40,
},
localVideo: {
width: "35%",
left: "64%",
height: "25%",
zIndex: 2,
},
remoteGrid: {
flex: 1,
flexDirection: "column",
},
remoteVideo: {
width: wp("100%"),
height: hp("100%"),
zIndex: 1,
},
buttonContainer: {
height: normalize(45),
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
marginBottom: 20,
width: wp("90%"),
borderRadius: 30,
},
loginButton: {
backgroundColor: "#1E3378",
width: wp("90%"),
justifyContent: "center",
alignItems: "center",
marginLeft: 20,
marginTop: 10,
},
Buttontext: {
color: "white",
fontWeight: "500",
fontSize: 18,
},
callScreenButtonContainer: {
position: "absolute",
left: 0,
bottom: 0,
right: 0,
height: 100,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-evenly",
},
});
Running the App:
Run the application by executing the react-native run-android command from the terminal window.
Below are the screenshots of the app running on an android device.
The above screenshot is when the device doesn’t have the internet connection. Once the internet connection is established the error message disappears like shown below.
Tip 2: Don’t Repeat Yourself
One of the basic principles of software development is Don’t Repeat Yourself. We must not write the same piece of code twice. Whenever you write the same piece of code twice, you must try to refactor it into something reusable, even if not completely. You can create your own reusable components. For example, if your app contains multiple input fields, you can create a reusable <TextInput> component and use it across any screen within your app. Not only input fields, if your app contains multiple buttons, but you can also create a reusable <Button> component and use it anywhere within your app. Likewise, you can create any number of reusable components based on your app architecture.
Within our app, we have created a <TextInput> component which can be used at multiple places.
Create a file TextInputFieldWithLabel.js and place the below code in it.
TextInputWithLabel.js:
import React from 'react';
import {View, StyleSheet, Text, TextInput, Platform} from 'react-native';
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp,
} from 'react-native-responsive-screen';
export default function InputFieldWithLabelComponent(props) {
return (
<View style={styles.spacing}>
<Text style={styles.inputLabel}>{props.label}</Text>
<TextInput style={styles.inputBox}
placeholder={props.placeholder}
defaultValue={props.defaultValue}
onChangeText={props.onChangeText}
/>
</View>
);
}
const styles = StyleSheet.create({
spacing: {
padding: 10
},
inputLabel: {
fontSize: 18
},
inputBox: {
borderBottomColor: '#cccccc',
fontSize: 16,
width: wp("95%"),
borderBottomWidth: 1
},
});
Then we can import and use the above component within our app as shown below.
<InputWithLabel
label = "Room Name"
placeholder="Room Name"
defaultValue={this.state.roomName}
onChangeText={(text) => this.setState({roomName: text})}
/>
<InputWithLabel
label = "Token"
placeholder="Token"
defaultValue={this.state.token}
onChangeText={(text) => this.setState({token: text})}
/>
Tip 3: Avoid Inline Stylings
Using inline stylings is much harder to maintain if a change is to be made there will be hundreds of places in the code you will have to search and change unless all stylings are clearly defined with unique class names in a CSS stylesheet. For any property you want to change, you would have to simply modify the corresponding class name properties in the stylesheet, all the divs that use the class name will be affected.
A well-defined stylesheet is extremely helpful while building complex React Native apps. Use React Native Stylesheet object to add stylings specific to a certain component.
Tip 4: Exception Handling in React Native Apps
One of the bad user experiences is using a mobile application that crashes with errors that aren’t handled gracefully. So, exception handling plays an important role in making your app run smoothly.
We use the try and catch blocks to handle exceptions within a React Native app.
The try…catch statement marks a block of statements to try, and specifies one or more responses should an exception be thrown. If an exception is thrown, the try…catch statement catches it.
The try…catch statement consists of a try block, which contains one or more statements, and a catch block, containing statements that specify what to do if an exception is thrown in the try block.
If your app consists of a block of code that may throw exceptions, you can handle that in a try-catch block like shown below.
try {
throw new Error("Error");
} catch (error) {
// handle Error
}
Not only these, there are other ways to handle exceptions. For those you can refer here.
Tip 5: Perform the API Calls in componentDidMount()
In order to always have the correct flow of data and rendering the components, you must put all your API calls within the componentDidMount() life cycle method.
Using componentDidMount() makes it clear that data won’t be loaded until after the initial render. This will assist you in setting up the initial state properly, so you don’t end up with an undefined state that results in errors.
Tip 6: Always Perform Both Local and Server Validations
Although, there are some validations or tests which only the server can validate, such as if the entered username or password exists in the database or if the entered email exists in the database. But it is a best practice that you always implement as much client validation as possible such as entering the proper email format, empty field validation and also minimum or maximum number of characters required. So, it is always preferable to perform both local and server validations.
Tip 7: Make Sure Your App is Responsive
You must always make sure that the app you are building is responsive, meaning it is consistent across different devices and platforms.
There are many ways you can attain this behavior to your app. One such way is using the react-native-normalize package. It is a small and simple package that helps in making your React Native app responsive easily.
Tip 8: Add Loading spinners While Fetching The Data Or Waiting For an API Response
This is something that is very easy to implement. Adding Loading Indicators makes your app look more responsive and professional to users.
You can follow this example to add a loading spinner within your app.
That’s it folks! Thanks for the read.
This story is authored by Dheeraj Kumar and Santosh Kumar. Dheeraj is a software engineer specializing in React Native and React based frontend development. Santosh specializes in Cloud Services based development.
Comments