Learnitweb

Push Notification in React Web App

1. What Are Push Notifications in Web Apps?

Push notifications in web applications allow your site to send notifications to users, even when the browser is not open, as long as the service worker is active in the background. These are commonly used for:

  • Reminders and alerts
  • News or content updates
  • Real-time chat or app messages

Push notifications rely on two main technologies:

  • Service Worker: A script that runs in the background to handle push messages and display notifications.
  • Push API: A browser API to subscribe the client to notifications.

They also require a secure context (https://) or localhost during development.

2. Overview of the Architecture

Here’s a high-level view of how everything works together:

+--------------------+          +------------------+           +--------------------+
|     React App      |  --->    |  Node.js Server  |   --->    |   Push Service     |
| (Frontend UI)      |          | (Stores subs)    |           | (e.g. Chrome, FCM) |
+--------------------+          +------------------+           +--------------------+
        |                                  ↑
        |         receives notification    |
        +------------<------------<--------+

  • The React app asks the user for notification permission.
  • If accepted, it registers a Service Worker and subscribes to push notifications.
  • This subscription is sent to the Node.js server and stored in memory (or a database).
  • Later, the server can send a message to that subscription using the Web Push Protocol.
  • The browser’s Push Service delivers the notification to the user’s device.
  • The Service Worker displays the notification even if the site is closed.

3. Create the application

3.1 Create a Vite + React project

npm create vite@latest react-vite-push --template react
cd react-vite-push
npm install

3.2 Add sw.js in public/ directory

Create a file: public/sw.js

self.addEventListener('push', event => {
  const data = event.data?.json() || {};
  const title = data.title || 'Notification';
  const options = {
    body: data.body || 'You have a new message',
    icon: '/vite.svg'
  };

  event.waitUntil(
    self.registration.showNotification(title, options)
  );
});

3.3 Register the service worker in main.jsx

Edit src/main.jsx:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => {
        console.log('Service Worker registered', reg);
      })
      .catch(err => {
        console.error('Service Worker registration failed:', err);
      });
  });
}

3.4 Add push subscription logic

Create a file: src/utils/subscribe.js

export async function subscribeUser() {
  const registration = await navigator.serviceWorker.ready;
  const publicVapidKey = 'YOUR_PUBLIC_VAPID_KEY';

  const convertedVapidKey = urlBase64ToUint8Array(publicVapidKey);

  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: convertedVapidKey
  });

  await fetch('http://localhost:4000/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription),
    headers: {
      'Content-Type': 'application/json'
    }
  });

  console.log('Push subscription sent to server', subscription);
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');
  const rawData = atob(base64);
  return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}

3.5 Update App.jsx to request permission and subscribe

import { subscribeUser } from './utils/subscribe';

function App() {
  const handleClick = () => {
    if ('Notification' in window && 'serviceWorker' in navigator) {
      Notification.requestPermission().then(permission => {
        if (permission === 'granted') {
          subscribeUser();
        } else {
          alert('Permission denied');
        }
      });
    } else {
      alert('Push not supported');
    }
  };

  return (
    <div style={{ textAlign: 'center', marginTop: '2rem' }}>
      <h1>Vite + React Push Notification Demo</h1>
      <button onClick={handleClick}>Enable Notifications</button>
    </div>
  );
}

export default App;

Node.js Backend to Send Push

3.6 Setup basic server

mkdir push-server
cd push-server
npm init -y
npm install express web-push body-parser cors

3.7 Generate VAPID keys

npx web-push generate-vapid-keys

Copy both keys. You will use the public key in your frontend.

3.8 Create server.js

const express = require('express');
const webpush = require('web-push');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();
app.use(cors());
app.use(bodyParser.json());

let subscriptions = [];

const vapidKeys = {
  publicKey: 'YOUR_PUBLIC_VAPID_KEY',
  privateKey: 'YOUR_PRIVATE_VAPID_KEY'
};

webpush.setVapidDetails(
  'mailto:test@example.com',
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

app.post('/subscribe', (req, res) => {
  const sub = req.body;
  subscriptions.push(sub);
  res.status(201).json({ message: 'Subscription saved' });
});

app.post('/send', (req, res) => {
  const payload = JSON.stringify({
    title: 'Hello from server!',
    body: 'This is a test push message.'
  });

  Promise.all(subscriptions.map(sub => webpush.sendNotification(sub, payload)))
    .then(() => res.status(200).json({ message: 'Push sent' }))
    .catch(err => {
      console.error('Push error:', err);
      res.sendStatus(500);
    });
});

app.listen(4000, () => {
  console.log('Server running on http://localhost:4000');
});

4. Testing Your App

4.1 Start the backend server:

node server.js

4.2 Start the frontend Vite app:

npm run dev

Open your app in the browser. Click Enable Notifications.

In another terminal, trigger the push from backend:

curl -X POST http://localhost:4000/send

You should receive a browser notification, even if the React app tab is inactive.