Creating a simple toast module for your React Native app using context, hooks and TypeScript
I recently needed to add a toast module to a React Native app. There are plenty of packages available that can be used. However, since I needed a very basic toast that I could fully customized, I thought it could be interesting to implement it without external libraries.
This tutorial is for developers that want to create a simple toast module and need some inspiration or something to start from. There are many ways to do it, so feel free to comment if you think about a better approach.
You can play with the demo here.
Demo repo is available here.
Specifications
Let’s start from a React Native app built using TypeScript and React Navigation. We can easily set this up with the Expo CLI.
% expo init
Then select the managed workflow: tabs (TypeScript).
This is the start point of the demo.
Now we are going to create the simple toast module. It will display three kinds of message: info, error and success. The message will be displayed at the bottom of the screen just above the tab bar. It must not disturb user navigation.
React context with hooks will be used to control the toast from anywhere in the app. Finally, we will use Animated to add a simple animation. Toast will fade in and then fade out after some time.
Let’s go!
We are going to create 2 components and one hook:
- ToastProvider.tsx — this component will define the context in which toast can be controlled.
- useToast.ts — this hook will be accessible throughout the app to control the toast.
- Toast.tsx — this is the component that contains the message.
ToastProvider.tsx
The toast is controlled by the toastConfig
state variable. When set to null
, the toast is hidden.
After creating the context, we must wrap our app with the ToastProvider component. It can be done above navigation, in your App.tsx file:
<ToastProvider>
<Navigation colorScheme={colorScheme} />
<StatusBar />
</ToastProvider>
useToast.ts
This hook is a simple wrapper of the useContext hook:
import * as React from "react";
import { ToastContext } from "../components/ToastProvider";export function useToast() {
return React.useContext(ToastContext)!;
}
Note that the exclamation mark is telling TypeScript that ToastContext
is not null
here. Now that this hook is ready, we can call showToast
from anywhere:
import * as React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import { useToast } from '../hooks/useToast';
import { ToastType } from './ToastProvider';export const SomeComponent: React.FC = () => {
const { showToast } = useToast(); return (
<TouchableOpacity
onPress={() => showToast(ToastType.Error, 'Error toast')}>
<Text>Show error</Text>
</TouchableOpacity>
);
}
At this point this will not work because we did not create the toast component :)
Toast.tsx
Let’s start by creating first version of the toast:
We set a timer in the useEffect hook to hide the toast after duration (4 seconds by default).
After creating this component, we must put it somewhere within the ToastProvider. Good location could be next to the navigation, in your App.tsx file:
<ToastProvider>
<Navigation colorScheme={colorScheme} />
<StatusBar />
<Toast />
</ToastProvider>
Almost ready
We set up everything needed to display the first version of the toast, let’s try to press Show error
defined above in SomeComponent:
Toast is displayed at the bottom of the screen and disappears after 4 seconds. We now need to adjust its position, which depends on device safe area.
Setting position
We will use the useSafeAreaInsets hook to compute bottom position of the toast:
import { useSafeAreaInsets } from "react-native-safe-area-context";export const Toast: React.FC = () => {
const insets = useSafeAreaInsets(); // ... return (
<View style={[styles.container, { bottom: insets.bottom }]}>
// ...
</View>
);
}const styles = StyleSheet.create({
container: {
// bottom: 0, This can be removed now
}
});
Now bottom position is adjusted depending on devices and orientation (portrait, landscape). But we still need to put the toast above tab bar, so that user navigation is not disturbed. So, how do we get the tab bar height?
Unfortunately, I did not find an automatic way to get this height with React Navigation. But when you look at the code, there is a default height of 49
. Thus it seems reasonable to set an height like 60
, so that our toast be just above the tab bar without touching it:
import { useSafeAreaInsets } from "react-native-safe-area-context";const tabBarHeight = 60;export const Toast: React.FC = () => {
const insets = useSafeAreaInsets(); // ... return (
<View style={[
styles.container,
{ bottom: insets.bottom + tabBarHeight }
]}>
// ...
</View>
);
}
The toast is now well positioned, and user can change tab while it is displayed.
Setting animation
Finally, it would be nicer to display and hide the toast smoothly. Let’s add a little fade animation to our toast when it is displayed or hidden:
import { Animated } from "react-native";const fadeDuration = 300;export const Toast: React.FC = () => {
const opacity = React.useRef(new Animated.Value(0)).current; const fadeIn = React.useCallback(() => {
Animated.timing(opacity, {
toValue: 1,
duration: fadeDuration,
useNativeDriver: true,
}).start();
}, [opacity]); const fadeOut = React.useCallback(() => {
Animated.timing(opacity, {
toValue: 0,
duration: fadeDuration,
useNativeDriver: true,
}).start(() => {
hideToast();
});
}, [opacity, hideToast]); React.useEffect(() => {
if (!toastConfig) {
return;
} fadeIn(); const timer = setTimeout(fadeOut, toastConfig.duration); return () => clearTimeout(timer);
}, [toastConfig, fadeIn, fadeOut]); // ... return (
<Animated.View style={[
styles.container,
{ bottom: insets.bottom + tabBarHeight, opacity }
]}>
// ...
</Animated.View>
);
}
We first initialize the toast opacity to 0
with the useRef hook as suggested in Animated documentation. We then create two functions fadeIn
and fadeOut
wrapped in useCallback hooks because they both are dependencies to the useEffect hook. This effect is triggered each time toastConfig
is set.
Note that hideToast
is called inside start
callback once fade out animation is done.