blog bg

July 29, 2024

Streamlining Onboarding in React Native

Share what you learn in this blog to prepare for your interview, create your forever-free profile now, and explore how to monetize your valuable knowledge.

 

In mobile app development, a smooth and intuitive onboarding experience is crucial for user retention and satisfaction. In this blog post, I’ll walk you through my onboarding workflow for a React Native application using TypeScript. We'll cover everything from loading custom fonts and managing onboarding states to configuring navigations.

 

Prerequisite

Before moving along with this post, you’ll need to set up a React Native project and install the necessary navigation packages. If you haven’t already done this, please refer to my previous blog posts where I provide detailed instructions on setting up a React Native environment and configuring React Navigation:


Once your environment is set up and the required packages are installed, you’re ready to follow along with this workflow.

 

The Onboarding Workflow Overview


Our onboarding workflow is designed to ensure that users who are new to the app are guided through a series of introductory screens. For returning users, the app skips the onboarding process and directly navigates to the main content.

 

Here's a high-level view of the steps involved:

  • Loading Custom Fonts
  • Checking Onboarding State
  • Setting Up Navigation
  • Creating Onboarding Screens
  • Handling Navigation Logic

 

Let's dive into each step in detail.

 

Loading Custom Fonts

To create a visually appealing onboarding experience, we first need to load custom fonts. This is done using a custom hook, useLoadFonts.

import { useEffect } from "react";
import * as SplashScreen from "expo-splash-screen";
import {
  useFonts,
  HankenGrotesk_100Thin,
  HankenGrotesk_200ExtraLight,
  HankenGrotesk_300Light,
  HankenGrotesk_400Regular,
  HankenGrotesk_500Medium,
  HankenGrotesk_600SemiBold,
  HankenGrotesk_700Bold,
  HankenGrotesk_800ExtraBold,
  HankenGrotesk_900Black,
} from "@expo-google-fonts/hanken-grotesk";

export default function useLoadFonts() {
  const [loaded, error] = useFonts({
    HankenGrotesk_100Thin,
    HankenGrotesk_200ExtraLight,
    HankenGrotesk_300Light,
    HankenGrotesk_400Regular,
    HankenGrotesk_500Medium,
    HankenGrotesk_600SemiBold,
    HankenGrotesk_700Bold,
    HankenGrotesk_800ExtraBold,
    HankenGrotesk_900Black,
  });

  /**
   * Hide the Application's Splash screen when the custom font is loaded
   */
  useEffect(() => {
    if (loaded || error) {
      SplashScreen.hideAsync();
    }
  }, [loaded, error]);

  // Return the loaded value and error value
  return { loaded, error };
}

 

 

Checking Onboarding State

To determine whether to show the onboarding screens or not, we use the useOnboarding hook. This hook checks if the user has previously completed the onboarding process by storing a flag in AsyncStorage.

 

import { useEffect, useState } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";

export default function useOnboarding() {
  const [loading, setLoading] = useState(true);
  const [viewedOnboarding, setViewedOnboarding] = useState(false);
  const [initialRouteName, setInitialRouteName] = useState<"OnboardingStack" | "AppStack" | null>(null);

  async function checkOnboardingHandler() {
    try {
      const value = await AsyncStorage.getItem("@viewedOnboarding");
      setViewedOnboarding(value !== null);
    } catch (error) {
      console.log("Error @CheckOnboarding: ", error);
    } finally {
      setLoading(false);
    }
  }

  async function setOnboarded() {
    try {
      await AsyncStorage.setItem("@viewedOnboarding", "true");
    } catch (error) {
      console.log("Error @setItem: ", error);
    }
  }

  useEffect(() => {
    checkOnboardingHandler();
  }, []);

  useEffect(() => {
    if (!loading) {
      setInitialRouteName(viewedOnboarding ? "AppStack" : "OnboardingStack");
    }
  }, [loading, viewedOnboarding]);

  return { initialRouteName, loading, viewedOnboarding, setOnboarded };
}

 

The hook determines whether to display the onboarding stack or the app stack based on whether the user has completed the onboarding.

You also need to install the AsyncStorage package to use it:

npm install @react-native-async-storage

 

Setting Up Navigation

We use React Navigation to set up our stack navigators for onboarding and the main app. Here’s how we configure the MainNavigation component:

 

import { RootStackParamList } from "./types";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

import useOnboarding from "../hooks/useOnboarding";

