Back
Mastering Notifications with Apollo GraphQL and React Native Expo
A Step-by-Step Guide to Engaging Users with Real-Time Updates
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 theExpoPushToken
. 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 theprojectId
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 setshasUnreadNotifications
to true when a notification is received.responseListener
listens for interactions (e.g., the user taps on the notification) and also setshasUnreadNotifications
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'sread
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 theNOTIFICATIONS_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!
Check out our blog posts
From development, product, UX and sales, stay in the loop!
Sales
Fueling Barbering Expansion
How an Entrepreneurial Barber Expanded His Business with Flexible Booking Software
CEO & Co-Founder
•
Cameron Calder
Thursday, October 10, 2024
Development
Creating Dynamic Email Templates: A Comprehensive Guide
Here's how we streamlined our email creation process, from design to backend integration, to deliver a seamless experience for our clients.
CEO & Co-Founder
•
Cameron Calder
Sunday, August 18, 2024
Development
Unleashing Frontend Development with Apollo Server Schema Mocks
Accelerate your development cycle and enhance productivity without waiting on the backend
CTO & Co-Founder
•
Max Shugar
Saturday, June 8, 2024
View all