Just-in-Time Learning initiative

Version 1.3

This tutorial shows you how to add a loading indicator to show when a mobile app is busy. In our case, we'll create an animated spinner that occupies the screen while users are waiting for a login confirmation.

We'll assume you know how to make custom components in React Native, have some familiarity with modern JavaScript, and have a React Native app with a functional login screen, as explained in previous tutorials.

Note: this draft presents only a synopsis of this tutorial. Please watch the video for details.

Decide when to show a loading message


const WelcomeScreen = ({ navigation }) => {
  const [ isLoggedIn, setIsLoggedIn ] = useState( false );
  const [ accountName, setAccountName ] = useState( "" );
  const [ isLoading, setIsLoading ] = useState( false );
  ...

const WelcomeScreen = ({ navigation }) => {
	const [ isLoggedIn, setIsLoggedIn ] = useState( false );
	const [ accountName, setAccountName ] = useState( "" );
	const [ isLoading, setIsLoading ] = useState( false );
	const showLoading = () => {
		setIsLoading( true );
	};
	const hideLoading = () => {
		setIsLoading( false );
	};
	...

🤔 Why do we seem to be repeating the purpose of a function we already have, ie setIsLoading? Because in my app so far, the actual decisions about whether the user is logged in are being made in a different component, namely LoginForm.js. In order to "lift the state," as described in an earlier tutorial, we will need to create the loading function in the <WelcomeScreen> parent but call it from the <LoginForm> child.


return (
    ...
	<LoginForm
		acceptUser={ acceptUser }
		rejectUser={ rejectUser }
		showLoading={ showLoading }
		hideLoading={ hideLoading }
	/>

const LoginForm = ( props ) => {
  const [ email, setEmail ] = useState( "" );
  const [ password, setPassword ] = useState( "" );
  const logInUser = () => {
    props.showLoading();
    firebase
      .auth()
      .signInWithEmailAndPassword( email, password )
  ...

const WelcomeScreen = ({ navigation }) => {
...	
const acceptUser = ( email ) => {
	setIsLoggedIn( true );
	Alert.alert(`Welcome ${ email }!`);
	setAccountName( email );
	hideLoading();
};
const rejectUser = () => {
	setIsLoggedIn( false );
	Alert.alert(`We're sorry, but those aren't valid credentials`);
	hideLoading();
};

Add a loading text

In a previous tutorial, we saw how to use ternary operators to display conditional content, since you're not allowed to use if statements in JSX. You start with a variable or condition to evaluate, then add a question mark. If the condition is true, JavaScript executes what's before the colon. If not, JavaScript executes what comes after the colon.


{ couldItBe
	? // JSX if true.
	: // JSX if false.
}

return !isLoading ? (
	<View style={ styles.welcomeContainer }>
		<Text style={ styles.welcomeText }>{ accountName }</Text>
		 {
				!isLoggedIn ? (
					<LoginForm
						acceptUser={ acceptUser }
						rejectUser={ rejectUser }
						showLoading={ showLoading }
						hideLoading={ hideLoading }
					/>
				 ) : (
						<SubmitButton whenPressed={
							() => navigation.navigate( "Pets" )
						}
					>
						See lost pets
					</SubmitButton>
			   )
		 }
		</View>
) : (
		<Text>Loading...</Text>
);

Yay! Now we have a loading message.

Make a simple loading animation

If your user thinks your app has stalled, showing something moving on the screen will help cue them otherwise. So let's replace that static text with the animated <ActivityIndicator> that comes automatically with React Native.


import React from "react";
import { ActivityIndicator } from "react-native";
const Spinner = () => {
	return (
	    <ActivityIndicator />
	)
};
export default Spinner;

import React from "react";
import { StyleSheet, View, ActivityIndicator } from "react-native";
const Spinner = () => {
	return (
	  <View style={styles.spinnerContainer}>
    	<ActivityIndicator />
  	  </View>;
	)
};
export default Spinner;
const styles = StyleSheet.create({
  spinnerContainer: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
});

const Spinner = () => {
	return (
	  <View style={styles.spinnerContainer}>
    	<ActivityIndicator size="large" color="gray" />
  	  </View>;
	)
};

Set default spinner values

It's convenient to have values for size and color already set, but we should leave open the possibility that we want to change those if we use our <Spinner> component in a future app. Fortunately, we can set default values in our component definition that can be overridden by arguments passed to the component later.

The verbose way of setting a default variable is:


if ( typeof colorArgument === "undefined" ) {
	color = "gray" ;
}
else {
	color = colorArgument ;
}

This is easy to understand for a JavaScript coder, but it takes a lot of lines, especially if you're dealing with fancy component with a dozen parameters like width, height, fontSize, fontFamily, and so on. Plus we can't put if statements in JSX.

Fortunately, as we saw in previous tutorials, we can use the ternary operators ? and : to make the code more concise:


const Spinner = ( {sizeArgument, colorArgument} ) => {
	return (
		  <View style={styles.spinnerContainer}>
		    <ActivityIndicator
				size={ typeof sizeArgument === "undefined" ? "large" : sizeArgument }
				color={ typeof colorArgument === "undefined" ? "gray" : colorArgument }
			/>
		  </View>;
	)
};

Or, since the JSK attributes are not technically variables, we can use the same name (like color) for the attribute and its value.


const Spinner = ( {size, color} ) => {
	return (
		  <View style={styles.spinnerContainer}>
		    <ActivityIndicator
				size={ typeof size === "undefined" ? "large" : size }
				color={ typeof color === "undefined" ? "gray" : color }
			/>
		  </View>;
	)
};

This syntax is simpler but still rather ugly. Programmers who are obsessed with making code as concise as possible have hit upon an even shorter option, which looks like this:


const Spinner = ( {size, color} ) => {
	return (
		  <View style={styles.spinnerContainer}>
		    <ActivityIndicator size={ size || "large" } color={ color || "gray" } />
		  </View>;
	)
};

Wow, that's really short! But I thought the || operator meant "OR," which doesn't seem to make sense in this context since there should be only one outcome 🤔

It's true that in normal JavaScript, the || operator means "OR," which is a condition that is true if one or both of the separated expressions is true.


10 > 5 ✅  ||  20 > 5 ✅  // Both are true, so the result is true.
10 > 5 ✅  ||  5 > 20 ❌  // Only the first is true, but the result is still true.

Programmers who aim for the shortest possible code realized two things:

The combination of these two facts means that JavaScript will skip over an answer in a ternary if it's undefined. That lets you turn a variable assignment that looks like this:


typeof color === "undefined" ? color : "gray"

into an assignment that looks like this:


color = color || "gray" ;

⚠️ Although this syntax appears often in React Native, it's controversial. If the value passed to the function is one of these, your code may not do what you expect:


		false
		0
		null
		""
		NaN

We saw before that JavaScript interprets undefined as false, but it does that for all of these other "falsy" examples too. That is, if you force JavaScript to interpret anything in that list as a Boolean, JavaScript will return false. This can produce the wrong result.

Say you have a component whose default borderWidth is 4 but you want it to have no border. What will happen if you pass in a borderWidthArgument of 0 and JavaScript gets to this part of the code?


borderWidth = borderWidthArgument || 4 ;

JavaScript will see that borderWidthArgument is 0, but because it's part of a Boolean (true or false) expression, it will interpret 0 to mean false. You'll get a border 4 pixels wide even though you asked for no border at all!

The fact that JavaScript is loosey-goosey about variable types can be a bug or feature depending on your perspective. If you want to be safe, use the full ternary expression. If you want to live dangerously, well, feel free to use the double pipe syntax--it's your choice!

(By the way, NaN stands for "Not a Number," and is what JavaScript will return if it expected a number as input but you gave it a string or another variable type.)

I'll play it safe for now. Here's our finished Spinner.js.


import React from "react";
import { StyleSheet, ActivityIndicator } from "react-native";
const Spinner = ( {size, color} ) => {
	return (
		  <View style={styles.spinnerContainer}>
				size={ typeof size === "undefined" ? "large" : size }
				color={ typeof color === "undefined" ? "gray" : color }
		  </View>;
	)
};
export default Spinner;
const styles = StyleSheet.create({
  spinnerContainer: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
});

Add the loading animation to our welcome screen


import Spinner from "../components/Spinner";

import React, { useState } from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { View, StyleSheet, Text, Alert } from "react-native";
import SubmitButton from "../components/SubmitButton";
import LoginForm from "../components/LoginForm";
import Spinner from "../components/Spinner";
const WelcomeScreen = ({ navigation }) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [accountName, setAccountName] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const showLoading = () => {
    setIsLoading(true);
  };
  const hideLoading = () => {
    setIsLoading(false);
  };
  const acceptUser = (email) => {
    setIsLoggedIn(true);
    Alert.alert(`Welcome ${email}!`);
    setAccountName(email);
    hideLoading();
  };
  const rejectUser = () => {
    setIsLoggedIn(false);
    Alert.alert(`We're sorry, but those aren't valid credentials`);
    hideLoading();
  };
  return !isLoading ? (
    <View style={styles.welcomeContainer}>
      <Text style={styles.welcomeText}>{accountName}</Text>
      {!isLoggedIn ? (
        <LoginForm
			acceptUser={acceptUser}
			rejectUser={rejectUser}
			showLoading={showLoading}
			hideLoading={hideLoading}
        />
      ) : (
        <SubmitButton whenPressed={() => navigation.navigate("Pets")}>
			See lost pets
        </SubmitButton>
      )}
    </View>
  ) : (
    <Spinner color="teal" />
  );
};
export default WelcomeScreen;
const styles = StyleSheet.create({
  welcomeContainer: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "paleturquoise",
  },
  welcomeText: {
    fontSize: 20,
    marginBottom: 20,
    color: "teal",
  },
});

Make a custom loading animation

If you want something more distinctive than the generic spinner, you can make nice-looking loaders of your own or incorporate a variety of others into your app using the using the React Native Lottie library.