import OnboardingStackNavigator from "./OnboardingStackNavigator";
import AppStackNavigator from "./AppStackNavigator";

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function MainNavigation() {
  const { initialRouteName } = useOnboarding();

  if (initialRouteName === null) {
    return;
  }

  return (
    <NavigationContainer>
      <Stack.Navigator
        screenOptions={{ headerShown: false }}
        initialRouteName={initialRouteName}
      >
        <Stack.Screen name="AppStack" component={AppStackNavigator} />
        <Stack.Screen
          name="OnboardingStack"
          component={OnboardingStackNavigator}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

 

Creating Onboarding Screens

The onboarding stack consists of a single onboarding screen. Here’s the setup for OnboardingStackNavigator:

 

import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

import Onboarding from "../screens/Onboarding";
import { OnboardingStackParamList } from "./types";

const Stack = createNativeStackNavigator<OnboardingStackParamList>();

export default function OnboardingStackNavigator() {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      <Stack.Screen name="Onboarding" component={Onboarding} />
    </Stack.Navigator>
  );
}


Similarly, the app stack contains the main content screens:

import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

import Home from "../screens/Home";
import { AppStackParamList } from "./types";

const Stack = createNativeStackNavigator<AppStackParamList>();

export default function AppStackNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={Home} />
    </Stack.Navigator>
  );
}

 

Handling Navigation Logic

The onboarding screen (Onboarding.tsx) handles user interactions such as completing the onboarding and logging in:

 

import { useRef } from "react";
import {
  Animated,
  FlatList,
  SafeAreaView,
  StyleSheet,
  View,
} from "react-native";
import { CommonActions } from "@react-navigation/native";

import { SIZES } from "../constants/themes";
import slides from "../constants/onboarding";

import Paginator from "../components/ui/Paginator";
import PrimaryButton from "../components/buttons/PrimaryButton";
import TertiaryButton from "../components/buttons/TertiaryButton";
import OnboardingItem from "../components/onboarding/OnboardingItem";

import useOnboarding from "../hooks/useOnboarding";
import { useNavigation } from "@react-navigation/native";
import { RootNavigationProp } from "../navigations/types";

export default function Onboarding() {
  const { setOnboarded } = useOnboarding();
  const navigation = useNavigation<RootNavigationProp>();

  function getStartedHandler() {
    setOnboarded();

    navigation.dispatch(
      CommonActions.reset({
        index: 0,
        routes: [{ name: "AppStack" }],
      })
    );
  }

  function loginHandler() {
    setOnboarded();

    navigation.dispatch(
      CommonActions.reset({
        index: 0,
        routes: [{ name: "AppStack" }],
      })
    );
  }

  const slideRef = useRef(null);
  const scrollX = useRef(new Animated.Value(0)).current;
  const viewConfig = useRef({ viewAreaCoveragePercentThreshold: 50 }).current;

  return (
    <View style={styles.screen}>
      <SafeAreaView>
        <View style={styles.container}>
          <View style={styles.onboarding}>
            <FlatList
              data={slides}
              renderItem={({ item }) => <OnboardingItem item={item} />}
              horizontal
              showsHorizontalScrollIndicator={false}
              pagingEnabled
              onScroll={Animated.event(
                [{ nativeEvent: { contentOffset: { x: scrollX } } }],
                { useNativeDriver: false }
              )}
              viewabilityConfig={viewConfig}
              ref={slideRef}
            />
          </View>
          <View style={styles.footer}>
            <Paginator count={slides.length} scrollX={scrollX} />

            <View style={styles.buttons}>
              <PrimaryButton onPress={getStartedHandler}>
                Get Started
              </PrimaryButton>
              <TertiaryButton onPress={loginHandler}>Log in</TertiaryButton>
            </View>
          </View>
        </View>
      </SafeAreaView>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
  container: {
    flex: 1,
    paddingTop: 27,
    paddingBottom: 17,
    width: SIZES.width,
    alignItems: "center",
    justifyContent: "flex-start",
  },
  onboarding: {
    flex: 3,
  },
  footer: {
    flex: 1,
    width: "100%",
  },
  buttons: {
    marginTop: 67,
    width: "80%",
    marginHorizontal: "auto",
  },
});

 

Remember to utilize your onboarding screen. If you want to use mine, I will leave the link to the repository at the end of this post.

Finally, we put it all together in the App.tsx

 

import { StatusBar } from "expo-status-bar";

import Loading from "./components/Loading";

import useLoadFonts from "./hooks/useLoadFonts";
import useOnboarding from "./hooks/useOnboarding";
import MainNavigation from "./navigations/MainNavigation";

export default function App() {
  const { loaded, error } = useLoadFonts();
  const { loading, initialRouteName } = useOnboarding();

  /**
   * loading: checking onboarding state
   * initialRouteName: The stack to be routed to - AppStack or OnboardingStack. If null, App is still loading
   * loaded: fonts loaded state
   * error: error from trying to load the fonts
   */
  if (loading || initialRouteName === null || (!loaded && !error)) {
    return <Loading />;
  }

  return (
    <>
      <StatusBar style="auto" />
      <MainNavigation />
    </>
  );
}

 

Conclusion

This onboarding workflow effectively separates concerns and manages user states seamlessly, providing a smooth transition for both new and returning users. By incorporating custom fonts, managing the onboarding state with AsyncStorage, and using React Navigation to handle different screens, this approach ensures that your app’s onboarding process is both user-friendly and efficient.

REPOSITORY: https://github.com/Intuneteq/PIGN/tree/onboarding

373 views

Please Login to create a Question