diff --git a/.agents/skills/vercel-react-best-practices/AGENTS.md b/.agents/skills/vercel-react-best-practices/AGENTS.md
index b54829e9dd..e7b7ca90df 100644
--- a/.agents/skills/vercel-react-best-practices/AGENTS.md
+++ b/.agents/skills/vercel-react-best-practices/AGENTS.md
@@ -47,12 +47,14 @@ Comprehensive performance optimization guide for React and Next.js applications,
- 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
- 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point)
- - 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components)
- - 5.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies)
- - 5.4 [Subscribe to Derived State](#54-subscribe-to-derived-state)
- - 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates)
- - 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization)
- - 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates)
+ - 5.2 [Do not wrap a simple expression with a primitive result type in useMemo](#52-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)
+ - 5.3 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#53-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
+ - 5.4 [Extract to Memoized Components](#54-extract-to-memoized-components)
+ - 5.5 [Narrow Effect Dependencies](#55-narrow-effect-dependencies)
+ - 5.6 [Subscribe to Derived State](#56-subscribe-to-derived-state)
+ - 5.7 [Use Functional setState Updates](#57-use-functional-setstate-updates)
+ - 5.8 [Use Lazy State Initialization](#58-use-lazy-state-initialization)
+ - 5.9 [Use Transitions for Non-Urgent Updates](#59-use-transitions-for-non-urgent-updates)
6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
- 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
- 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
@@ -61,8 +63,9 @@ Comprehensive performance optimization guide for React and Next.js applications,
- 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
- 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide)
- 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering)
+ - 6.8 [Use useTransition Over Manual Loading States](#68-use-usetransition-over-manual-loading-states)
7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
- - 7.1 [Batch DOM CSS Changes](#71-batch-dom-css-changes)
+ - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
- 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
- 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
- 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
@@ -76,7 +79,7 @@ Comprehensive performance optimization guide for React and Next.js applications,
- 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability)
8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
- 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs)
- - 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs)
+ - 8.2 [useEffectEvent for Stable Callback Refs](#82-useeffectevent-for-stable-callback-refs)
---
@@ -192,6 +195,21 @@ const { user, config, profile } = await all({
})
```
+**Alternative without extra dependencies:**
+
+```typescript
+const userPromise = fetchUser()
+const profilePromise = userPromise.then(user => fetchProfile(user.id))
+
+const [user, config, profile] = await Promise.all([
+ userPromise,
+ fetchConfig(),
+ profilePromise
+])
+```
+
+We can also create all the promises first, and do `Promise.all()` at the end.
+
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
### 1.3 Prevent Waterfall Chains in API Routes
@@ -1295,7 +1313,71 @@ function ShareButton({ chatId }: { chatId: string }) {
}
```
-### 5.2 Extract to Memoized Components
+### 5.2 Do not wrap a simple expression with a primitive result type in useMemo
+
+**Impact: LOW-MEDIUM (wasted computation on every render)**
+
+When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
+
+Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
+
+**Incorrect:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = useMemo(() => {
+ return user.isLoading || notifications.isLoading
+ }, [user.isLoading, notifications.isLoading])
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+**Correct:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = user.isLoading || notifications.isLoading
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+### 5.3 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+
+**Impact: MEDIUM (restores memoization by using a constant for default value)**
+
+When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
+
+To address this issue, extract the default value into a constant.
+
+**Incorrect: `onClick` has different values on every rerender**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+**Correct: stable default value**
+
+```tsx
+const NOOP = () => {};
+
+const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+### 5.4 Extract to Memoized Components
**Impact: MEDIUM (enables early returns)**
@@ -1335,7 +1417,7 @@ function Profile({ user, loading }: Props) {
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
-### 5.3 Narrow Effect Dependencies
+### 5.5 Narrow Effect Dependencies
**Impact: LOW (minimizes effect re-runs)**
@@ -1376,7 +1458,7 @@ useEffect(() => {
}, [isMobile])
```
-### 5.4 Subscribe to Derived State
+### 5.6 Subscribe to Derived State
**Impact: MEDIUM (reduces re-render frequency)**
@@ -1401,7 +1483,7 @@ function Sidebar() {
}
```
-### 5.5 Use Functional setState Updates
+### 5.7 Use Functional setState Updates
**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
@@ -1479,7 +1561,7 @@ function TodoList() {
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
-### 5.6 Use Lazy State Initialization
+### 5.8 Use Lazy State Initialization
**Impact: MEDIUM (wasted computation on every render)**
@@ -1533,7 +1615,7 @@ Use lazy initialization when computing initial values from localStorage/sessionS
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
-### 5.7 Use Transitions for Non-Urgent Updates
+### 5.9 Use Transitions for Non-Urgent Updates
**Impact: MEDIUM (maintains UI responsiveness)**
@@ -1856,6 +1938,80 @@ function Badge({ count }: { count: number }) {
// When count = 5, renders:
5
```
+### 6.8 Use useTransition Over Manual Loading States
+
+**Impact: LOW (reduces re-renders and improves code clarity)**
+
+Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
+
+**Incorrect: manual loading state**
+
+```tsx
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSearch = async (value: string) => {
+ setIsLoading(true)
+ setQuery(value)
+ const data = await fetchResults(value)
+ setResults(data)
+ setIsLoading(false)
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isLoading && }
+
+ >
+ )
+}
+```
+
+**Correct: useTransition with built-in pending state**
+
+```tsx
+import { useTransition, useState } from 'react'
+
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isPending, startTransition] = useTransition()
+
+ const handleSearch = (value: string) => {
+ setQuery(value) // Update input immediately
+
+ startTransition(async () => {
+ // Fetch and update results
+ const data = await fetchResults(value)
+ setResults(data)
+ })
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isPending && }
+
+ >
+ )
+}
+```
+
+**Benefits:**
+
+- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
+
+- **Error resilience**: Pending state correctly resets even if the transition throws
+
+- **Better responsiveness**: Keeps the UI responsive during updates
+
+- **Interrupt handling**: New transitions automatically cancel pending ones
+
+Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
+
---
## 7. JavaScript Performance
@@ -1864,16 +2020,28 @@ function Badge({ count }: { count: number }) {
Micro-optimizations for hot paths can add up to meaningful improvements.
-### 7.1 Batch DOM CSS Changes
+### 7.1 Avoid Layout Thrashing
-**Impact: MEDIUM (reduces reflows/repaints)**
+**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
+**This is OK: browser batches style changes**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ // Each line invalidates style, but browser batches the recalculation
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+}
+```
+
**Incorrect: interleaved reads and writes force reflows**
```typescript
-function updateElementStyles(element: HTMLElement) {
+function layoutThrashing(element: HTMLElement) {
element.style.width = '100px'
const width = element.offsetWidth // Forces reflow
element.style.height = '200px'
@@ -1885,16 +2053,61 @@ function updateElementStyles(element: HTMLElement) {
```typescript
function updateElementStyles(element: HTMLElement) {
- element.classList.add('highlighted-box')
+ // Batch all writes together
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+
+ // Read after all writes are done (single reflow)
+ const { width, height } = element.getBoundingClientRect()
+}
+```
+**Correct: batch reads, then writes**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ element.classList.add('highlighted-box')
+
const { width, height } = element.getBoundingClientRect()
}
```
**Better: use CSS classes**
+**React example:**
+
+```tsx
+// Incorrect: interleaving style changes with layout queries
+function Box({ isHighlighted }: { isHighlighted: boolean }) {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ if (ref.current && isHighlighted) {
+ ref.current.style.width = '100px'
+ const width = ref.current.offsetWidth // Forces layout
+ ref.current.style.height = '200px'
+ }
+ }, [isHighlighted])
+
+ return