DEV Community

devhabeeb
devhabeeb

Posted on

Extracting a React presence hook from a production event app

How we turned a battle-tested Supabase Realtime Presence layer into react-supabase-presence
Real-time “who is in the room” sounds simple until you ship it on phones.

Tabs freeze. Networks drop. React Strict Mode double-mounts effects. Supabase will often restore the socket for you — but presence track state does not magically re-announce itself. If you do not re-track after resubscribe, other clients still think you are offline.

We hit that in production while building an event networking product (QR check-in, live roster, ephemeral rooms). The presence logic had to be honest in the UI: not a single boolean “connected,” but a small state machine the interface could show without lying.

That code is now open source:
npm: react-supabase-presence
GitHub: devhabeeblateef/react-supabase-presence
License: MIT

The problem we actually had
Supabase Realtime Presence is a good primitive. The gaps show up at the edges:

Lifecycle — subscribe → track → sync/join/leave. Miss a re-track after reconnect and your user vanishes for everyone else.
Mobile browsers — background tabs can freeze sockets without a clean CLOSED event. Coming back to the tab should re-announce.
UI honesty — “loading” vs “live” vs “trying again” are different states. Collapsing them into one spinner trains users to ignore the product.

App boundaries — roster, roles, and business logic should not live inside the presence hook. The hook should only answer: who is online, and is the channel healthy?

We implemented that as a React hook inside the product first, then extracted it so the package has zero knowledge of events, QR codes, or billing.

What the hook does

`import { useEventPresence } from "react-supabase-presence";
import type { EventPresence, PresenceStatus } from "react-supabase-presence";
TypeScriptconst { online, status, version } = useEventPresence(
  supabase,   // your browser Supabase client
  channelKey, // stable room / event id
  selfId      // stable presence key for the current user
);
Enter fullscreen mode Exit fullscreen mode

Behaviour that mattered in production

Channel name is derived as presence:${channelKey} with config.presence.key = selfId.
On SUBSCRIBED, the hook tracks { user_id, at }.
On CHANNEL_ERROR / TIMED_OUT / CLOSED, status becomes reconnecting (the client library may still recover underneath).
On visibilitychange → visible, it re-tracks so a returning mobile tab reappears.

Cleanup removes the channel; no shared global channel name that breaks under Strict Mode remounts.

You own the SupabaseClient. The library does not create clients, read env vars, or assume your schema.

Minimal example

`"use client";
import { createClient } from "@supabase/supabase-js";
import { useEventPresence } from "react-supabase-presence";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

export function Room({ roomId, userId }: { roomId: string; userId: string }) {
  const { online, status, version } = useEventPresence(
    supabase,
    roomId,
    userId
  );

  return (
    <div>
      <p>Status: {status}</p>
      <p>
        {online.size} online (v{version})
      </p>
      <ul>
        {[...online].map((id) => (
          <li key={id}>{id}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Install: npm install react-supabase-presence
Peer dependencies: react (≥18) and @supabase/supabase-js (^2.40).

Design choices (and non-goals)
In scope for v0.1

Presence subscribe + track + state aggregation
Explicit connection state for UI
Tab-visible re-track
TypeScript types exported from the package root

Out of scope (on purpose)
Authentication
Authorization / RLS policy design
Handshakes, chat, or matching
Rendering components (dots, avatars, lists)

Those belong in the product. Keeping them out is what makes the package reusable and reviewable.
The product that motivated this work still uses the published package for its live room roster — same API, one implementation path.

Why open source this slice
Presence is a sharp edge many Supabase + React apps re-implement poorly. Publishing a small, MIT-licensed hook:

Documents the reconnect and visibility behaviour in public
Gives others a starting point that is not tied to one startup’s domain model
Forces a clean boundary: if it needs your event table, it does not belong in the library

If you find bugs or want a multi-tab leader election helper, issues and PRs are welcome on the repo.

Links
npm: https://www.npmjs.com/package/react-supabase-presence
Source: https://github.com/devhabeeblateef/react-supabase-presence
License: MIT

Top comments (0)