-
-
Save MonaAghili/df3575cec1f6346d3d047d1d915bc617 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| I'm facing an issue with the toast message in my React Native app using the react-native-toast-message library. When the keyboard is minimized, the toast stays in the middle of the page, causing a poor user experience. | |
| Problem Details: | |
| I'm dismissing the keyboard when an error occurs using Keyboard.dismiss(). | |
| The toast is shown after the keyboard dismissal, but it does not adjust its position when the keyboard is minimized. | |
| I need the toast to appear at the bottom of the screen, but when the keyboard is active or dismissed, it should adapt its position accordingly. | |
| I’ve provided my app’s code, including API and error handling setup. Could you help me refactor the code to ensure the toast appears correctly at the bottom of the screen and behaves well with the keyboard? Additionally, if you have suggestions for improvements in my error handling or toast configuration, I'd appreciate it. | |
| // layout | |
| import { ErrorProvider } from "@/components/error/ErrorProvider"; | |
| import { toastConfig } from "@/utils/toastConfig"; | |
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | |
| import { useFonts } from "expo-font"; | |
| import { Stack } from "expo-router"; | |
| import { StatusBar } from "expo-status-bar"; | |
| import "react-native-reanimated"; | |
| import Toast from "react-native-toast-message"; | |
| const queryClient = new QueryClient({ | |
| defaultOptions: { | |
| queries: { | |
| retry: false, | |
| staleTime: 5 * 60 * 1000, | |
| refetchOnWindowFocus: false, | |
| }, | |
| }, | |
| }); | |
| export default function RootLayout() { | |
| const [loaded] = useFonts({ | |
| SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"), | |
| }); | |
| if (!loaded) { | |
| // Async font loading only occurs in development. | |
| return null; | |
| } | |
| return ( | |
| <QueryClientProvider client={queryClient}> | |
| <ErrorProvider> | |
| <Stack> | |
| <Stack.Screen name="index" options={{ title: "Giphy Explorer" }} /> | |
| <Stack.Screen | |
| name="details/[id]" | |
| options={{ title: "GIF Details" }} | |
| /> | |
| <Stack.Screen name="+not-found" /> | |
| </Stack> | |
| </ErrorProvider> | |
| <StatusBar style="auto" /> | |
| <Toast config={toastConfig} /> | |
| </QueryClientProvider> | |
| ); | |
| } | |
| // error provider | |
| import React, { createContext, ReactNode, useContext, useState } from 'react'; | |
| import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; | |
| import { ErrorContextType } from './types'; | |
| const ErrorContext = createContext<ErrorContextType | undefined>(undefined); | |
| export function ErrorProvider({ children }: { children: ReactNode }) { | |
| const [error, setError] = useState<Error | null>(null); | |
| const resetError = () => setError(null); | |
| // Value to be provided | |
| const contextValue: ErrorContextType = { | |
| error, | |
| setError, | |
| resetError, | |
| }; | |
| // Render fallback UI or children | |
| if (error) { | |
| return ( | |
| <View style={styles.container}> | |
| <Text style={styles.title}>Something went wrong</Text> | |
| <Text style={styles.message}>{error.message || 'an unexpected error occurred'}</Text> | |
| <TouchableOpacity style={styles.button} onPress={resetError}> | |
| <Text style={styles.buttonText}>Try Again</Text> | |
| </TouchableOpacity> | |
| </View> | |
| ); | |
| } | |
| return <ErrorContext.Provider value={contextValue}>{children}</ErrorContext.Provider>; | |
| } | |
| // Custom hook to use the error context | |
| export function useError() { | |
| const context = useContext(ErrorContext); | |
| if (context === undefined) { | |
| throw new Error('useError must be used within an ErrorProvider'); | |
| } | |
| return context; | |
| } | |
| // a higher order component for try catch pattern | |
| export function withErrorHandling<P extends object>(Component: React.ComponentType<P>) { | |
| return function WithErrorHandling(props: P) { | |
| const { setError } = useError(); | |
| try { | |
| return <Component {...props} />; | |
| } catch (error) { | |
| setError(error instanceof Error ? error : new Error('An unknown error occurred')); | |
| return null; | |
| } | |
| }; | |
| } | |
| const styles = StyleSheet.create({ | |
| container: { | |
| flex: 1, | |
| justifyContent: 'center', | |
| alignItems: 'center', | |
| padding: 20, | |
| backgroundColor: '#fff', | |
| }, | |
| title: { | |
| fontSize: 18, | |
| fontWeight: 'bold', | |
| marginBottom: 12, | |
| }, | |
| message: { | |
| fontSize: 14, | |
| color: '#666', | |
| textAlign: 'center', | |
| marginBottom: 20, | |
| }, | |
| button: { | |
| backgroundColor: '#2196F3', | |
| paddingVertical: 10, | |
| paddingHorizontal: 20, | |
| borderRadius: 4, | |
| }, | |
| buttonText: { | |
| color: 'white', | |
| fontWeight: 'bold', | |
| }, | |
| }); | |
| // toast config | |
| import { StyleSheet } from "react-native"; | |
| import { | |
| BaseToast, | |
| ErrorToast, | |
| InfoToast, | |
| ToastProps, | |
| } from "react-native-toast-message"; | |
| export const toastConfig = { | |
| success: (props: ToastProps) => ( | |
| <BaseToast | |
| {...props} | |
| style={styles.successToast} | |
| contentContainerStyle={styles.contentContainer} | |
| text1Style={styles.title} | |
| text2Style={styles.message} | |
| /> | |
| ), | |
| error: (props: ToastProps) => ( | |
| <ErrorToast | |
| {...props} | |
| style={styles.errorToast} | |
| contentContainerStyle={styles.contentContainer} | |
| text1Style={styles.title} | |
| text2Style={styles.message} | |
| /> | |
| ), | |
| info: (props: ToastProps) => ( | |
| <InfoToast | |
| {...props} | |
| style={styles.infoToast} | |
| contentContainerStyle={styles.contentContainer} | |
| text1Style={styles.title} | |
| text2Style={styles.message} | |
| /> | |
| ), | |
| }; | |
| const styles = StyleSheet.create({ | |
| successToast: { | |
| borderLeftColor: "#4CAF50", | |
| backgroundColor: "#EEFBEF", | |
| height: "auto", | |
| minHeight: 60, | |
| maxWidth: "95%", | |
| width: "95%", | |
| }, | |
| errorToast: { | |
| borderLeftColor: "#F44336", | |
| backgroundColor: "#FEECEB", | |
| height: "auto", | |
| minHeight: 60, | |
| maxWidth: "95%", | |
| width: "95%", | |
| }, | |
| infoToast: { | |
| borderLeftColor: "#2196F3", | |
| backgroundColor: "#E7F3FE", | |
| height: "auto", | |
| minHeight: 60, | |
| maxWidth: "95%", | |
| width: "95%", | |
| }, | |
| contentContainer: { | |
| paddingHorizontal: 15, | |
| paddingVertical: 10, | |
| }, | |
| title: { | |
| fontSize: 16, | |
| fontWeight: "600", | |
| color: "#333", | |
| }, | |
| message: { | |
| fontSize: 14, | |
| color: "#555", | |
| marginTop: 2, | |
| }, | |
| }); | |
| // | |
| error utils | |
| /** | |
| * Handles global errors in the application and displays appropriate toast notifications. | |
| */ | |
| import { Keyboard, Platform } from 'react-native'; | |
| import Toast from 'react-native-toast-message'; | |
| export const handleGlobalError = (error: any) => { | |
| let toastType = 'error'; | |
| let title = 'Error'; | |
| let message = error?.customMessage || 'Something went wrong'; | |
| // Network errors (no response) | |
| if (!error.response && error.request) { | |
| title = 'Network Error'; | |
| message = 'Unable to connect. Check your internet connection.'; | |
| } | |
| // Server returned a response with an error status | |
| else if (error.response?.status) { | |
| switch (error.response.status) { | |
| case 400: | |
| title = 'Invalid Request'; | |
| message = 'The request contains invalid parameters.'; | |
| break; | |
| case 401: | |
| case 403: | |
| title = 'Authentication Error'; | |
| message = 'API key may be invalid or expired.'; | |
| break; | |
| case 404: | |
| toastType = 'info'; | |
| title = 'No Results'; | |
| message = 'No GIFs found matching your search.'; | |
| break; | |
| case 429: | |
| title = 'Too Many Requests'; | |
| message = 'Please try again in a moment.'; | |
| break; | |
| case 500: | |
| case 502: | |
| case 503: | |
| case 504: | |
| title = 'Server Error'; | |
| message = 'Giphy service is currently unavailable.'; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| if (error.response?.data?.meta?.msg) { | |
| message = error.response.data.meta.msg; | |
| } | |
| // Check if keyboard is visible | |
| const isKeyboardVisible = Keyboard.isVisible; | |
| // If keyboard is visible, dismiss it first and then show toast with a slight delay | |
| if (isKeyboardVisible()) { | |
| Keyboard.dismiss(); | |
| // Wait for keyboard to fully dismiss before showing toast | |
| setTimeout(() => { | |
| showToast(toastType, title, message); | |
| }, Platform.OS === 'ios' ? 300 : 150); | |
| } else { | |
| // Show toast immediately if keyboard is not visible | |
| showToast(toastType, title, message); | |
| } | |
| return error; | |
| }; | |
| // Helper function to show toast | |
| const showToast = (type: string, title: string, message: string) => { | |
| Toast.show({ | |
| type: type, | |
| text1: title, | |
| text2: message, | |
| position: 'bottom', | |
| visibilityTime: 3000, | |
| bottomOffset: 20, | |
| }); | |
| }; | |
| // http sender | |
| import { handleGlobalError } from '@/utils/errorUtils'; | |
| import axios from 'axios'; | |
| import { API_CONFIG } from './APIs'; | |
| export const giphyAxios = axios.create({ | |
| baseURL: API_CONFIG.URL, | |
| params: { | |
| api_key: API_CONFIG.KEY, | |
| }, | |
| }); | |
| giphyAxios.interceptors.response.use( | |
| response => response, | |
| error => { | |
| handleGlobalError(error); | |
| return Promise.reject(error); | |
| }, | |
| ); | |
| //apis | |
| /** | |
| * this objects containing Giphy API endpoint routes. | |
| * It made readonly in order to prevent accidental modifications of API routes and | |
| * ensuring consistent API endpoint usage throughout the entire application. | |
| */ | |
| type ReadOnly<T> = { | |
| readonly [P in keyof T]: T[P]; | |
| }; | |
| const apiConfigData = { | |
| KEY: process.env.EXPO_PUBLIC_API_KEY, | |
| URL: process.env.EXPO_PUBLIC_API_URL, | |
| }; | |
| const giphyRoutesData = { | |
| RANDOM: '/gifs/random', | |
| SEARCH: '/gifs/search', | |
| GIF_BY_ID: (id: string) => `/gifs/${id}`, | |
| }; | |
| export const API_CONFIG: ReadOnly<typeof apiConfigData> = apiConfigData; | |
| export const GIPHY_ROUTES: ReadOnly<typeof giphyRoutesData> = giphyRoutesData; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment