This post was originally written by Tooba Jamal on the Ably Blog
In today’s digital landscape, where websites deal with realtime data flows, keeping users informed of updates by sending alerts is critical to the user experience. One way of achieving this is in-app notifications. In-app notifications are messages or alerts that appear within a website while it's being accessed in a web browser.
Consider a web application that interacts with a server in response to user requests. A traditional server response to a client request typically involves rendering a web page. However, this isn't always the case. Sometimes, the server's response carries crucial information—a groundbreaking healthcare research update on a medical app, a notification that your favorite restaurant is preparing your food order, an alert regarding scheduled maintenance on your bank's website, or even an unexpected error message. In these cases, in-app notifications provide direct realtime communication between the client and a server - enhancing the user experience by providing timely information and guidance.
In this article, we are going to build a React application that sends users a work-break time reminder. Users can specify their work and break time durations, and when their work session is completed, the application will automatically send a notification, suggesting activities for their scheduled break time.
We will start by setting up a basic React project in Vite, use Ably to enable realtime communication between the client and a server, use react-toastify to display notifications in the browser, and send system notifications using the Notifications API.
Follow along as this post takes you through a step-by-step guide to build this application, or if you’d rather jump straight to the finished code, check out this Github repo.
Getting started with React in-app notifications
Prerequisites
Before we begin, it's essential to ensure that we have Tailwind CSS and Concurrently installed. Tailwind CSS utility classes will be used for styling our project and will not affect the functionality. Concurrently will allow us to run our React frontend and server file simultaneously on our machines. For now, knowing the purpose that Concurrently serves is enough. We will see how to make it work later in the article.
To use Ably for sending and receiving realtime notifications, we will need an Ably account. Signup for one here, create a project by following the instructions and obtain your API key. You can find your API key under the "API keys'' tab of your project. If you have any doubts, you can follow the instructions here.
Since we do not want to expose our API keys to the public, we will also use the dotenv package to store API keys in the “.env” file.
Understanding the app architecture
We are building a React app that keeps users informed with the help of in-app notifications. The application has a form with two number input fields, allowing users to specify their expected work time duration and break time duration. Upon submitting the form, the server performs a search in our database for an “Activity” that the user can practice in their specified break time duration. This “Activity” represents a suggested healthy task the user can perform during their break, such as “walk” or “stretch”. The backend searches for suitable activities based on the user’s entered break time duration. The fetched activity is then sent to the client in realtime using Ably Rest and it is displayed on the browser as a notification.
For instance, if a user enters 60 minutes for work time duration and 10 minutes for break, the backend will search for an “Activity” that can be completed within 10 minutes. If the database suggests “walk”, the backend sends this recommendation to the client in realtime using Ably. The notification is then displayed on the user screen after the work time duration (60 minutes) has passed.
Another form allows users to input their name and a message that they might want to share with others using the application. The message is then displayed as a notification on all clients browsers in realtime using Ably.
Setting up a basic React project using Vite
We are using Vite which is a recommended bundler for React applications.
Begin by running the following command in your terminal which will create a React app boilerplate in a directory named after the project name you provide in the command. For the sake of this tutorial, we are naming our project “ably-react-notifications”.
npm create vite@latest ably-react-notifications
This will prompt you to answer a few questions related to the framework and variant. We will select React and JavaScript respectively.
Once the project is created, navigate to the project directory by running cd ably-react-notifications
and install the project dependencies with the npm install
command and finally run the app with npm run dev
as suggested by Vite in your terminal.
Setting up concurrently to run client and server simultaneously
Once your Vite project is set up, create a 'server.js' file in your project's root directory. We will add server side code in “server.js” later in the article. For now, we just want a “server.js” file to avoid errors when we run the client and server together using the "Concurrently" package.
After creating the “server.js” file, replace your dev script in the package.json file with the following to ensure that “App.jsx” and “server.js” run simultaneously.
"dev": "concurrently \"vite\" \"node server.js\"",
At this point, your project directory structure should look like the following.
└── ably-react-notifications/
├── public/
│ └── vite.svg
├── src/
│ ├── assets
│ ├── App.css
│ ├── App.jsx
│ ├── index.css
│ └── main.jsx
├── index.html
├── package-lock.json
├── package.json
├── server.js
└── vite.config.js
If you've set up Tailwind CSS for styling, you would typically have Tailwind configuration files like “tailwind.config.js” and “postcss.config.js” as well.
Using Ably for receiving notification
To start using Ably for sending realtime notifications, we begin by installing “ably-js”, which is a JavaScript client library for Ably realtime. Run the following command in your project directory to install it:
npm install ably –-save
Once the package is installed, we need to authenticate our client for secure realtime communication using our project API key. If you are unfamiliar about getting an API key through the Ably dashboard this guide covers all the necessary information.
Setting up auth token API endpoint
Ably provides two authentication mechanisms to ensure secure subscription and publishing of messages. Basic authentication is recommended on the server side. However, client side applications are recommended to integrate token authentication to reduce the risk of exposing your Ably API key.
We add token authentication to our app using the Ably REST SDK. In the backend, we create a new Ably REST client with the help of our API key and add a /auth endpoint with a random client ID and publish and subscribe capabilities. We pass these as an object inside the createTokenRequest function provided by the Ably Rest SDK which then creates a new token for our client request.
We are building an Express server, therefore we will need to install it along with other dependencies first. Run the following command in the terminal window:
npm install express cors dotenv body-parser
- Express is a Node.js application framework
- Cors enables the client server communication from different URLs
- Dotenv is required to read the secret API key from .env file
- Body-parser will help reading HTTP request body
We will be using our API key in “server.js” file for authentication. To keep your API key secret, create a “.env” file in your root directory and save your API key inside it as shown below:
API_KEY = [YOUR_ABLY_API_KEY]
Anything stored inside a “.env” file can be accessed with the help of process.env.SECRET_NAME
command. In our case, we access the “API_KEY” as follows:
process.env.API_KEY
Add the following code inside the “server.js” file we created in the project root directory to add token authentication.
const express = require("express");
const bodyParser = require("body-parser");
const fs = require("fs");
const Ably = require("ably");
const cors = require("cors");
const app = express();
require('dotenv').config();
app.use(bodyParser.json());
app.use(cors());
const client = new Ably.Rest(process.env.API_KEY);
app.get("/auth", (req, res) => {
const tokenParams = {
clientId: `anonymous-${Math.random().toString(36).substring(7)}`,
capability: { '*': ['publish', 'subscribe'] } };
client.auth.createTokenRequest(tokenParams, (err, token) => {
if (err) {
res.status(500).json({ error: "Failed to generate token request" });
} else {
res.setHeader("Content-Type", "application/json");
res.send(JSON.stringify(token));
}
});
});
app.listen(3001, () => console.log("Server started on port 3001"));
In the above code snippet, we first import the installed dependencies in our file. Then, we create a client using Ably REST by providing it our API key. Finally, we create an “/auth” API endpoint that generates a token.
Since we are running the server with our frontend app, we are bound to encounter the “ReferenceError: require is not defined” error, which is common when using “require” in a browser environment. To avoid this error, add "type": "commonjs" to your package.json file.
"type": "commonjs",
"scripts": {
// Your scripts here
}
Navigate to http://localhost:3001/auth in your browser and ensure you see a JSON object with a keyName, random client id, timestamp, capability, nonce, and mac key. The JSON object should look like the following:
Publishing a notification from server
We will set up our client to send user inputs to the backend, allowing it to search the database. To keep things simple, we will add dummy data in a JSON file which will serve as our backend for this tutorial. Create an activities.json file in the project root directory with the following data:
{
"activities": [
{
"name": "Stretching",
"duration": 5
},
{
"name": "Meditation",
"duration": 10
},
{
"name": "Walk",
"duration": 15
},
{
"name": "Deep Breathing",
"duration": 7
},
{
"name": "Snack Break",
"duration": 20
}
]
}
In the server.js file, read and parse the JSON content and then, using a new server route, locate and send the matched data to client:
const activitiesData = JSON.parse(fs.readFileSync("activities.json"));
app.post("/sendbreaktime", (req, res) => {
const { duration } = req.body;
const matchedActivity = activitiesData.activities.find(
(activity) => activity.duration === Number(duration),
);
if (matchedActivity) {
const channel = client.channels.get("activities");
channel.publish("matched activity", matchedActivity);
res.json({
success: true,
activity: matchedActivity,
});
} else {
res.status(400).json({
success: false,
message: "No activity found for the specified duration",
});
}
});
The route function uses the "find" array method to find the matched activity based on the break time duration provided by the user. This matched activity will be recommended as a notification to the user.
A conditional statement checks whether a suitable activity is found. If a match is found, the server subscribes to a channel using the client instance created at the top of our "server.js" file. Subsequently, it publishes a notification with the name "matched activity," and the body of this notification contains the details of the activity found in the data file.
It's important to note that when subscribing to a channel, you use the channel name within the “useChannel” hook on the client side and the “get” function on the server side. However, when we publish the notification, we provide the message name before the message body which in our case is “matched activity”.
At this point, your project directory structure should look like the following.
└── ably-react-notifications/
├── public/
│ └── vite.svg
├── src/
│ ├── assets
│ ├── App.css
│ ├── App.jsx
│ ├── index.css
│ └── main.jsx
├── .env
├── activities.json
├── index.html
├── package-lock.json
├── package.json
├── server.js
└── vite.config.js
Setting up the Ably provider
On the client side, the component connects our application to Ably. We wrap our component in the Ably provider to make Ably available throughout our application.
Import the component and “Realtime” from Ably to connect to Ably and configure the client to the “/auth” API endpoint.
The client variable configures our frontend to the “/auth” API endpoint which is then passed as a client prop to the AblyProvider component.
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import { Realtime } from 'ably';
import { AblyProvider } from 'ably/react';
const client = new Realtime({authUrl: "http://127.0.0.1:3001/auth"})
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AblyProvider client={client}>
<App />
</AblyProvider>
</React.StrictMode>,
)
Take a moment and run npm run dev
again to check the browser to ensure we have no errors at this point.
Displaying notifications on the browser
Now that we have our server all set up, our next step is to display 'activities' sent by the server to our users in the form of notifications. To achieve this, we will first define JSX for the user interface and then use user data to “matchedActivity” from “activities.json”. Finally, we use Ably to subscribe to the “matchedActivity” server fetches from the database in realtime.
Collecting user data in App.jsx and sending it to server.js
In our application, we need to collect user input for work and break time durations. We achieve this by replacing the default App.jsx code with the following:
import { useState } from "react";
export const App = () => {
const [breakTimeDuration, setBreakTimeDuration] = useState(0);
const [workTimeDuration, setWorkTimeDuration] = useState(0);
return (
<div className="max-w-screen-xl mx-auto text-center">
<h1 className="mt-4 max-w-lg text-3xl font-semibold leading-loose text-gray-900 mx-auto">
Schedule your work and breaks!
</h1>
<div className="w-full px-2 m-auto md:w-2/5">
<form className="my-10">
<div className="mb-6">
<label className="block mb-2 text-sm font-medium text-gray-900">
Enter work time duration (in minutes):
</label>
<input
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
type="number"
value={workTimeDuration}
onChange={(e) => setWorkTimeDuration(e.target.value)}
/>
</div>
<div className="mb-6">
<label className="block mb-2 text-sm font-medium text-gray-900">
Enter break time duration (in minutes):
</label>
<input
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
type="number"
value={breakTimeDuration}
onChange={(e) => setBreakTimeDuration(e.target.value)}
/>
</div>
<button
className="focus:outline-none text-white bg-purple-700 hover:bg-purple-800 focus:ring-4 focus:ring-purple-300 font-medium rounded-lg text-sm px-5 py-2.5"
type="submit"
>
Set Schedule
</button>
</form>
</div>
</div>
)
}
export default App;
In the above code, we use the "useState" hook to store the "workTimeDuration" and "breakTimeDuration" a user enters in the input fields. The JSX has an <h1>
element for the heading and a <form>
for the input fields. We bind our states to the respective input fields using the "value" prop. The "onChange" event sets the states equal to whatever the user enters in the input fields to keep track of it.
Next, we need to send the user inputs to our server. We can achieve this by defining a "handleSubmit" function and passing it as an "onSubmit" event handler in our form.
The "handleSubmit" function sends a POST request to the "/sendbreaktime" endpoint with the "breakTimeDuration" as the request body. This request is processed by our 'server.js' file within the '/sendbreaktime' API endpoint to trigger a notification. Add the following handleSubmit function in App.jsx file:
function handleSubmit(e) {
e.preventDefault();
fetch("http://127.0.0.1:3001/sendbreaktime", {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify({
duration: breakTimeDuration,
}),
})
.then((response) => {
response.json();
})
.catch((error) => {
console.error("Error sending break time data:", error);
});
}
Now, we can pass this to the form “onSubmit” event by just adding the following code in the form opening tag:
<form className="my-10" onSubmit={handleSubmit}>
After making these changes, refresh your application. Enter a random number in the work time input field, and enter '10' in the break time duration input field. Then, submit the form. You won't see a notification because we haven't handled its display yet, but the application won't break either. If you enter a value that doesn't exist in our 'activities.json' file, you are likely to encounter a '400 - Bad Request' error in your console because the server fails to find a matching activity.
Using useChannel to subscribe to the notification in App.jsx
Now that the client is ready to send necessary data to the server and server is all set up to send the matched activity to the client, we need to display it on the client side.
Ably provides a "useChannel" hook that allows you to subscribe to a channel and receive messages from it in realtime. This hook takes a channel name and a callback function, which defines how you want to handle the messages received on that channel. Importantly, the same channel instance created using the "useChannel" hook can also be used to publish messages over the channel.
This hook simplifies the process of subscribing to and interacting with channels, making it easier to implement realtime communication and display notifications within your web application.
First, we import the “useChannel” hook in our App.jsx file.
import { useChannel } from "ably/react";
Next, we create a new state called “activity” to store the activity we receive through Ably REST.
const [activity, setActivity] = useState("");
Finally, we utilize the "useChannel" hook to subscribe to the "activities" channel, which will receive the matched activity from the server. This hook allows us to set the "activity" state with the received data:
const { channel } = useChannel("activities", (matchedActivity) => {
setActivity(matchedActivity);
});
Our client now effectively listens for any notifications sent by the sever in the “activities” channel. However, since we have not yet implemented logic to display the notifications in the browser we won’t be able to see them at this point. You can use console logs to see it in action.
Adding a goal sharing feature in App.jsx
To implement the feature that allows clients to share their goals with others in our React application, we need to introduce a new <form>
element in our "App.jsx" file. We will need useState hook to store user inputs just like we did in work-time duration form.
Begin by adding two new states named “message” and “userName” using the “useState” hook:
const [message, setMessage] = useState("");
const [userName, setUserName] = useState("");
Next add the new form in your “App.jsx” file below the work-break duration form.
<form className="my-10">
<p className="tracking-tighter text-gray-500 md:text-lg mb-2">
Wanna share your goals with others?
</p>
<div className="mb-6">
<label className="block mb-2 text-sm font-medium text-gray-900">
Name
</label>
<input className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" type="text" value={userName} onChange={(e) => setUserName(e.target.value)} />
</div>
<div className="mb-6">
<label className="block mb-2 text-sm font-medium text-gray-900">
Message
</label>
<input className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" type="text" value={message} onChange={(e) => setMessage(e.target.value)} />
</div>
<button className="focus:outline-none text-white bg-purple-700 hover:bg-purple-800 focus:ring-4 focus:ring-purple-300 font-medium rounded-lg text-sm px-5 py-2.5" type="submit">
Submit
</button>
</form>
Next, we create another “useChannel” instance named “messagesChannel” to subscribe to the message sent by the client and store it in the “message” state. This instance will enable realtime communication between clients by subscribing to the “messages'' channel and storing it in the “message” state.
When the user submits the form using the "handleMessageSubmit" function, the message data, including the user's name and goal message, is published to the "messagesChannel."
const { channel: messagesChannel } = useChannel("messages", (message) => {
setMessage(message)
});
Now we define a “handleMessgeSubmit” function which uses the “messagesChannel” instance of the “useChannel” hook to publish the “userName” and “message” over the “messages” channel.
function handleMessageSubmit(e) {
e.preventDefault();
messagesChannel.publish("message", {
name: userName,
message: message,
});
setMessage("");
setUserName("");
};
Now, we can pass this to the form “onSubmit” event by just adding the following code in the form opening tag:
<form className="my-10" onSubmit={handleMessageSubmit}>
The frontend of the application would look like the attached screenshot below. Styling may vary depending on whether or not you have used Tailwind CSS.
Using react-toastify to implement the notifications component
At this point, we have stored notifications sent by the server and client in their respective states. However, we have not yet displayed them as notifications to the browser. To achieve this, we can use a toast component library. In this tutorial, we are using “react-toastify” to display the notifications.
To get started, we install "react-toastify" in our project.
npm install react-toastify
Next, we import the necessary components and styles for "react-toastify" into App.jsx:
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
Don’t forget to include the CSS for proper styling.
The handleActivityNotification function uses the setTimeout function to display toast success notification after the work time duration is over.
Then we use the useEffect hook to run the “handleActivityNotification” function whenever the “activity” and “workTimeDuration” states change. To import the “useEffect” hook from React, change the code where you import useState hook at the top of your App.jsx to the following:
import { useState, useEffect } from "react";
Toast takes in a message that we want to render as notification and an object to configure the notification settings. The “position” property in the object specifies the position in a browser where we want to display the notification and “autoClose” specifies the time period after which the notification disappears.
To customize toast messages color, we can use toast emitter to specify the type of notification. We can choose from success, error, warning, and info according to our preferences.
function handleActivityNotification() {
setTimeout(
() => {
toast.success(
`After all the ${workTimeDuration} minutes of hard work, your body deserves a ${activity.data.duration} minutes ${activity.data.name}`,
{
position: "top-right",
autoClose: 3000,
},
);
},
workTimeDuration * 60 * 1000,
);
}
useEffect(() => {
if (activity && workTimeDuration > 0) {
handleActivityNotification();
}
}, [activity, workTimeDuration]);
We do not need to wait to display client notifications like we did for the server notifications. Therefore, we directly call the “toast” function inside the “messagesChannel” instance of the “useChannel” hook.
<CustomToast />
component allows us to customize the toast messages, we used it to style “userName” as bold in the notification.
A toast message without any emitter displays the custom styled notification.
const CustomToast = ({ sender, message }) => (
<div>
<strong>{sender}:</strong> {message}
</div>
);
const { channel: messagesChannel} = useChannel("messages", (message) => {
toast(
<CustomToast sender={message.data.name} message={message.data.message} />,
{
position: "top-right",
autoClose: 3000,
},
);
});
Finally, we add the <ToastContainer />
component at the end of our JSX, inside the outermost <div/>
tag so the toast messages can be displayed on the browser.
<ToastContainer />
At this point, test your app to ensure everything works as expected. Enter a work time duration and break time duration in the first form. Make sure you enter the break time duration that's specified in our "activities.json" file.
Once you submit the form, you should see a notification after the work time duration has passed, similar to the screenshot attached below.
Test the goal sharing form by entering a name and a message. You should see the message as a notification with the name in your browser.
You might also test it from different browsers or devices to see how multiple clients can share the goals at the same time. The way it looks will be similar to the screenshot below.
Using the Notifications API to trigger notifications
The Notifications API is a browser API that allows web applications to display system-level notifications to users on their devices. Some users don’t prefer notifications to be displayed on their screens. However, our application uses notifications for the exchange of information among client and server.
To let the user know that our app will display notifications to their screens, we will use the Notifications API. The Notifications API can send notifications even when the web application is not in the foreground or actively being used. However, it requires that the web browser is open and the application is loaded.
To use the Notification API. begin by importing the “bellImage” in “App.jsx” which we will use as a notification icon. The image used can be found in the project GitHub repository here. You can use any image of your choice if you want.
import bellImage from './assets/bell.png';
Next, define a function named “showSystemNotification” that allows us to display system level notifications.
function showSystemNotification() {
if (“Notification” in window) {
if (Notification.permission === "granted") {
let notification = new Notification("Need your attention", {
body: "This site uses notifications for the best user experience. Thank you for understanding",
icon: bellImage,
});
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
let notification = new Notification("Need your attention", {
body: "This site uses notifications for the best user experience. Thank you for understanding",
icon: bellImage,
});
} else if (permission === "denied") {
alert("This site uses notifications for the best user experience. Thank you for understanding");
}
});
} else {
alert("This site uses notifications for the best user experience. Thank you for understanding");
}
} else {
alert("This site uses notifications for the best user experience. Thank you for understanding");
}
}
It first checks if the Notification object is available in the window. This is used to determine if the browser supports the Notifications API.
If the Notification object is available, it checks the permission status using Notification.permission.
If permission is “granted”, it creates a new notification using the new Notification constructor. The notification includes a title (“Need your attention”), body text (“This site uses notifications for the best user experience. Thank you for understanding”), and an icon.
If permission is not “denied” but also not “granted”, it requests permission from the user using Notification.requestPermission(). It handles the permission response in a then block. If permission is granted, it creates a notification; if denied, it displays an alert to inform the user about the notifications in our application.
If permission is “denied” from the beginning, or if the browser doesn't support the Notifications API, it displays the same alert message explaining the importance of notifications for the user experience.
Finally, add a useEffect hook to trigger the showSystemNotification function when the component mounts. This means that the notification logic will run as soon as the component is loaded.
useEffect(() => showSystemNotification(), []);
Rerun your app and check if it displays a notification at the bottom right of your screen or an alert message based on your browser notification permission.
Using the Notifications API to display Ably notifications
Toast notifications can only be seen when our application is active in a browser. However, we want our users to stay informed even if they have minimized our application or switched to a different tab while working. The Notifications API displays notifications even when an application is minimized because it sends notifications to the operating system, which displays them on the user's screen.
To achieve this, in our “handleActivityNotification” function, we add a conditional statement to check if notification permissions have been granted. If permission is granted, our application sends activity as a system notification to the user. In cases where permission hasn't been granted, we fall back to using “react-toastify” to display the notification within our application.
function handleActivityNotification() {
setTimeout(
() => {
if(“Notification” in window && Notification.permission === "granted") {
let notification = new Notification(`${activity.data.name}`, {
body: `After all the ${workTimeDuration} minutes of hardwork, your body deserves a ${activity.data.duration} minutes ${activity.data.name}`,
icon: bellImage
})
} else {
toast.success(
`After all the ${workTimeDuration} minutes of hardwork, your body deserves a ${activity.data.duration} minutes ${activity.data.name}`,
{
position: "top-right",
autoClose: 3000,
},
);
}
},
workTimeDuration * 60 * 1000,
);
}
Conclusion
This guide has explored the process of implementing in-app notifications in a React application. We’ve covered all the aspects of this implementation from setting up an Ably Provider and authentication to displaying realtime notifications in our application using react-toastify.
Furthermore, we explored how Notifications API can be used to send system level notifications to our users. We also demonstrated its integration in our application, making it more informative and engaging and also integrated it in our application.
The entire source code of the application we built in this guide is available in a GitHub repository. We would love to hear about what you build and how you use React notifications! Simply tweet @ablyrealtime or drop us a line in /r/ablyrealtime.
Top comments (3)
I really like this. It would be a valuable addition to Webcrumbs. Perhaps we'll see an Ably Plugin for Webcrumbs being built by the community. One-click Ably integration with Webcrumbs. Sounds nice, hu? I'll save it in our idea box.
So useful. Thanks for sharing those tips. tankless water heater installation colorado springs
💛🌴