Skip to content
All articles
TutorialReactWeb push 10 min read

React Push Notification Tutorial with ReachBell

Add web push notifications to any React app — hooks, context provider, a permission UI component, and sending triggered pushes from your backend.

DotSpheres Engineering

Engineering, ReachBell ·

This guide adds web push to a generic React app — Create React App, Vite, or any setup that is not Next.js (which has its own walkthrough). We will write a React context, a custom hook, a permission UI component, and wire a backend trigger. Expect 30-45 minutes from npm install to first push.

Assumed: React 18+, TypeScript, a backend that can call a REST API.

1. Install

npm install @reachbell/web

2. Host the service worker

Service workers must live at the domain root. In a Vite project, drop the file in `public/reachbell-sw.js`. In CRA, same place — `public/` is copied to the build root as-is.

// public/reachbell-sw.js
importScripts("https://cdn.reachbell.com/sw/v1/reachbell-sw.js");

3. Build a ReachBell context

A single context owns the SDK lifecycle and exposes subscribe/unsubscribe to any descendant.

// src/push/PushContext.tsx
import React, { createContext, useContext, useEffect, useState } from "react";
import { ReachBell } from "@reachbell/web";

type PushState = {
  ready: boolean;
  subscribed: boolean;
  subscribe: () => Promise<void>;
  unsubscribe: () => Promise<void>;
};

const PushContext = createContext<PushState | null>(null);

export function PushProvider({ children }: { children: React.ReactNode }) {
  const [ready, setReady] = useState(false);
  const [subscribed, setSubscribed] = useState(false);

  useEffect(() => {
    ReachBell.init({
      projectId: import.meta.env.VITE_REACHBELL_PROJECT,
      serviceWorkerPath: "/reachbell-sw.js",
    }).then(async () => {
      const status = await ReachBell.getSubscription();
      setSubscribed(Boolean(status?.granted));
      setReady(true);
    });
  }, []);

  const subscribe = async () => {
    const result = await ReachBell.subscribe();
    setSubscribed(result.granted);
  };

  const unsubscribe = async () => {
    await ReachBell.unsubscribe();
    setSubscribed(false);
  };

  return (
    <PushContext.Provider value={{ ready, subscribed, subscribe, unsubscribe }}>
      {children}
    </PushContext.Provider>
  );
}

export function usePush() {
  const ctx = useContext(PushContext);
  if (!ctx) throw new Error("usePush must be used inside <PushProvider>");
  return ctx;
}

4. Wrap your app

// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { PushProvider } from "./push/PushContext";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <PushProvider>
      <App />
    </PushProvider>
  </React.StrictMode>,
);

5. A permission UI component

A drop-in component that shows a soft prompt, handles the subscribe call, and reflects state. Style to taste.

// src/push/EnablePushCard.tsx
import { usePush } from "./PushContext";

export function EnablePushCard() {
  const { ready, subscribed, subscribe } = usePush();

  if (!ready) return null;
  if (subscribed) {
    return (
      <div className="rounded-lg bg-green-50 p-3 text-green-800 text-sm">
        You will get order updates and offers. Thanks!
      </div>
    );
  }

  return (
    <div className="rounded-lg border p-4">
      <h3 className="font-semibold">Stay in the loop</h3>
      <p className="text-sm text-gray-600 mt-1">
        Get order updates and back-in-stock alerts. One tap, no email needed.
      </p>
      <button
        onClick={subscribe}
        className="mt-3 rounded-md bg-black px-4 py-2 text-white text-sm"
      >
        Allow notifications
      </button>
    </div>
  );
}

6. Trigger pushes from the backend

Sending pushes from the React client is wrong — anyone with browser dev tools could spoof them. Route through your backend. Minimal Node/Express handler:

// server/notify.ts
import express from "express";
import { ReachBellServer } from "@reachbell/server";

const rb = new ReachBellServer({ apiKey: process.env.REACHBELL_API_KEY! });

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

app.post("/api/push/order-shipped", async (req, res) => {
  const { userId, orderId } = req.body;

  await rb.send({
    audience: { externalId: userId },
    title: "Order shipped 📦",
    body: `Order #${orderId} is on its way.`,
    url: `/orders/${orderId}`,
  });

  res.json({ ok: true });
});

Then from your order pipeline (webhook, queue worker, cron) hit `/api/push/order-shipped`. The push lands seconds later.

7. Linking React users to push subscribers

For triggered pushes targeting an authenticated user, pass `externalId` on the subscribe call. Update the context:

const subscribe = async () => {
  const result = await ReachBell.subscribe({
    externalId: user?.id,
    attributes: {
      plan: user?.plan,
      locale: navigator.language,
    },
  });
  setSubscribed(result.granted);
};

Now the backend can target `externalId: userId` and ReachBell delivers across every device that user subscribed on.

8. Handling auth changes

When users log in or out, re-attach the subscription so the externalId matches the right account. Add an effect on user state:

useEffect(() => {
  if (!ready || !subscribed) return;
  ReachBell.setExternalId(user?.id ?? null);
}, [ready, subscribed, user?.id]);

9. Production checklist

  • Service worker at `public/reachbell-sw.js`. After build, confirm it is served from the site root.
  • `VITE_REACHBELL_PROJECT` set in your env. Backend `REACHBELL_API_KEY` lives only in server env vars.
  • Soft prompt triggers on engagement, not page load.
  • Push sends happen from server endpoints only — never expose the API key to the browser.
  • Test on a real Android device — desktop browsers and mobile emulators differ in subtle ways.

Common pitfalls

  • Service worker not found — most often a build/output path issue. Confirm by visiting `/reachbell-sw.js` directly.
  • Subscribed but no pushes arrive — check that the externalId matches what your backend sends. Mismatch = no targets.
  • Multiple subscriptions per user — expected: one per browser per device. Use externalId to send to all of them at once.
  • StrictMode double-init — guard the `init()` call with a ref to avoid duplicate registrations in dev.

That is React push notifications, end to end. See the React integration page for the official sample repo, or the Next.js tutorial if you are on the App Router. Free tier covers your first 1,000 subscribers — enough to ship and iterate.

Put this playbook to work.

Push, email & automations — free for your first 1,000 subscribers.

Start free

Ready to make some noise?

Free forever for your first 1,000 subscribers. Set up in five minutes — no credit card needed.

Start free today