How to add push notifications in React

Push notifications enable real-time user engagement, important alerts, and re-engagement capabilities even when users aren’t actively browsing your application. As the creator of CoreUI, a widely used open-source UI library, I’ve implemented push notification systems in enterprise React applications throughout my 12 years of frontend development since 2014. The most reliable approach is combining the Push API with service workers for background message handling and managing notification permissions properly. This method provides cross-browser push notification support, handles permission states, and enables rich notification experiences with actions and data payloads.

Request notification permission, subscribe to push service, register service worker message handler, and send notifications from backend.

// src/hooks/usePushNotifications.js
import { useState, useEffect } from 'react'

const usePushNotifications = () => {
  const [permission, setPermission] = useState(Notification.permission)
  const [subscription, setSubscription] = useState(null)
  const [isSupported, setIsSupported] = useState(false)

  useEffect(() => {
    setIsSupported('serviceWorker' in navigator && 'PushManager' in window)
  }, [])

  const requestPermission = async () => {
    if (!isSupported) {
      console.error('Push notifications not supported')
      return false
    }

    try {
      const result = await Notification.requestPermission()
      setPermission(result)
      return result === 'granted'
    } catch (error) {
      console.error('Error requesting notification permission:', error)
      return false
    }
  }

  const subscribeToPush = async () => {
    if (permission !== 'granted') {
      const granted = await requestPermission()
      if (!granted) return null
    }

    try {
      const registration = await navigator.serviceWorker.ready

      const existingSubscription = await registration.pushManager.getSubscription()
      if (existingSubscription) {
        setSubscription(existingSubscription)
        return existingSubscription
      }

      const vapidPublicKey = process.env.REACT_APP_VAPID_PUBLIC_KEY
      const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey)

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

      await sendSubscriptionToServer(newSubscription)
      setSubscription(newSubscription)
      return newSubscription
    } catch (error) {
      console.error('Failed to subscribe to push notifications:', error)
      return null
    }
  }

  const unsubscribeFromPush = async () => {
    if (!subscription) return

    try {
      await subscription.unsubscribe()
      await removeSubscriptionFromServer(subscription)
      setSubscription(null)
    } catch (error) {
      console.error('Failed to unsubscribe from push notifications:', error)
    }
  }

  return {
    permission,
    subscription,
    isSupported,
    requestPermission,
    subscribeToPush,
    unsubscribeFromPush
  }
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4)
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/')

  const rawData = window.atob(base64)
  const outputArray = new Uint8Array(rawData.length)

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i)
  }
  return outputArray
}

async function sendSubscriptionToServer(subscription) {
  const response = await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(subscription)
  })

  if (!response.ok) {
    throw new Error('Failed to send subscription to server')
  }
}

async function removeSubscriptionFromServer(subscription) {
  await fetch('/api/push/unsubscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(subscription)
  })
}

export default usePushNotifications
// src/components/NotificationManager.js
import { useState } from 'react'
import usePushNotifications from '../hooks/usePushNotifications'

const NotificationManager = () => {
  const {
    permission,
    subscription,
    isSupported,
    subscribeToPush,
    unsubscribeFromPush
  } = usePushNotifications()

  const [loading, setLoading] = useState(false)

  const handleSubscribe = async () => {
    setLoading(true)
    await subscribeToPush()
    setLoading(false)
  }

  const handleUnsubscribe = async () => {
    setLoading(true)
    await unsubscribeFromPush()
    setLoading(false)
  }

  if (!isSupported) {
    return (
      <div className='notification-manager'>
        <p>Push notifications are not supported in this browser.</p>
      </div>
    )
  }

  return (
    <div className='notification-manager'>
      <h3>Push Notifications</h3>
      <p>Permission status: {permission}</p>
      <p>Subscription status: {subscription ? 'Subscribed' : 'Not subscribed'}</p>

      {!subscription ? (
        <button
          onClick={handleSubscribe}
          disabled={loading}
        >
          {loading ? 'Subscribing...' : 'Enable Notifications'}
        </button>
      ) : (
        <button
          onClick={handleUnsubscribe}
          disabled={loading}
        >
          {loading ? 'Unsubscribing...' : 'Disable Notifications'}
        </button>
      )}
    </div>
  )
}

export default NotificationManager
// public/service-worker.js
self.addEventListener('push', (event) => {
  const data = event.data ? event.data.json() : {}

  const options = {
    body: data.body || 'New notification',
    icon: data.icon || '/logo192.png',
    badge: data.badge || '/badge.png',
    data: data.data || {},
    actions: data.actions || [
      { action: 'open', title: 'Open' },
      { action: 'close', title: 'Close' }
    ],
    tag: data.tag || 'default-notification',
    requireInteraction: data.requireInteraction || false,
    vibrate: data.vibrate || [200, 100, 200]
  }

  event.waitUntil(
    self.registration.showNotification(data.title || 'Notification', options)
  )
})

self.addEventListener('notificationclick', (event) => {
  event.notification.close()

  if (event.action === 'open' || !event.action) {
    event.waitUntil(
      clients.openWindow(event.notification.data.url || '/')
    )
  }
})

self.addEventListener('notificationclose', (event) => {
  console.log('Notification closed:', event.notification.tag)
})

Here the usePushNotifications hook encapsulates notification permission logic and subscription management. The Notification.requestPermission prompts user for permission with granted, denied, or default states. The pushManager.subscribe creates push subscription with VAPID public key for server authentication. The urlBase64ToUint8Array converts VAPID key from base64 to Uint8Array format required by Push API. The subscription object contains endpoint and keys needed by backend to send push messages. The service worker push event listener receives push messages and displays notifications. The notification actions provide interactive buttons users can click. The notificationclick event handles user interaction with notifications.

Best Practice Note:

This is the push notification architecture we use in CoreUI React admin panels for real-time user engagement and critical alerts. Always respect user notification preferences by providing easy opt-in and opt-out mechanisms, implement notification grouping and proper tagging to prevent notification spam, and test notification delivery across different browsers and devices as Push API implementation varies between platforms.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team