July 29, 2024
Streamlining Onboarding in React Native
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