mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Closes [DOCS-1289](https://linear.app/supabase/issue/DOCS-1289/get-the-linter-to-fix-what-it-flags-or-retirereplace-the-linter) Stacked on #50600, which points contributors at the authoring skills. Merge that one first. ## Problem Contributors experienced friction with the linter. They felt nickle and dimed for tiny nits and felt detracted from the work itself. PRs would become noisy with tiny one-word suggestions. Additionally, our homegrown linter is not very intelligent, causing frequent overrides. ## Solution This removes the linter entirely in favor of directing contributors to use SKILLS instead. The removal entails... - **CI.** Delete the three `docs_lint` workflows: the PR check, the external-PR comment companion, and the nightly `--fix` bot. Drop the stale `zizmor.yml` ignore entry for the deleted workflow. - **Tooling.** Delete `supa-mdx-lint.config.toml` and the 14 rule files. Drop the `lint:mdx` script and the `@supabase/supa-mdx-lint` dependency from docs, learn, and ui-library, and regenerate the lockfile. - **Content.** Remove the 181 directives. A separate commit carries Prettier's reformatting of the tables and blank lines those comments had suppressed, so the deletion commit stays readable. No prose changes. - **Style guide.** The word list states each rule directly instead of describing what the linter flagged. Every term survives, including the phrase groups that mirrored `Rule004ExcludeWords`. - **Skills.** `write-the-docs`, `edit-the-docs`, and `review-the-docs` drop `pnpm lint:mdx` from their self-review commands and check the word list directly. `ask-the-docs`'s CI reference drops both workflows. ## Manual testing 1. Run `git grep -i supa-mdx-lint -- . ':!pnpm-lock.yaml'`. No matches. 2. Run `pnpm install --frozen-lockfile --lockfile-only`. It passes, so the lockfile matches the three trimmed manifests. 3. Run `git diff master...HEAD --name-only --diff-filter=ACMR | grep -E '\.(md|mdx)$' | xargs npx prettier --config prettier.config.mjs --check`. All changed markdown passes. 4. Open the [reformatted filter table](https://docs-git-docs-retire-mdx-linter-supabase.vercel.app/docs/guides/observability/logs#filter-events) on the preview and compare it with [production](https://supabase.com/docs/guides/observability/logs#filter-events). The table renders the same. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Documentation guidance now uses manual prose and terminology review with the shared word list. * Clarified storage configuration and common Realtime channel mistakes. * Improved table formatting, text wrapping, and selected reference links. * Updated documentation authoring and review guidance. * **Chores** * Retired automated MDX linting from workflows and local validation commands. * Removed lint-suppression markers throughout documentation without changing instructions. * Added targeted documentation review guidance for pull requests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
235 lines
5.8 KiB
Plaintext
235 lines
5.8 KiB
Plaintext
---
|
|
title = "Fixing the TooManyChannels Error"
|
|
date_created = "2025-11-28T00:00:00+00:00"
|
|
topics = [ "realtime" ]
|
|
keywords = [ "channels", "useEffect", "react", "memory leak", "quota", "TooManyChannels", "ChannelRateLimitReached", "unsubscribe", "cleanup" ]
|
|
database_id = "dee93cc3-0ab1-4101-8ad4-31d8682c8844"
|
|
---
|
|
|
|
## What is the TooManyChannels error?
|
|
|
|
The TooManyChannels error occurs when your application tries to create more than the allowed number of Realtime channels. When you exceed this limit, you'll see an error with the code `ChannelRateLimitReached`.
|
|
|
|
This limit exists to protect both your application and Supabase servers from resource exhaustion.
|
|
|
|
## What causes TooManyChannels errors?
|
|
|
|
The most common cause is accidentally creating channels without cleaning them up, especially in React applications. This happens when:
|
|
|
|
- Components create channels on every render without unsubscribing
|
|
- `useEffect` runs multiple times due to missing or incorrect dependencies
|
|
- Components unmount without cleaning up their channels
|
|
- Development mode in React (StrictMode) causes effects to run twice
|
|
|
|
Each time you call `supabase.channel('topic').subscribe()`, a new channel is created unless you properly clean it up.
|
|
Here's the most common mistake that might lead to TooManyChannels errors:
|
|
|
|
```tsx
|
|
// ❌ WRONG - Creates new channel on every render
|
|
function ChatRoom() {
|
|
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
|
|
|
useEffect(() => {
|
|
const channel = supabase.channel('chat')
|
|
|
|
channel
|
|
.on('broadcast', { event: 'message' }, (payload) => {
|
|
console.log(payload)
|
|
})
|
|
.subscribe()
|
|
|
|
// Missing cleanup!
|
|
}, []) // supabase is missing from dependencies
|
|
|
|
return <div>Chat</div>
|
|
}
|
|
```
|
|
|
|
Why this fails:
|
|
|
|
- Creating `supabase` client inside component causes it to change on every render
|
|
- Missing `supabase` from dependencies array
|
|
- No cleanup function to unsubscribe the channel
|
|
- Each render creates a new channel that's never removed
|
|
|
|
## The correct approach
|
|
|
|
```tsx
|
|
// ✅ CORRECT - Properly manages channel lifecycle
|
|
import { createClient } from '@supabase/supabase-js'
|
|
import { useEffect } from 'react'
|
|
|
|
// Create client outside component (singleton)
|
|
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
|
|
|
function ChatRoom() {
|
|
useEffect(() => {
|
|
const channel = supabase
|
|
.channel('chat')
|
|
.on('broadcast', { event: 'message' }, (payload) => {
|
|
console.log(payload)
|
|
})
|
|
.subscribe()
|
|
|
|
// Cleanup function - ALWAYS unsubscribe!
|
|
return () => {
|
|
channel.unsubscribe()
|
|
}
|
|
}, []) // Empty dependencies because supabase is stable
|
|
|
|
return <div>Chat</div>
|
|
}
|
|
```
|
|
|
|
## How to debug channel creation
|
|
|
|
Check how many channels your app has created:
|
|
|
|
```tsx
|
|
import { useEffect } from 'react'
|
|
|
|
function ChannelDebugger() {
|
|
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
const channels = supabase.getChannels()
|
|
console.log(`Active channels: ${channels.length}`)
|
|
console.log(
|
|
'Channel topics:',
|
|
channels.map((c) => c.topic)
|
|
)
|
|
}, 2000)
|
|
|
|
return () => clearInterval(interval)
|
|
}, [supabase])
|
|
|
|
return <div>Check console for channel count</div>
|
|
}
|
|
```
|
|
|
|
If you see the number climbing, you have a leak. Look for:
|
|
|
|
- Channel count increasing without user action
|
|
- Same channel topics appearing multiple times
|
|
- Count going up when navigating between pages
|
|
|
|
## Best practices for channel management
|
|
|
|
### 1. Create Supabase client outside components
|
|
|
|
```tsx
|
|
// ✅ Create once at module level
|
|
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
|
|
|
function MyComponent() {
|
|
// Use the stable client
|
|
}
|
|
```
|
|
|
|
### 2. Always unsubscribe in cleanup
|
|
|
|
```tsx
|
|
useEffect(() => {
|
|
const channel = supabase.channel('topic').subscribe()
|
|
|
|
return () => {
|
|
channel.unsubscribe()
|
|
}
|
|
}, [])
|
|
```
|
|
|
|
### 3. Use stable channel names
|
|
|
|
```tsx
|
|
// ❌ WRONG - Creates new channel topic on every render
|
|
function BadExample({ userId }) {
|
|
useEffect(() => {
|
|
const channel = supabase
|
|
.channel(`user-${Math.random()}`) // Random topic!
|
|
.subscribe()
|
|
|
|
return () => {
|
|
channel.unsubscribe()
|
|
}
|
|
}, [userId])
|
|
}
|
|
|
|
// ✅ CORRECT - Predictable channel topic
|
|
function GoodExample({ userId }) {
|
|
useEffect(() => {
|
|
const channel = supabase.channel(`user-${userId}`).subscribe()
|
|
|
|
return () => {
|
|
channel.unsubscribe()
|
|
}
|
|
}, [userId])
|
|
}
|
|
```
|
|
|
|
### 4. Reuse channels when possible
|
|
|
|
The Supabase client automatically reuses channels with the same topic:
|
|
|
|
```tsx
|
|
// These return the same channel instance
|
|
const channel1 = supabase.channel('chat')
|
|
const channel2 = supabase.channel('chat') // Same as channel1
|
|
|
|
console.log(channel1 === channel2) // true
|
|
```
|
|
|
|
### 5. Handle strict mode in development
|
|
|
|
React StrictMode intentionally runs effects twice in development. Your cleanup function will handle this:
|
|
|
|
```tsx
|
|
// This works correctly even in StrictMode
|
|
useEffect(() => {
|
|
console.log('Effect running')
|
|
const channel = supabase.channel('chat').subscribe()
|
|
|
|
return () => {
|
|
console.log('Cleanup running')
|
|
channel.unsubscribe()
|
|
}
|
|
}, [])
|
|
```
|
|
|
|
### 6. Clean up on unmount for dynamic channels
|
|
|
|
If you create channels based on props:
|
|
|
|
```tsx
|
|
function RoomComponent({ roomId }) {
|
|
useEffect(() => {
|
|
const channel = supabase
|
|
.channel(`room:${roomId}`)
|
|
.on('broadcast', { event: 'message' }, handleMessage)
|
|
.subscribe()
|
|
|
|
return () => {
|
|
channel.unsubscribe()
|
|
}
|
|
}, [roomId]) // Re-subscribe when roomId changes
|
|
}
|
|
```
|
|
|
|
### 7. Remove all channels when disconnecting
|
|
|
|
When logging out or leaving your app:
|
|
|
|
```tsx
|
|
function LogoutButton() {
|
|
const handleLogout = async () => {
|
|
// Clean up all channels before logout
|
|
await supabase.removeAllChannels()
|
|
|
|
// Then handle logout
|
|
await supabase.auth.signOut()
|
|
}
|
|
|
|
return <button onClick={handleLogout}>Logout</button>
|
|
}
|
|
```
|