How to Build a React Native Toast Message in Expo Without Third-Party Libraries
You can build a fully functional react native toast message system in Expo using only React's built-in hooks and React Native's Animated API by creating a context provider that manages visibility state and opacity animations.
Implementing a react native toast message in an Expo project does not require external dependencies like react-native-root-toast or react-native-toast-message. By leveraging core React primitives found in the Facebook React repository and React Native's built-in animation capabilities, you can create a lightweight, customizable toast system that works consistently across iOS and Android.
Architectural Foundation: Core React APIs
The toast implementation relies on four fundamental React mechanisms defined in packages/react/src/ReactHooks.js and packages/react/src/ReactContext.js:
useState– Stores the toast message text and visibility boolean. The implementation inpackages/react/src/ReactHooks.jsprovides the stateful primitive that triggers re-renders when the toast content changes.useEffect– Manages the auto-hide timer lifecycle. As implemented inpackages/react/src/ReactHooks.js, this hook runs after render and can clean up timers to prevent memory leaks.useRef– Holds theAnimated.Valueand timeout ID across renders without causing re-renders. TheuseRefimplementation inpackages/react/src/ReactHooks.jsreturns a mutable object reference that persists through the component lifecycle.createContext/useContext– Enables the toast API to be accessible from any nested component. Defined inpackages/react/src/ReactContext.js, this mechanism allows theToastProviderto broadcast theshowToastfunction deep into the component tree.
Building the Toast System
Step 1: Create the Toast Context
First, define the context type and provider using the primitives from packages/react/src/ReactContext.js. This establishes the contract for the showToast function that child components will consume.
Step 2: Implement the Provider with Animation
The ToastProvider component manages three pieces of state: the message string, the animated opacity value, and the hide timeout reference. By leveraging useRef from packages/react/src/ReactHooks.js for the animation value, the component avoids unnecessary re-renders during the fade-in/fade-out transitions.
The provider uses useEffect to clean up the timer when the component unmounts. According to the implementation details in packages/react/src/ReactFiberHooks.js, effects run after the paint phase, making them ideal for starting the auto-hide timeout. The cleanup function ensures that if the provider unmounts, pending timers are cleared to prevent memory leaks.
Step 3: Create the useToast Hook
Expose a custom hook that wraps useContext to provide type-safe access to the toast functionality. This abstraction ensures that components can trigger toast messages without importing the context directly.
Complete Implementation Code
The following implementation requires no external dependencies beyond Expo and React Native core.
// src/ToastContext.tsx
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
ReactNode,
} from 'react';
import {Animated, StyleSheet, Text, View, Dimensions} from 'react-native';
// ---------------------------------------------------------------------------
// 1️⃣ Context definition
// ---------------------------------------------------------------------------
type ToastContextType = {
showToast: (msg: string, durationMs?: number) => void;
};
const ToastContext = createContext<ToastContextType | undefined>(undefined);
// ---------------------------------------------------------------------------
// 2️⃣ Provider component – core logic lives here
// ---------------------------------------------------------------------------
export const ToastProvider = ({children}: {children: ReactNode}) => {
const [message, setMessage] = useState<string | null>(null);
const opacity = useRef(new Animated.Value(0)).current;
const hideTimeout = useRef<NodeJS.Timeout | null>(null);
const hideToast = () => {
Animated.timing(opacity, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}).start(() => setMessage(null));
};
const showToast = (msg: string, durationMs = 2000) => {
// Clear any pending hide
if (hideTimeout.current) {
clearTimeout(hideTimeout.current);
}
setMessage(msg);
Animated.timing(opacity, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}).start();
hideTimeout.current = setTimeout(hideToast, durationMs);
};
// Clean up timer on unmount
useEffect(() => {
return () => {
if (hideTimeout.current) {
clearTimeout(hideTimeout.current);
}
};
}, []);
return (
<ToastContext.Provider value={{showToast}}>
{children}
{/* --------------------------------------------------------------------
3️⃣ Render the toast overlay – positioned absolute, centered horizontally
-------------------------------------------------------------------- */}
{message && (
<Animated.View style={[styles.toast, {opacity}]}>
<Text style={styles.text}>{message}</Text>
</Animated.View>
)}
</ToastContext.Provider>
);
};
// ---------------------------------------------------------------------------
// 4️⃣ Hook for consumer components
// ---------------------------------------------------------------------------
export const useToast = (): ToastContextType => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
// ---------------------------------------------------------------------------
// 5️⃣ Styles – keep it minimal
// ---------------------------------------------------------------------------
const {width} = Dimensions.get('window');
const styles = StyleSheet.create({
toast: {
position: 'absolute',
bottom: 80,
left: width * 0.1,
right: width * 0.1,
backgroundColor: '#333',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 8,
alignItems: 'center',
},
text: {
color: '#fff',
fontSize: 14,
},
});
// App.tsx – wrap the app with the provider
import React from 'react';
import {ToastProvider} from './src/ToastContext';
import {HomeScreen} from './src/HomeScreen';
export default function App() {
return (
<ToastProvider>
<HomeScreen />
</ToastProvider>
);
}
// src/HomeScreen.tsx – how a component triggers a toast
import React from 'react';
import {Button, View, StyleSheet} from 'react-native';
import {useToast} from './ToastContext';
export const HomeScreen = () => {
const {showToast} = useToast();
return (
<View style={styles.container}>
<Button
title="Press me"
onPress={() => showToast('Hello from Expo toast!')}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {flex: 1, justifyContent: 'center', alignItems: 'center'},
});
How the Animation and State Management Work
The toast system leverages the exact hook implementations found in the Facebook React repository. When showToast is called, the component updates state via useState, triggering a re-render. The useRef hook—implemented in packages/react/src/ReactHooks.js—maintains the Animated.Value reference across these renders without causing additional cycles.
The useEffect hook manages the timer lifecycle. According to the implementation details in packages/react/src/ReactFiberHooks.js, effects run after the paint phase, making them ideal for starting the auto-hide timeout. The cleanup function ensures that if the provider unmounts, pending timers are cleared to prevent memory leaks.
React Native's Animated API handles the visual transition. By setting useNativeDriver: true, the animation runs on the native thread, ensuring 60fps performance even while JavaScript executes the state logic.
Summary
- Zero dependencies: The implementation requires only React Native core and Expo, avoiding the bundle bloat of third-party toast libraries.
- Hook-based architecture: Leverages
useState,useEffect, anduseReffrompackages/react/src/ReactHooks.jsto manage toast state and timers. - Context API: Uses
createContextanduseContextfrompackages/react/src/ReactContext.jsto expose theshowToastfunction throughout the component tree. - Native animations: React Native's
AnimatedAPI with native driver enabled ensures smooth fade-in/fade-out transitions. - Memory safe: Proper cleanup in
useEffectprevents timer leaks when components unmount, following the lifecycle patterns defined inpackages/react/src/ReactFiberHooks.js.
Frequently Asked Questions
How do I position the toast at the top instead of the bottom?
Change the position: 'absolute' style properties in the styles.toast object. Replace bottom: 80 with top: 60 (or use SafeAreaView insets) to position the toast at the top of the screen. The Animated.View will render at the new coordinates while maintaining the same fade animation logic.
Can I queue multiple toast messages simultaneously?
The current implementation replaces the existing message immediately when showToast is called. To support queuing, modify the provider to store an array of messages in useState rather than a single string. Push new messages to the array and shift them off after their duration expires, rendering the first item in the queue until it is dismissed.
Is this custom toast approach performant for production applications?
Yes. Because the solution uses React Native's Animated API with useNativeDriver: true, the opacity transitions execute on the native thread rather than the JavaScript thread. The state management relies on React's optimized hook implementation from packages/react/src/ReactHooks.js, ensuring minimal re-render overhead. For high-frequency updates, consider memoizing the context value with useMemo to prevent unnecessary renders in consuming components.
Can I use this implementation with React Native CLI instead of Expo?
Absolutely. The code relies solely on core React Native APIs (Animated, StyleSheet, Dimensions) and React primitives that are identical in both Expo and React Native CLI projects. Simply copy the ToastContext.tsx file into a React Native CLI project and wrap your root component with ToastProvider exactly as shown in the App.tsx example. No Expo-specific APIs are required.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →