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 in packages/react/src/ReactHooks.js provides the stateful primitive that triggers re-renders when the toast content changes.
  • useEffect – Manages the auto-hide timer lifecycle. As implemented in packages/react/src/ReactHooks.js, this hook runs after render and can clean up timers to prevent memory leaks.
  • useRef – Holds the Animated.Value and timeout ID across renders without causing re-renders. The useRef implementation in packages/react/src/ReactHooks.js returns a mutable object reference that persists through the component lifecycle.
  • createContext/useContext – Enables the toast API to be accessible from any nested component. Defined in packages/react/src/ReactContext.js, this mechanism allows the ToastProvider to broadcast the showToast function 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, and useRef from packages/react/src/ReactHooks.js to manage toast state and timers.
  • Context API: Uses createContext and useContext from packages/react/src/ReactContext.js to expose the showToast function throughout the component tree.
  • Native animations: React Native's Animated API with native driver enabled ensures smooth fade-in/fade-out transitions.
  • Memory safe: Proper cleanup in useEffect prevents timer leaks when components unmount, following the lifecycle patterns defined in packages/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →