Back

Mastering Notifications with Apollo GraphQL and React Native Expo

A Step-by-Step Guide to Engaging Users with Real-Time Updates

Hero image
Hero image
Hero image
Hero image

In this blog post, we will explore how to send notifications from an Apollo GraphQL Server to a React native Expo application. We will implement registration of a push token, sending the notification from the server with and without delays, and retrieving a list of notifications that can fill out a user's notification feed.

All code featured in this article can be found here: https://github.com/maxshugar/expo-notifications-graphql-integration-example

Step 1: Obtaining Android and iOS Credentials

Before we begin, we must obtain credentials for Android and iOS. Please refer to the expo documentation for the most up-to-date instructions on obtaining credentials for Android and iOS. Let me know if you require assistance with this step in the comments below.

Step 2: Creating an Expo Typescript Project

npx create-expo-app your-app-name -t expo-template-blank-typescript

Step 3: Installing libraries

npx expo install expo-notifications expo-device expo-constants

  • expo-notifications . The library is used to request a user's permission and to fetch the ExpoPushToken. It is not supported on an Android Emulator or an iOS Simulator.

  • expo-device is used to check whether the app is running on a physical device.

  • expo-constants is used to get the projectId value from the app config.

Step 4: Registering for Push Notifications

COPYCOPYasync function registerForPushNotificationsAsync(): Promise<string | undefined> { if (Platform.OS === 'android') { Notifications.setNotificationChannelAsync('default', { name: 'default', importance: Notifications.AndroidImportance.MAX, vibrationPattern: [0, 250, 250, 250], lightColor: '#FF231F7C', }); } let token; if (Device.isDevice) { const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } if (finalStatus !== 'granted') { alert('Failed to get push token for push notification!'); return undefined; } token = (await Notifications.getExpoPushTokenAsync({ projectId: Constants.expoConfig?.extra?.eas.projectId })).data; } else { alert('Must use physical device for Push Notifications'); } return token; }

Before we can send push notifications, we must request permission from the device. The registerForPushNotificationsAsync method sets the notification channel to default on Android before checking whether we are running on a real device, not an emulator. If running on a real device, we check whether notification permissions have been granted. If not, we request permission from the user. Based on the result of finalStatus, we set the permissionGranted state, so that our components know whether the user has granted permission. If the user denies permission, we may want to lock sections of our app off until the user enables notifications.

Step 5: Notification Provider

The notification provider component below creates a React context for notifications with an initial undefined value. This context will allow any component wrapped in the provider to access the Expo push token, a flag for unread notifications, and a setter function for updating this flag.

COPYCOPYimport Constants from 'expo-constants'; import * as Device from 'expo-device'; import * as Notifications from 'expo-notifications'; import React, { ReactNode, createContext, useContext, useEffect, useRef, useState } from 'react'; import { Platform } from 'react-native'; interface NotificationContextType { expoPushToken: string | undefined; hasUnreadNotifications: boolean; permissionGranted: boolean; setHasUnreadNotifications: React.Dispatch<React.SetStateAction<boolean>>; } const NotificationContext = createContext<NotificationContextType | undefined>(undefined); interface NotificationProviderProps { children: ReactNode; } export const NotificationProvider: React.FC<NotificationProviderProps> = ({ children }) => { const [expoPushToken, setExpoPushToken] = useState<string | undefined>(undefined); const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false); const [permissionGranted, setPermissionGranted] = useState(false); const notificationListener: any = useRef(); const responseListener: any = useRef(); useEffect(() => { registerForPushNotificationsAsync().then(token => setExpoPushToken(token)); notificationListener.current = Notifications.addNotificationReceivedListener(notification => { setHasUnreadNotifications(true); }); responseListener.current = Notifications.addNotificationResponseReceivedListener(response => { setHasUnreadNotifications(true); }); return () => { Notifications.removeNotificationSubscription(notificationListener.current); Notifications.removeNotificationSubscription(responseListener.current); }; }, []); // async function registerForPushNotificationsAsync()... return ( <NotificationContext.Provider value={{ expoPushToken, hasUnreadNotifications, permissionGranted, setHasUnreadNotifications, }}> {children} </NotificationContext.Provider> ); }; export const useNotifications = (): NotificationContextType => { const context = useContext(NotificationContext); if (context === undefined) { throw new Error('useNotifications must be used within a NotificationProvider'); } return context; };

When the component mounts, registerForPushNotificationsAsync is called to get the Expo push token and stores it using setExpoPushToken, before registering two listeners:

  • notificationListener listens for incoming notifications and sets hasUnreadNotifications to true when a notification is received.

  • responseListener listens for interactions (e.g., the user taps on the notification) and also sets hasUnreadNotifications to true in response.

The hasUnreadNotifications flag will be used to trigger API updates. When the component unmounts, both listeners are unregistered to prevent memory leaks. Our useNotifications hook can be used in all components that require access to the notification context such as a component making an API call that depends on the Expo Push Token.

Step 6: Defining our GraphQL Server

The code snippet below outlines the process of setting up a simple GraphQL server using Apollo Server, integrated with Expo's server SDK for managing push notifications. This server is designed to handle basic operations related to notifications, such as sending notifications, registering push tokens, and marking notifications as read.

COPYCOPYimport { ApolloServer, gql } from "apollo-server"; import Expo, { ExpoPushMessage } from "expo-server-sdk"; let expo = new Expo(); interface Notification { id: string; message: string; read: boolean; } let pushToken: string; // This holds our registered push token let notifications: Notification[] = [ { id: "1", message: "Hello World", read: false }, ]; const typeDefs = gql` type Notification { id: ID! message: String! read: Boolean! } type Query { notificationsFeed: [Notification]! } type Mutation { sendNotification(message: String!, delay: Int): Boolean registerPushToken(token: String!): Boolean markNotificationAsRead(id: ID!): Boolean } `; function delaySeconds(seconds: number): Promise<void> { return new Promise((resolve) => { setTimeout(resolve, seconds * 1000); }); } const resolvers = { Query: { notificationsFeed: () => { console.log("Returning notifications..."); return notifications; }, }, Mutation: { sendNotification: async ( _: any, { message, delay }: { message: string; delay: number } ): Promise<Boolean> => { if (!pushToken) { console.error("No push token registered"); return false; } console.log("Sending notification..."); const newNotification: Notification = { id: String(notifications.length + 1), message, read: false, }; notifications.push(newNotification); if (delay) { await delaySeconds(delay); } if (pushToken && Expo.isExpoPushToken(pushToken)) { const message: ExpoPushMessage = { to: pushToken, // The recipient push token sound: "default", body: newNotification.message, data: { withSome: "data" }, }; try { await expo.sendPushNotificationsAsync([message]); console.log("Notification sent successfully"); } catch (error) { console.error("Error sending notification:", error); return false; } } return true; }, registerPushToken: (_: any, { token }: { token: string }): boolean => { pushToken = token; return true; }, markNotificationAsRead: (_: any, { id }: { id: string }): Notification => { const notification = notifications.find( (notification) => notification.id === id ); if (!notification) { throw new Error("Notification not found"); } notification.read = true; return notification; }, }, }; const server = new ApolloServer({ typeDefs, resolvers }); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`); });

The GraphQL schema (typeDefs) defines the data types and operations available in our API. It includes a Notification type that mirrors our internal interface, a Query type for fetching notifications (i.e., notificationsFeed), and Mutation types for actions like sending notifications (sendNotification), registering push tokens (registerPushToken), and marking notifications as read (markNotificationAsRead). This schema acts as a contract between the server and the client, outlining how clients can interact with the API. Resolvers are functions that handle the logic for fetching or modifying the data corresponding to each type of operation defined in the schema. In this server:

  • The notificationsFeed query resolver returns the current array of notifications.

  • The sendNotification mutation constructs a new notification and uses the Expo SDK to send a push notification to a registered device, optionally after a delay.

  • The registerPushToken mutation saves the client's push token to the server, which is crucial for sending push notifications to the correct device.

  • The markNotificationAsRead mutation updates a notification's read status, indicating that the user has seen it.

While this server setup is simple, it demonstrates the core functionalities required for a notification system. In real-world applications, you would likely need to expand on this foundation to handle more complex scenarios, such as managing a large number of users and tokens, scaling the notification system, and ensuring security and privacy compliance. Additionally, integrating user authentication and managing user sessions would be necessary to securely register devices and send personalized notifications.

Step 7: Consuming our Apollo Server

This step demonstrates the practical application of the Apollo Client in a mobile setting, emphasizing the real-world utility of GraphQL in facilitating communication between the server and client side of an application.

COPYCOPYimport { useMutation, useQuery } from "@apollo/client"; import React, { useEffect, useState } from "react"; import { ActivityIndicator, Button, ScrollView, StyleSheet, Text, View } from "react-native"; import { useNotifications } from "../context/notification"; import { REGISTER_PUSH_TOKEN_MUTATION, SEND_NOTIFICATION_MUTATION } from "../gql/mutation"; import { NOTIFICATIONS_FEED_QUERY } from "../gql/query"; type Notification = { id: string; message: string; read: boolean; }; type NotificationsFeedData = { notificationsFeed: Notification[]; }; export const FeedScreen: React.FC = () => { const { expoPushToken, hasUnreadNotifications, setHasUnreadNotifications } = useNotifications(); const [feedback, setFeedback] = useState<string>(""); const [loadingAction, setLoadingAction] = useState<boolean>(false); const { loading, error, data, refetch } = useQuery<NotificationsFeedData>(NOTIFICATIONS_FEED_QUERY); const [registerPushToken] = useMutation(REGISTER_PUSH_TOKEN_MUTATION, { onCompleted: (data) => { if (data.registerPushToken === false) { setFeedback("Error registering push token: No push token registered"); } else { setFeedback("Push token registered successfully!"); } setLoadingAction(false); }, onError: (error) => { setFeedback(`Error registering push token: ${error.message}`); setLoadingAction(false); }, }); const [sendNotification] = useMutation(SEND_NOTIFICATION_MUTATION, { onCompleted: (data) => { if (data.sendNotification === false) { setFeedback("Error sending notification: No push token registered"); } else { setFeedback("Notification sent successfully!"); } setLoadingAction(false); }, onError: (error) => { setFeedback(`Error sending notification: ${error.message}`); setLoadingAction(false); }, }); useEffect(() => { if (hasUnreadNotifications) { console.log('Calling the API to mark notifications as read...'); setHasUnreadNotifications(false); refetch(); } }, [hasUnreadNotifications, refetch, setHasUnreadNotifications]); return ( <ScrollView contentContainerStyle={styles.container}> <Text style={styles.tokenText}>Your expo push token: {expoPushToken}</Text> <Text style={styles.status}>Has unread notifications: {hasUnreadNotifications ? 'Yes' : 'No'}</Text> {loadingAction ? <ActivityIndicator size="small" color="#0000ff" /> : null} <Text style={styles.feedback}>{feedback}</Text> <Button title="Register Push Token" onPress={() => { if (expoPushToken) { setLoadingAction(true); registerPushToken({ variables: { token: expoPushToken } }); } }} /> <Button title="Send Notification" onPress={() => { setLoadingAction(true); sendNotification({ variables: { message: "Test notification from the server" } }); }} /> <Button title="Send Notification with 5 second dely" onPress={() => { setLoadingAction(true); sendNotification({ variables: { message: "Test notification from the server", delay: 5 } }); }} /> {loading ? <ActivityIndicator size="large" color="#0000ff" /> : null} {error ? <Text style={styles.error}>Error loading notifications</Text> : null} {data?.notificationsFeed.map(({ id, message, read }) => ( <View key={id} style={styles.notification}> <Text>{`${message} - Read: ${read ? 'Yes' : 'No'}`}</Text> </View> ))} </ScrollView> ); };

The FeedScreen component showcases the integration of GraphQL operations within a React Native application using Apollo Client useMutation and useQuery hooks. This setup allows for a seamless interaction with the GraphQL server, enabling functionalities such as registering push tokens, sending notifications, and fetching a list of notifications. Functionalities Covered:

  • Register Push Token: This functionality allows the device to register its Expo push token with the server. It's crucial for the server to know where to send push notifications. The mutation REGISTER_PUSH_TOKEN_MUTATION is called with the push token obtained from the Expo notifications setup.

  • Send Notification: This feature demonstrates sending a notification via the server, which can be done immediately or with a specified delay. It utilizes the SEND_NOTIFICATION_MUTATION mutation, showcasing the dynamic nature of GraphQL where arguments can be passed to modify the behavior of the server-side operation.

  • Fetch Notifications: The FeedScreen component fetches and displays a list of notifications using the NOTIFICATIONS_FEED_QUERY. This query provides real-time feedback to the user by displaying notifications, enhancing the interactive experience of the application.

The UI components used in this step, such as ScrollView, ActivityIndicator, Button, and Text, are standard React Native components that provide a simple yet effective user interface. The application employs conditional rendering to display loading indicators and feedback messages, ensuring the user is always informed about the state of the application.

  • ActivityIndicator: Used to indicate loading states for both fetching notifications and awaiting responses from mutations.

  • Buttons: Allow the user to interact with the application, such as sending notifications and registering the push token.

  • Feedback Messages: Display the result of operations, such as the success or failure of sending notifications or registering the push token.

Conclusion

In this blog post, we've navigated through the process of integrating Apollo GraphQL Server with a React Native Expo application to manage and send notifications. This journey started with obtaining necessary credentials for Android and iOS, setting up a new Expo TypeScript project, and installing essential libraries, to the in-depth implementation of push notifications registration, sending notifications with optional delays, and displaying a feed of notifications.

Through each step, we've seen how to leverage the capabilities of Expo alongside Apollo's GraphQL Server to create a seamless communication channel between the server and a mobile application. By registering for push notifications, we ensure that our application can receive notifications. The Notification Provider wraps our application in a context that makes notification-related data accessible throughout the app. Defining our GraphQL server then allows us to handle key operations around notifications, including registering push tokens, sending notifications, and marking them as read.

Step 7, Consuming our Apollo Server, brings everything together by showing how to use Apollo Client within a React Native app to interact with our GraphQL server. This final step demonstrated practical implementation, including registering push tokens, triggering notifications from the server, and fetching notifications to keep the user informed. This integration exemplifies the power of GraphQL for efficient data fetching and mutations, providing a robust solution for real-time communication in mobile applications.

As we conclude, it's evident that the combination of Apollo GraphQL Server and React Native Expo provides a powerful toolkit for developing feature-rich, real-time applications. The ability to manage notifications in this manner opens up a myriad of possibilities for engaging users and enhancing the user experience. Whether for simple alerts, chat applications, or complex interactive platforms, the principles and code snippets provided here serve as a foundation that can be expanded and customized for a wide range of applications.

We hope this guide has been informative and practical, providing you with the knowledge and tools needed to implement your own notification systems using Apollo GraphQL and React Native Expo. The integration of these technologies not only streamlines the development process but also ensures that applications remain scalable, maintainable, and responsive to user needs.

For developers looking to dive deeper, we encourage exploring further customization, implementing security best practices, and considering the user experience at every step. As technology evolves, so do the opportunities to create more engaging and interactive applications, and staying ahead requires leveraging the best tools and practices available.

Thank you for following along with this tutorial. We look forward to seeing the innovative applications you'll build with Apollo GraphQL and React Native Expo. If you have any questions, comments, or wish to share your projects, feel free to leave a comment below. Happy coding!

Copyright 2024 @ QZee

English

Subscribe to our newsletter!

Enter your email to receive monthly newsletters with updates from QZee team.

Subscribe to our newsletter!

Enter your email to receive monthly newsletters with updates from QZee team.

Copyright 2024 @ QZee

English

Copyright 2024 @ QZee

Subscribe to our newsletter!

Enter your email to receive monthly newsletters with updates from QZee team.

English