mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
update skills
This commit is contained in:
@ -46,24 +46,28 @@ Comprehensive performance optimization guide for React and Next.js applications,
|
|||||||
- 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
|
- 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
|
||||||
- 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
|
- 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
|
||||||
5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
|
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.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
|
||||||
- 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.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
|
||||||
- 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.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)
|
||||||
- 5.4 [Extract to Memoized Components](#54-extract-to-memoized-components)
|
- 5.4 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#54-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
|
||||||
- 5.5 [Narrow Effect Dependencies](#55-narrow-effect-dependencies)
|
- 5.5 [Extract to Memoized Components](#55-extract-to-memoized-components)
|
||||||
- 5.6 [Subscribe to Derived State](#56-subscribe-to-derived-state)
|
- 5.6 [Narrow Effect Dependencies](#56-narrow-effect-dependencies)
|
||||||
- 5.7 [Use Functional setState Updates](#57-use-functional-setstate-updates)
|
- 5.7 [Put Interaction Logic in Event Handlers](#57-put-interaction-logic-in-event-handlers)
|
||||||
- 5.8 [Use Lazy State Initialization](#58-use-lazy-state-initialization)
|
- 5.8 [Subscribe to Derived State](#58-subscribe-to-derived-state)
|
||||||
- 5.9 [Use Transitions for Non-Urgent Updates](#59-use-transitions-for-non-urgent-updates)
|
- 5.9 [Use Functional setState Updates](#59-use-functional-setstate-updates)
|
||||||
|
- 5.10 [Use Lazy State Initialization](#510-use-lazy-state-initialization)
|
||||||
|
- 5.11 [Use Transitions for Non-Urgent Updates](#511-use-transitions-for-non-urgent-updates)
|
||||||
|
- 5.12 [Use useRef for Transient Values](#512-use-useref-for-transient-values)
|
||||||
6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
|
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.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)
|
- 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
|
||||||
- 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
|
- 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
|
||||||
- 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
|
- 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
|
||||||
- 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
|
- 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.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
|
||||||
- 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering)
|
- 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
|
||||||
- 6.8 [Use useTransition Over Manual Loading States](#68-use-usetransition-over-manual-loading-states)
|
- 6.8 [Use Explicit Conditional Rendering](#68-use-explicit-conditional-rendering)
|
||||||
|
- 6.9 [Use useTransition Over Manual Loading States](#69-use-usetransition-over-manual-loading-states)
|
||||||
7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
|
7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
|
||||||
- 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
|
- 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.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
|
||||||
@ -78,8 +82,9 @@ Comprehensive performance optimization guide for React and Next.js applications,
|
|||||||
- 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups)
|
- 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups)
|
||||||
- 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability)
|
- 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. [Advanced Patterns](#8-advanced-patterns) — **LOW**
|
||||||
- 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs)
|
- 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
|
||||||
- 8.2 [useEffectEvent for Stable Callback Refs](#82-useeffectevent-for-stable-callback-refs)
|
- 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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -1278,7 +1283,43 @@ function cachePrefs(user: FullUser) {
|
|||||||
|
|
||||||
Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
|
Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
|
||||||
|
|
||||||
### 5.1 Defer State Reads to Usage Point
|
### 5.1 Calculate Derived State During Rendering
|
||||||
|
|
||||||
|
**Impact: MEDIUM (avoids redundant renders and state drift)**
|
||||||
|
|
||||||
|
If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
|
||||||
|
|
||||||
|
**Incorrect: redundant state and effect**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Form() {
|
||||||
|
const [firstName, setFirstName] = useState('First')
|
||||||
|
const [lastName, setLastName] = useState('Last')
|
||||||
|
const [fullName, setFullName] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFullName(firstName + ' ' + lastName)
|
||||||
|
}, [firstName, lastName])
|
||||||
|
|
||||||
|
return <p>{fullName}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct: derive during render**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Form() {
|
||||||
|
const [firstName, setFirstName] = useState('First')
|
||||||
|
const [lastName, setLastName] = useState('Last')
|
||||||
|
const fullName = firstName + ' ' + lastName
|
||||||
|
|
||||||
|
return <p>{fullName}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
|
||||||
|
|
||||||
|
### 5.2 Defer State Reads to Usage Point
|
||||||
|
|
||||||
**Impact: MEDIUM (avoids unnecessary subscriptions)**
|
**Impact: MEDIUM (avoids unnecessary subscriptions)**
|
||||||
|
|
||||||
@ -1313,7 +1354,7 @@ function ShareButton({ chatId }: { chatId: string }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.2 Do not wrap a simple expression with a primitive result type in useMemo
|
### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
|
||||||
|
|
||||||
**Impact: LOW-MEDIUM (wasted computation on every render)**
|
**Impact: LOW-MEDIUM (wasted computation on every render)**
|
||||||
|
|
||||||
@ -1345,7 +1386,7 @@ function Header({ user, notifications }: Props) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.3 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
### 5.4 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
||||||
|
|
||||||
**Impact: MEDIUM (restores memoization by using a constant for default value)**
|
**Impact: MEDIUM (restores memoization by using a constant for default value)**
|
||||||
|
|
||||||
@ -1377,7 +1418,7 @@ const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () =
|
|||||||
<UserAvatar />
|
<UserAvatar />
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.4 Extract to Memoized Components
|
### 5.5 Extract to Memoized Components
|
||||||
|
|
||||||
**Impact: MEDIUM (enables early returns)**
|
**Impact: MEDIUM (enables early returns)**
|
||||||
|
|
||||||
@ -1417,7 +1458,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.
|
**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.5 Narrow Effect Dependencies
|
### 5.6 Narrow Effect Dependencies
|
||||||
|
|
||||||
**Impact: LOW (minimizes effect re-runs)**
|
**Impact: LOW (minimizes effect re-runs)**
|
||||||
|
|
||||||
@ -1458,7 +1499,48 @@ useEffect(() => {
|
|||||||
}, [isMobile])
|
}, [isMobile])
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.6 Subscribe to Derived State
|
### 5.7 Put Interaction Logic in Event Handlers
|
||||||
|
|
||||||
|
**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**
|
||||||
|
|
||||||
|
If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
|
||||||
|
|
||||||
|
**Incorrect: event modeled as state + effect**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Form() {
|
||||||
|
const [submitted, setSubmitted] = useState(false)
|
||||||
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitted) {
|
||||||
|
post('/api/register')
|
||||||
|
showToast('Registered', theme)
|
||||||
|
}
|
||||||
|
}, [submitted, theme])
|
||||||
|
|
||||||
|
return <button onClick={() => setSubmitted(true)}>Submit</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct: do it in the handler**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Form() {
|
||||||
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
post('/api/register')
|
||||||
|
showToast('Registered', theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button onClick={handleSubmit}>Submit</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
|
||||||
|
|
||||||
|
### 5.8 Subscribe to Derived State
|
||||||
|
|
||||||
**Impact: MEDIUM (reduces re-render frequency)**
|
**Impact: MEDIUM (reduces re-render frequency)**
|
||||||
|
|
||||||
@ -1483,7 +1565,7 @@ function Sidebar() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.7 Use Functional setState Updates
|
### 5.9 Use Functional setState Updates
|
||||||
|
|
||||||
**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
|
**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
|
||||||
|
|
||||||
@ -1561,7 +1643,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.
|
**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.8 Use Lazy State Initialization
|
### 5.10 Use Lazy State Initialization
|
||||||
|
|
||||||
**Impact: MEDIUM (wasted computation on every render)**
|
**Impact: MEDIUM (wasted computation on every render)**
|
||||||
|
|
||||||
@ -1615,7 +1697,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.
|
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
|
||||||
|
|
||||||
### 5.9 Use Transitions for Non-Urgent Updates
|
### 5.11 Use Transitions for Non-Urgent Updates
|
||||||
|
|
||||||
**Impact: MEDIUM (maintains UI responsiveness)**
|
**Impact: MEDIUM (maintains UI responsiveness)**
|
||||||
|
|
||||||
@ -1651,6 +1733,75 @@ function ScrollTracker() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 5.12 Use useRef for Transient Values
|
||||||
|
|
||||||
|
**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
|
||||||
|
|
||||||
|
When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
|
||||||
|
|
||||||
|
**Incorrect: renders every update**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Tracker() {
|
||||||
|
const [lastX, setLastX] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMove = (e: MouseEvent) => setLastX(e.clientX)
|
||||||
|
window.addEventListener('mousemove', onMove)
|
||||||
|
return () => window.removeEventListener('mousemove', onMove)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: lastX,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
background: 'black',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct: no re-render for tracking**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Tracker() {
|
||||||
|
const lastXRef = useRef(0)
|
||||||
|
const dotRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMove = (e: MouseEvent) => {
|
||||||
|
lastXRef.current = e.clientX
|
||||||
|
const node = dotRef.current
|
||||||
|
if (node) {
|
||||||
|
node.style.transform = `translateX(${e.clientX}px)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('mousemove', onMove)
|
||||||
|
return () => window.removeEventListener('mousemove', onMove)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={dotRef}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
background: 'black',
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Rendering Performance
|
## 6. Rendering Performance
|
||||||
@ -1880,7 +2031,33 @@ The inline script executes synchronously before showing the element, ensuring th
|
|||||||
|
|
||||||
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
|
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
|
||||||
|
|
||||||
### 6.6 Use Activity Component for Show/Hide
|
### 6.6 Suppress Expected Hydration Mismatches
|
||||||
|
|
||||||
|
**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
|
||||||
|
|
||||||
|
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
|
||||||
|
|
||||||
|
**Incorrect: known mismatch warnings**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Timestamp() {
|
||||||
|
return <span>{new Date().toLocaleString()}</span>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct: suppress expected mismatch only**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Timestamp() {
|
||||||
|
return (
|
||||||
|
<span suppressHydrationWarning>
|
||||||
|
{new Date().toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.7 Use Activity Component for Show/Hide
|
||||||
|
|
||||||
**Impact: MEDIUM (preserves state/DOM)**
|
**Impact: MEDIUM (preserves state/DOM)**
|
||||||
|
|
||||||
@ -1902,7 +2079,7 @@ function Dropdown({ isOpen }: Props) {
|
|||||||
|
|
||||||
Avoids expensive re-renders and state loss.
|
Avoids expensive re-renders and state loss.
|
||||||
|
|
||||||
### 6.7 Use Explicit Conditional Rendering
|
### 6.8 Use Explicit Conditional Rendering
|
||||||
|
|
||||||
**Impact: LOW (prevents rendering 0 or NaN)**
|
**Impact: LOW (prevents rendering 0 or NaN)**
|
||||||
|
|
||||||
@ -1938,7 +2115,7 @@ function Badge({ count }: { count: number }) {
|
|||||||
// When count = 5, renders: <div><span class="badge">5</span></div>
|
// When count = 5, renders: <div><span class="badge">5</span></div>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.8 Use useTransition Over Manual Loading States
|
### 6.9 Use useTransition Over Manual Loading States
|
||||||
|
|
||||||
**Impact: LOW (reduces re-renders and improves code clarity)**
|
**Impact: LOW (reduces re-renders and improves code clarity)**
|
||||||
|
|
||||||
@ -2635,7 +2812,45 @@ const sorted = [...items].sort((a, b) => a.value - b.value)
|
|||||||
|
|
||||||
Advanced patterns for specific cases that require careful implementation.
|
Advanced patterns for specific cases that require careful implementation.
|
||||||
|
|
||||||
### 8.1 Store Event Handlers in Refs
|
### 8.1 Initialize App Once, Not Per Mount
|
||||||
|
|
||||||
|
**Impact: LOW-MEDIUM (avoids duplicate init in development)**
|
||||||
|
|
||||||
|
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
|
||||||
|
|
||||||
|
**Incorrect: runs twice in dev, re-runs on remount**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Comp() {
|
||||||
|
useEffect(() => {
|
||||||
|
loadFromStorage()
|
||||||
|
checkAuthToken()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct: once per app load**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
let didInit = false
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (didInit) return
|
||||||
|
didInit = true
|
||||||
|
loadFromStorage()
|
||||||
|
checkAuthToken()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
**Impact: LOW (stable subscriptions)**
|
**Impact: LOW (stable subscriptions)**
|
||||||
|
|
||||||
@ -2671,7 +2886,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.
|
`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.2 useEffectEvent for Stable Callback Refs
|
### 8.3 useEffectEvent for Stable Callback Refs
|
||||||
|
|
||||||
**Impact: LOW (prevents effect re-runs)**
|
**Impact: LOW (prevents effect re-runs)**
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ metadata:
|
|||||||
|
|
||||||
# Vercel React Best Practices
|
# Vercel React Best Practices
|
||||||
|
|
||||||
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 45 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 57 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
|
||||||
|
|
||||||
## When to Apply
|
## When to Apply
|
||||||
|
|
||||||
@ -53,8 +53,10 @@ Reference these guidelines when:
|
|||||||
|
|
||||||
### 3. Server-Side Performance (HIGH)
|
### 3. Server-Side Performance (HIGH)
|
||||||
|
|
||||||
|
- `server-auth-actions` - Authenticate server actions like API routes
|
||||||
- `server-cache-react` - Use React.cache() for per-request deduplication
|
- `server-cache-react` - Use React.cache() for per-request deduplication
|
||||||
- `server-cache-lru` - Use LRU cache for cross-request caching
|
- `server-cache-lru` - Use LRU cache for cross-request caching
|
||||||
|
- `server-dedup-props` - Avoid duplicate serialization in RSC props
|
||||||
- `server-serialization` - Minimize data passed to client components
|
- `server-serialization` - Minimize data passed to client components
|
||||||
- `server-parallel-fetching` - Restructure components to parallelize fetches
|
- `server-parallel-fetching` - Restructure components to parallelize fetches
|
||||||
- `server-after-nonblocking` - Use after() for non-blocking operations
|
- `server-after-nonblocking` - Use after() for non-blocking operations
|
||||||
@ -63,16 +65,23 @@ Reference these guidelines when:
|
|||||||
|
|
||||||
- `client-swr-dedup` - Use SWR for automatic request deduplication
|
- `client-swr-dedup` - Use SWR for automatic request deduplication
|
||||||
- `client-event-listeners` - Deduplicate global event listeners
|
- `client-event-listeners` - Deduplicate global event listeners
|
||||||
|
- `client-passive-event-listeners` - Use passive listeners for scroll
|
||||||
|
- `client-localstorage-schema` - Version and minimize localStorage data
|
||||||
|
|
||||||
### 5. Re-render Optimization (MEDIUM)
|
### 5. Re-render Optimization (MEDIUM)
|
||||||
|
|
||||||
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
|
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
|
||||||
- `rerender-memo` - Extract expensive work into memoized components
|
- `rerender-memo` - Extract expensive work into memoized components
|
||||||
|
- `rerender-memo-with-default-value` - Hoist default non-primitive props
|
||||||
- `rerender-dependencies` - Use primitive dependencies in effects
|
- `rerender-dependencies` - Use primitive dependencies in effects
|
||||||
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
|
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
|
||||||
|
- `rerender-derived-state-no-effect` - Derive state during render, not effects
|
||||||
- `rerender-functional-setstate` - Use functional setState for stable callbacks
|
- `rerender-functional-setstate` - Use functional setState for stable callbacks
|
||||||
- `rerender-lazy-state-init` - Pass function to useState for expensive values
|
- `rerender-lazy-state-init` - Pass function to useState for expensive values
|
||||||
|
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
|
||||||
|
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
|
||||||
- `rerender-transitions` - Use startTransition for non-urgent updates
|
- `rerender-transitions` - Use startTransition for non-urgent updates
|
||||||
|
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
|
||||||
|
|
||||||
### 6. Rendering Performance (MEDIUM)
|
### 6. Rendering Performance (MEDIUM)
|
||||||
|
|
||||||
@ -81,8 +90,10 @@ Reference these guidelines when:
|
|||||||
- `rendering-hoist-jsx` - Extract static JSX outside components
|
- `rendering-hoist-jsx` - Extract static JSX outside components
|
||||||
- `rendering-svg-precision` - Reduce SVG coordinate precision
|
- `rendering-svg-precision` - Reduce SVG coordinate precision
|
||||||
- `rendering-hydration-no-flicker` - Use inline script for client-only data
|
- `rendering-hydration-no-flicker` - Use inline script for client-only data
|
||||||
|
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
|
||||||
- `rendering-activity` - Use Activity component for show/hide
|
- `rendering-activity` - Use Activity component for show/hide
|
||||||
- `rendering-conditional-render` - Use ternary, not && for conditionals
|
- `rendering-conditional-render` - Use ternary, not && for conditionals
|
||||||
|
- `rendering-usetransition-loading` - Prefer useTransition for loading state
|
||||||
|
|
||||||
### 7. JavaScript Performance (LOW-MEDIUM)
|
### 7. JavaScript Performance (LOW-MEDIUM)
|
||||||
|
|
||||||
@ -102,6 +113,7 @@ Reference these guidelines when:
|
|||||||
### 8. Advanced Patterns (LOW)
|
### 8. Advanced Patterns (LOW)
|
||||||
|
|
||||||
- `advanced-event-handler-refs` - Store event handlers in refs
|
- `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
|
- `advanced-use-latest` - useLatest for stable callback refs
|
||||||
|
|
||||||
## How to Use
|
## How to Use
|
||||||
@ -111,7 +123,6 @@ Read individual rule files for detailed explanations and code examples:
|
|||||||
```
|
```
|
||||||
rules/async-parallel.md
|
rules/async-parallel.md
|
||||||
rules/bundle-barrel-imports.md
|
rules/bundle-barrel-imports.md
|
||||||
rules/_sections.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Each rule file contains:
|
Each rule file contains:
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
title: Initialize App Once, Not Per Mount
|
||||||
|
impact: LOW-MEDIUM
|
||||||
|
impactDescription: avoids duplicate init in development
|
||||||
|
tags: initialization, useEffect, app-startup, side-effects
|
||||||
|
---
|
||||||
|
|
||||||
|
## Initialize App Once, Not Per Mount
|
||||||
|
|
||||||
|
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
|
||||||
|
|
||||||
|
**Incorrect (runs twice in dev, re-runs on remount):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Comp() {
|
||||||
|
useEffect(() => {
|
||||||
|
loadFromStorage()
|
||||||
|
checkAuthToken()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (once per app load):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
let didInit = false
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (didInit) return
|
||||||
|
didInit = true
|
||||||
|
loadFromStorage()
|
||||||
|
checkAuthToken()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
title: Suppress Expected Hydration Mismatches
|
||||||
|
impact: LOW-MEDIUM
|
||||||
|
impactDescription: avoids noisy hydration warnings for known differences
|
||||||
|
tags: rendering, hydration, ssr, nextjs
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suppress Expected Hydration Mismatches
|
||||||
|
|
||||||
|
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
|
||||||
|
|
||||||
|
**Incorrect (known mismatch warnings):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Timestamp() {
|
||||||
|
return <span>{new Date().toLocaleString()}</span>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (suppress expected mismatch only):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Timestamp() {
|
||||||
|
return (
|
||||||
|
<span suppressHydrationWarning>
|
||||||
|
{new Date().toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Calculate Derived State During Rendering
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: avoids redundant renders and state drift
|
||||||
|
tags: rerender, derived-state, useEffect, state
|
||||||
|
---
|
||||||
|
|
||||||
|
## Calculate Derived State During Rendering
|
||||||
|
|
||||||
|
If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
|
||||||
|
|
||||||
|
**Incorrect (redundant state and effect):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Form() {
|
||||||
|
const [firstName, setFirstName] = useState('First')
|
||||||
|
const [lastName, setLastName] = useState('Last')
|
||||||
|
const [fullName, setFullName] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFullName(firstName + ' ' + lastName)
|
||||||
|
}, [firstName, lastName])
|
||||||
|
|
||||||
|
return <p>{fullName}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (derive during render):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Form() {
|
||||||
|
const [firstName, setFirstName] = useState('First')
|
||||||
|
const [lastName, setLastName] = useState('Last')
|
||||||
|
const fullName = firstName + ' ' + lastName
|
||||||
|
|
||||||
|
return <p>{fullName}</p>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: Put Interaction Logic in Event Handlers
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: avoids effect re-runs and duplicate side effects
|
||||||
|
tags: rerender, useEffect, events, side-effects, dependencies
|
||||||
|
---
|
||||||
|
|
||||||
|
## Put Interaction Logic in Event Handlers
|
||||||
|
|
||||||
|
If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
|
||||||
|
|
||||||
|
**Incorrect (event modeled as state + effect):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Form() {
|
||||||
|
const [submitted, setSubmitted] = useState(false)
|
||||||
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitted) {
|
||||||
|
post('/api/register')
|
||||||
|
showToast('Registered', theme)
|
||||||
|
}
|
||||||
|
}, [submitted, theme])
|
||||||
|
|
||||||
|
return <button onClick={() => setSubmitted(true)}>Submit</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (do it in the handler):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Form() {
|
||||||
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
post('/api/register')
|
||||||
|
showToast('Registered', theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button onClick={handleSubmit}>Submit</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
title: Use useRef for Transient Values
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: avoids unnecessary re-renders on frequent updates
|
||||||
|
tags: rerender, useref, state, performance
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use useRef for Transient Values
|
||||||
|
|
||||||
|
When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
|
||||||
|
|
||||||
|
**Incorrect (renders every update):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Tracker() {
|
||||||
|
const [lastX, setLastX] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMove = (e: MouseEvent) => setLastX(e.clientX)
|
||||||
|
window.addEventListener('mousemove', onMove)
|
||||||
|
return () => window.removeEventListener('mousemove', onMove)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: lastX,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
background: 'black',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (no re-render for tracking):**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function Tracker() {
|
||||||
|
const lastXRef = useRef(0)
|
||||||
|
const dotRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMove = (e: MouseEvent) => {
|
||||||
|
lastXRef.current = e.clientX
|
||||||
|
const node = dotRef.current
|
||||||
|
if (node) {
|
||||||
|
node.style.transform = `translateX(${e.clientX}px)`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('mousemove', onMove)
|
||||||
|
return () => window.removeEventListener('mousemove', onMove)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={dotRef}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
background: 'black',
|
||||||
|
transform: 'translateX(0px)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user