Demo, all content is generated
Question

'tried to subscribe multiple times' error with Supabase realtime in React

Solved · 264 views · asked by santi_dev_wannabe · edited

I have a chat component. Cursor wrote this:

const channel = supabase.channel('room-1')

useEffect(() => {
  channel
    .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (p) => setMessages(m => [...m, p.new]))
    .subscribe()
}, [])

In dev I get:

tried to subscribe multiple times. 'subscribe' can only be called a single time per channel instance

and sometimes every message shows up twice.

What I’ve tried

Removed React.StrictMode, the error went away but Cursor says that's not the fix. Tried moving the channel into a useRef.

Comment
Is the channel created outside the component or inside useEffect? mei_lin · edited
outside, at the top of the component santi_dev_wannabe · edited

3 answers

Marked as helpful by the asker
dev_ana · edited

Create the channel inside the effect and remove it in the cleanup. StrictMode mounts, unmounts and mounts again in dev on purpose, to surface exactly this missing cleanup.

useEffect(() => {
  const channel = supabase
    .channel('room-1')
    .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' },
      (p) => setMessages((m) => [...m, p.new as Message]))
    .subscribe()

  return () => { supabase.removeChannel(channel) }
}, [])

Put StrictMode back. The duplicated messages were two live subscriptions from the double mount. In production without cleanup you'd leak a channel every time someone navigates away and back.

Comment
works, and no more doubles. put strictmode back too santi_dev_wannabe · edited
mei_lin · edited

If you later filter by room, include the room id in the dependency array and in the channel name, e.g. room-${roomId}. Otherwise switching rooms keeps you on the old channel.

Comment
aiko_n · edited

Also check where the supabase client itself is created. If it's createClient() inside the component body, every render makes a new client with its own socket. Create it once in a module (or use createBrowserClient from @supabase/ssr, which reuses one instance in the browser).

Comment