Almost everyone is familiar with notifications these days. They can be divided into two broad groups: push notifications and in-app notifications.
Because push notifications are handled by the user's mobile device or browser, they must be implemented in specific ways. In-app notifications, however, are displayed in your app, so it is entirely up to you to decide how they will be implemented—from the look and feel to the technology behind them.
This customizability of in-app notifications has the benefit of allowing them to be deeply integrated with your application, which leads to a better user experience than generic push notifications. However, because in-app notifications differ on a case-by-case basis, it can be hard to implement them. There is no single "right way" to do it.
What most in-app notifications have in common, however, is that they provide a way for you to send content to users who are actively engaged with your application. This is the best window of opportunity for you to share news such as new features, sales, or promotions with them.
In this tutorial, you will learn how to build simple in-app notifications using Hygraph's powerful content system, webhooks, and WebSockets with a React.js frontend. The in-app notifications will show the user some text fields and a color to indicate the importance of the notification.
#Project overview
Before you jump in, review this sequence diagram to get an understanding of how the different components will interact:
Hygraph can be configured to send requests to a webhook endpoint when certain events happen. Because you will be running the backend locally, ngrok allows you to expose the backend through a tunnel to the internet, which Hygraph can use as the webhook endpoint. When the backend receives a request on the webhook endpoint, it will emit the data it received to all connected clients via WebSockets. Lastly, when the frontend receives a WebSocket message containing a notification, the frontend will render it.
The completed code for this tutorial can be found in this public GitHub repo.
#Prerequisites
To follow along with this tutorial, you will need a few things:
- Node.js
- A code editor. VS Code is a good choice if you don't have a preference.
- A Hygraph account
- An Ngrok account and the corresponding download for your OS of choice
- Optional: Google Chrome or a Chrome-based browser for running a particular browser extension for testing the WebSocket connection. If you don't have Chrome or don't want to install it, this can be skipped.
#Setting up Hygraph
The first piece of the puzzle is Hygraph. Hygraph is a content platform that offers a lot of flexibility. In this tutorial, you will use it to publish pieces of content and have them automatically sent via webhook to your app's backend.
If you haven't already done so, create an account. Once you are signed in, select the option to create a new blank project from the home screen. Give it a name like Notification demo and then click Add project:
Once the new project is created and you are taken to the project dashboard, from the left-hand menu, select Schema. In the new menu that appears, click on Add next to Enumerations. This will open a dialog that prompts you for some details. The enumeration you are creating will denote the "intent" of your notifications so that you can show notifications with different severities. Fill out the dialog with these details, as shown in the screenshot below:
- Display name: Use intent.
- API ID: Use Intent.
- Description: This is optional; you can leave it blank.
- Enumeration values: Use info, warning, error, and success, pressing Enter after each value.
Then click Add Enumeration:
Next, at the top of the Schema menu, click the Add button next to Models. This will open a new dialog that lets you create the model representing your notifications. Fill out the dialog with these details, as shown in the screenshot below:
- Display name: Use Notification.
- API ID: Use Notification.
- Plural API ID: Use Notifications.
- Description: This is optional; you can leave it blank.
Then click Add Model.
Hygraph will take you to a new page, where you will be prompted to add fields to your model. You can select fields from the right-hand menu. For this tutorial, add the following fields:
- Single line text: Use Title as the field name.
- Multi line text: Use Message as the field name.
- Dropdown: Use Intent as the field name. For this one, select your Intent enumeration as the value for the Enumeration field.
With these fields created, your model should look something like this:
Next, you must configure a webhook for this model to ensure everything is working as expected.
Select Webhooks at the bottom of the left-hand menu and then choose Add Webhook. This will open a form that lets you configure your webhook.
Because you have not created the backend yet, you can use a simple webhook testing service to verify that everything is working. In a new tab, go to https://webhook.site/, where you will be given a unique URL. Copy this URL, and use it as the value for Url
in the Hygraph webhook form.
Under Triggers, select your model (notification) as well as the stage (Published) and action (Publish) on which you wish it to trigger. Your completed form should look something like this:
Submit the form by clicking Add in the top-right corner.
Next, in the left-hand menu, select Content and then Add entry. Fill out the three fields—use My title for Title, My message for Message, and success for Intent, as shown below—then click Save & publish.
Once that is saved, go back to the webhook.site tab. You should see that it has received a message containing the values you specified:
This verifies that everything on Hygraph is configured correctly.
Once you have created the backend, you will change the webhook URL; for now, you can leave it as is.
#Creating the backend
The next thing you need to create is the backend service that will do two things. First, it will receive POST
request webhooks coming from Hygraph. Second, it will host a WebSocket server and emit messages to any connected clients containing the details of the notifications received via the webhook.
This is a simple, contrived form of what would likely be a more complicated application in a real-world scenario, but it serves to demonstrate the architectural patterns in play.
To get started, create a new directory to hold the code for this tutorial. Inside the directory, create another to hold the code for the server:
mkdir -p hygraph-notifications/servercd hygraph-notifications/server
Once in this directory, you need to install a few dependencies. First, initialize a new npm project (you can accept the default values for any questions it asks), and then install the dependencies as follows:
npm initnpm i -D typescript @types/ws @types/expressnpm i body-parser express ws
Next, create a file called tsconfig.json and give it the following content:
{"compilerOptions": {"esModuleInterop": true,"target": "es6","module": "commonjs","outDir": "./dist/server","strict": true,"sourceMap": true,"typeRoots": ["node_modules/@types"]},"exclude": ["dist","node_modules"]}
Finally, create a file called index.ts and give it the following content:
import express from "express";import http from "http";import WebSocket from "ws";import bodyParser from "body-parser";import { randomUUID } from "crypto";// Create a new express app instanceconst app = express();// Parse JSON bodiesconst jsonParser = bodyParser.json();// Create a new HTTP serverconst server = http.createServer(app);// Create a new WebSocket serverconst wss = new WebSocket.Server({ server });app.post("/webhook", jsonParser, (req, res) => {// Broadcast the notification to all connected clients// In a real app, you would probably want to send to specific clientswss.clients.forEach((client) => {if (client.readyState === WebSocket.OPEN) {client.send(JSON.stringify({id: randomUUID(), // Generate a random ID for the messagetype: "notification", // Give the message a type// Encode the data from the webhook into the messagedata: {title: req.body.data.title,message: req.body.data.message,intent: req.body.data.intent,},}));}});res.sendStatus(200);});// Handle new WebSocket connectionswss.on("connection", (ws: WebSocket) => {// Acknowledge connectionws.send(JSON.stringify({ id: randomUUID(), type: "connection", data: null }));});const port = process.env.PORT || 8999;// Start the serverserver.listen(port, () => {console.log(`Server started on port ${port}`);});
Comments in this code describe what each section does, but the gist is that it creates a web server to handle the incoming webhook and the WebSocket connections, as detailed above.
Because the code is written in TypeScript, you need to transpile it before it can be executed. To do this, run the following commands:
./node_modules/.bin/tscnode dist/server/index.js
You should see the output in your terminal saying Server started on port 8999
.
At this stage, testing everything is working so far is a good idea. The easiest way to do this before you've created the frontend is to use the Simple WebSocket Client Google Chrome extension.
Install this extension, and then click on the icon it adds near your omnibar. This should take you to a simple UI with a field labeled URL:. In this field, enter ws://localhost:8999
and click Open. You should see something like this:
This output indicates that the client has successfully connected to your WebSocket server.
#Setting up ngrok
Next, you will use ngrok to expose your server to the internet so that Hygraph can send webhook requests to it.
If you haven't already done so, create an account with ngrok, follow the instructions to download the ngrok binary for your OS of choice and authenticate it with your ngrok account.
Once this is done, running the following command will start an ngrok session on your machine:
ngrok http 8999
You should see some output like this:
Copy the URL from the Forwarding line (blurred above), and go back to Hygraph. Navigate back to the Webhooks menu item at the bottom of the left-hand menu, and edit your existing webhook. Replace the previous URL with the ngrok URL you copied, suffixed with /webhook
, as this is the route your server is configured to listen on. Save the changes to the webhook configuration by clicking Update in the top-right corner.
Next, navigate back to the Content menu. Click Add Entry and create another notification by filling out and submitting the form.
If you look in your ngrok terminal, you should see that it logged a POST
request to the /webhook endpoint. If so, look in your WebSocket client, where you should see the notification too:
If you don't see the message come through, check that the following are all true:
- The server is running locally.
- ngrok is running.
- The URL ngrok gave you is the URL configured in Hygraph and that you suffixed it with
/webhook
.
To renew the connection, click Close and then Open again in the WebSocket client.
If this works, you are ready to move on to the final piece, the frontend.
#Creating the frontend
You now have data coming from Hygraph via your webhook and being sent to your WebSocket client. If you implement handling for these WebSocket messages in a React application, you can show in-app notifications driven by Hygraph's content system. You will build this React application using TypeScript, Vite, and Tailwind.
To get started, navigate back to your project root directory, and create a new Vite project with the following command:
# From hygraph-notifications/npm create vite@latest frontend --template react-ts# If you are using the most recent versions of npm, use the following command instead:# npm create vite@latest frontend -- --template react-tscd frontendnpm install
Next, you need to install some other dependencies for Tailwind and to work with WebSockets:
npm i react-use-websocketnpm i -D tailwindcss postcss autoprefixernpx tailwindcss init -p
This command would have created a new file called tailwind.config.js. Open this file, and replace its content with the following to ensure that Tailwind watches the appropriate files:
/** @type {import('tailwindcss').Config} */export default {content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],theme: {extend: {},},plugins: [],};
Next, you must add Tailwind to your base CSS file and a few reset rules. Open src/index.css
, and replace its content with the following:
@tailwind base;@tailwind components;@tailwind utilities;html, body {padding: 0;margin: 0;}#root {height: 100vh;display: flex;justify-content: center;align-items: center;}
Finally, open src/App.tsx and replace its content with the following:
import { useCallback, useEffect, useState } from "react";import useWebSocket from "react-use-websocket";// Define some types for better autocompletetype Intent = "success" | "warning" | "error" | "info";type MessageType = "notification" | "connection";type MessageData = {notification: {title: string;message: string;intent: Intent;};connection: null;};type Message<T extends MessageType> = {id: string;type: T;data: MessageData[T];};// Define a map of intents to Tailwind CSS classes to color your notificationsconst intentMap: Record<Intent, string> = {success: "bg-green-500",warning: "bg-yellow-500",error: "bg-red-500",info: "bg-blue-500",};function App() {// Store the message history in stateconst [messageHistory, setMessageHistory] = useState<Message<MessageType>[]>([]);// Connect to the websocket serverconst [socketUrl] = useState("ws://localhost:8999");const { lastMessage } = useWebSocket(socketUrl);// When a new message is received, parse it and add it to the message historyuseEffect(() => {if (lastMessage !== null) {const parsedData = JSON.parse(lastMessage.data);setMessageHistory((prev) => prev.concat(parsedData));}}, [lastMessage, setMessageHistory]);// Define a function to dismiss a notificationconst handleDismiss = useCallback((message: Message<"notification">) => {setMessageHistory((prev) => prev.filter((m) => m.id !== message.id));},[setMessageHistory]);// Filter the message history to only include notificationsconst notifications = messageHistory.filter((data): data is Message<"notification"> => data.type === "notification");// Render the notificationsif (notifications.length > 0) {return (<div className="bg-gray-800 text-white flex justify-center items-center h-full w-full flex-col p-12"><h1 className="text-3xl font-bold font-sans mb-4">Notifications</h1><ul className="w-full">{notifications.map((message, idx) => (<likey={message.id}className={`w-full p-2 rounded-md my-2 flex items-start justify-start flex-col ${intentMap[message.data.intent]}`}><div className="font-bold flex justify-between w-full"><span>{message.data?.title}</span><button onClick={() => handleDismiss(message)}>x</button></div><div>{message.data?.message}</div></li>))}</ul></div>);}// If there are no notifications, render a waiting messagereturn (<div className="bg-gray-800 text-white flex justify-center items-center h-full w-full"><div>Waiting for notifications...</div></div>);}export default App;
The code above has comments describing what each piece does, but essentially, it connects to the WebSocket server running on port 8999 and waits until it receives a message with a type
of ”notification”
.
Once one of these notifications has been received, it renders the notification with simple styles. The notification's color is based on the intent
that you selected in Hygraph when creating the entry, and the rendered notification has a button for dismissing it.
Aside from this, the code includes some types to help with autocomplete. This would likely be spread across multiple files in a real application, but in this contrived example, it is all kept in one file for brevity.
From your terminal, run the following command to start your frontend app:
npm run dev
The output should indicate which port it is running on. Go to this address in your browser, where you should see something like this:
If you have previously stopped the backend service or ngrok, ensure that they are running and that the URL ngrok gives you is still configured for your webhooks in Hygraph. If all these pieces are running, publish a notification from Hygraph. You should see it appear as an in-app notification:
With that, you can push in-app notifications to your React frontend in real-time using Hygraph's powerful content tools.
#Wrapping up
In this tutorial, you've seen how to use Hygraph, webhooks, WebSockets, and React to implement basic real-time in-app notifications.
This example has kept things simple with just a few text fields and an enumeration. However, Hygraph supports many useful field types, including geo-coordinates, file uploads, and rich text inputs. With this flexibility and the tools and techniques you've seen in this tutorial, there's no limit to the real-time, content-driven experiences you could bring to your application.
If you need to bring content-driven experiences to your application, consider Hygraph, a GraphQL-friendly headless content management system that's easy to work with and simple to integrate with the backend and frontend technologies of your choice.