reactjsprogressive-web-appspocketbase

The real-time connection is not being restored - ReactJS / Pocketbase


I’m using PocketBase as the backend, with the frontend subscribing to it. The backend sends real-time updates to the frontend using Server-Sent Events (SSE). Everything works fine in the browser. When the app is installed as a PWA on a phone, the SSE connection is lost upon reopening the app after it has been minimized. Additionally, the page/data do not reload. The initial useEffect block containing router.refresh() does not trigger.

useVisibility.tsx

"use client";

import { useState, useEffect } from "react";

export function useVisibility() {
  const [isVisible, setVisible] = useState(true);

  useEffect(() => {
    const handleVisibilityChange = () => {
      setVisible(!document.hidden);
    };

    document.addEventListener("visibilitychange", handleVisibilityChange);
    return () => {
      document.removeEventListener("visibilitychange", handleVisibilityChange);
    };
  }, []);

  return isVisible;
}

AgendaLive.tsx

"use client";

import { Reservation } from "@/types";
import { useEffect } from "react";
import PocketBase from "pocketbase";
import { reservations } from "../tables";
import { useRouter } from "next/navigation";
import { useVisibility } from "@/hooks/useVisibility";

type Props = {
  token: string;
  date: string;
};

const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);

export function AgendaLive({ token, date }: Props) {
  const router = useRouter();
  const isVisible = useVisibility();

  useEffect(() => {
    if (!isVisible) return;
    router.refresh();
  }, [isVisible, router]);

  useEffect(() => {
    if (!isVisible) return;

    pb.authStore.save(token);
    pb.collection(reservations).subscribe<Reservation>("*", (event) => {
      if (event.record.date !== date) {
        return;
      }
      router.refresh();
    });

    return () => {
      pb.collection(reservations).unsubscribe("*");
    };
  }, [token, date, router, isVisible]);

  return null;
}


Solution

  • Sometimes, visibilitychange events are not triggered reliably. Therefore, I introduced an isConnected variable to track the connection status.

    When connecting, all current data is first fully loaded. After that, the system listens for changes in real time.

    Components that need real-time data can use the corresponding hook and get all the important information in one place.

    usePocketbaseRealtime.tsx

    import { useEffect, useState } from "react";
    import PocketBase from "pocketbase";
    
    type Props = {
      collectionName: string;
      filter: string;
      token: string;
      sort: string;
      expand: string;
    };
    
    const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
    export function usePocketbaseRealtime<T extends { id: string }>({
      collectionName,
      filter,
      token,
      sort,
      expand,
    }: Props) {
      const [data, setData] = useState<T[]>([]);
      const [isConnected, setIsConnected] = useState(false);
      const [isOnline, setIsOnline] = useState<boolean>(true);
    
      async function setupRealtimeConnection() {
        if (!isOnline) {
          return;
        }
    
        pb.authStore.save(token);
        try {
          await pb.realtime.unsubscribe(collectionName);
          const records = await pb.collection(collectionName).getFullList<T>({
            filter: filter,
            sort: sort,
            expand: expand,
          });
          setData(records);
    
          await pb.realtime.subscribe(
            collectionName,
            (e) => {
              if (e.action === "create") {
                setData((prevData) => [...prevData, e.record]);
              } else if (e.action === "update") {
                setData((prevData) =>
                  prevData.map((record) =>
                    record.id === e.record.id ? e.record : record
                  )
                );
              } else if (e.action === "delete") {
                setData((prevData) =>
                  prevData.filter((record) => record.id !== e.record.id)
                );
              }
            },
            {
              filter: filter,
              expand: expand,
            }
          );
    
          setIsConnected(true);
        } catch (error) {
          console.error("Error Realtime-Connection:", error);
          setIsConnected(false);
        }
      }
    
      async function disconnectRealtime() {
        setIsConnected(false);
        await pb.realtime.unsubscribe(collectionName);
      }
    
      useEffect(() => {
        const handleOnline = () => {
          setIsOnline(true);
          setupRealtimeConnection();
        };
    
        const handleOffline = () => {
          setIsOnline(false);
          disconnectRealtime();
        };
    
        const handleVisibilityChange = () => {
          if (document.visibilityState === "visible") {
            setupRealtimeConnection();
          } else {
            disconnectRealtime();
          }
        };
    
        if (isOnline) {
          setupRealtimeConnection();
        }
    
        window.addEventListener("online", handleOnline);
        window.addEventListener("offline", handleOffline);
        document.addEventListener("visibilitychange", handleVisibilityChange);
    
        return () => {
          window.removeEventListener("online", handleOnline);
          window.removeEventListener("offline", handleOffline);
          document.removeEventListener("visibilitychange", handleVisibilityChange);
          disconnectRealtime();
        };
      }, [collectionName, filter]);
    
      return { data, isConnected, isOnline, reconnect: setupRealtimeConnection };
    }