Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/react-best-practices-build/test-cases.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
[
{
"ruleId": "",
"ruleTitle": "Do Not Put Effect Events in Dependency Arrays",
"type": "bad",
"code": "import { useEffect, useEffectEvent } from 'react'\n\nfunction ChatRoom({ roomId, onConnected }: {\n roomId: string\n onConnected: () => void\n}) {\n const handleConnected = useEffectEvent(onConnected)\n\n useEffect(() => {\n const connection = createConnection(roomId)\n connection.on('connected', handleConnected)\n connection.connect()\n\n return () => connection.disconnect()\n }, [roomId, handleConnected])\n}",
"language": "tsx",
"description": "Effect Event added as a dependency"
},
{
"ruleId": "",
"ruleTitle": "Do Not Put Effect Events in Dependency Arrays",
"type": "good",
"code": "import { useEffect, useEffectEvent } from 'react'\n\nfunction ChatRoom({ roomId, onConnected }: {\n roomId: string\n onConnected: () => void\n}) {\n const handleConnected = useEffectEvent(onConnected)\n\n useEffect(() => {\n const connection = createConnection(roomId)\n connection.on('connected', handleConnected)\n connection.connect()\n\n return () => connection.disconnect()\n }, [roomId])\n}",
"language": "tsx",
"description": "depend on reactive values, not the Effect Event"
},
{
"ruleId": "",
"ruleTitle": "Store Event Handlers in Refs",
Expand Down
Binary file modified skills/react-best-practices.zip
Binary file not shown.
65 changes: 59 additions & 6 deletions skills/react-best-practices/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ Comprehensive performance optimization guide for React and Next.js applications,
- 7.13 [Use Set/Map for O(1) Lookups](#713-use-setmap-for-o1-lookups)
- 7.14 [Use toSorted() Instead of sort() for Immutability](#714-use-tosorted-instead-of-sort-for-immutability)
8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
- 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
- 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)
- 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
- 8.1 [Do Not Put Effect Events in Dependency Arrays](#81-do-not-put-effect-events-in-dependency-arrays)
- 8.2 [Initialize App Once, Not Per Mount](#82-initialize-app-once-not-per-mount)
- 8.3 [Store Event Handlers in Refs](#83-store-event-handlers-in-refs)
- 8.4 [useEffectEvent for Stable Callback Refs](#84-useeffectevent-for-stable-callback-refs)

---

Expand Down Expand Up @@ -3575,7 +3576,59 @@ const sorted = [...items].sort((a, b) => a.value - b.value)

Advanced patterns for specific cases that require careful implementation.

### 8.1 Initialize App Once, Not Per Mount
### 8.1 Do Not Put Effect Events in Dependency Arrays

**Impact: LOW (avoids unnecessary effect re-runs and lint errors)**

Effect Event functions do not have a stable identity. Their identity intentionally changes on every render. Do not include the function returned by `useEffectEvent` in a `useEffect` dependency array. Keep the actual reactive values as dependencies and call the Effect Event from inside the effect body or subscriptions created by that effect.

**Incorrect: Effect Event added as a dependency**

```tsx
import { useEffect, useEffectEvent } from 'react'

function ChatRoom({ roomId, onConnected }: {
roomId: string
onConnected: () => void
}) {
const handleConnected = useEffectEvent(onConnected)

useEffect(() => {
const connection = createConnection(roomId)
connection.on('connected', handleConnected)
connection.connect()

return () => connection.disconnect()
}, [roomId, handleConnected])
}
```

Including the Effect Event in dependencies makes the effect re-run every render and triggers the React Hooks lint rule.

**Correct: depend on reactive values, not the Effect Event**

```tsx
import { useEffect, useEffectEvent } from 'react'

function ChatRoom({ roomId, onConnected }: {
roomId: string
onConnected: () => void
}) {
const handleConnected = useEffectEvent(onConnected)

useEffect(() => {
const connection = createConnection(roomId)
connection.on('connected', handleConnected)
connection.connect()

return () => connection.disconnect()
}, [roomId])
}
```

Reference: [https://react.dev/reference/react/useEffectEvent#effect-event-in-deps](https://react.dev/reference/react/useEffectEvent#effect-event-in-deps)

### 8.2 Initialize App Once, Not Per Mount

**Impact: LOW-MEDIUM (avoids duplicate init in development)**

Expand Down Expand Up @@ -3613,7 +3666,7 @@ function Comp() {

Reference: [https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)

### 8.2 Store Event Handlers in Refs
### 8.3 Store Event Handlers in Refs

**Impact: LOW (stable subscriptions)**

Expand Down Expand Up @@ -3649,7 +3702,7 @@ function useWindowEvent(event: string, handler: (e) => void) {

`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.

### 8.3 useEffectEvent for Stable Callback Refs
### 8.4 useEffectEvent for Stable Callback Refs

**Impact: LOW (prevents effect re-runs)**

Expand Down
3 changes: 2 additions & 1 deletion skills/react-best-practices/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ metadata:

# Vercel React Best Practices

Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 68 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 69 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.

## When to Apply

Expand Down Expand Up @@ -123,6 +123,7 @@ Reference these guidelines when:

### 8. Advanced Patterns (LOW)

- `advanced-effect-event-deps` - Don't put `useEffectEvent` results in effect deps
- `advanced-event-handler-refs` - Store event handlers in refs
- `advanced-init-once` - Initialize app once per app load
- `advanced-use-latest` - useLatest for stable callback refs
Expand Down
56 changes: 56 additions & 0 deletions skills/react-best-practices/rules/advanced-effect-event-deps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: Do Not Put Effect Events in Dependency Arrays
impact: LOW
impactDescription: avoids unnecessary effect re-runs and lint errors
tags: advanced, hooks, useEffectEvent, dependencies, effects
---

## Do Not Put Effect Events in Dependency Arrays

Effect Event functions do not have a stable identity. Their identity intentionally changes on every render. Do not include the function returned by `useEffectEvent` in a `useEffect` dependency array. Keep the actual reactive values as dependencies and call the Effect Event from inside the effect body or subscriptions created by that effect.

**Incorrect (Effect Event added as a dependency):**

```tsx
import { useEffect, useEffectEvent } from 'react'

function ChatRoom({ roomId, onConnected }: {
roomId: string
onConnected: () => void
}) {
const handleConnected = useEffectEvent(onConnected)

useEffect(() => {
const connection = createConnection(roomId)
connection.on('connected', handleConnected)
connection.connect()

return () => connection.disconnect()
}, [roomId, handleConnected])
}
```

Including the Effect Event in dependencies makes the effect re-run every render and triggers the React Hooks lint rule.

**Correct (depend on reactive values, not the Effect Event):**

```tsx
import { useEffect, useEffectEvent } from 'react'

function ChatRoom({ roomId, onConnected }: {
roomId: string
onConnected: () => void
}) {
const handleConnected = useEffectEvent(onConnected)

useEffect(() => {
const connection = createConnection(roomId)
connection.on('connected', handleConnected)
connection.connect()

return () => connection.disconnect()
}, [roomId])
}
```

Reference: [React useEffectEvent: Effect Event in deps](https://react.dev/reference/react/useEffectEvent#effect-event-in-deps)
Loading