mirror of
https://github.com/langgenius/dify.git
synced 2026-06-17 05:07:44 +08:00
Compare commits
1 Commits
fix/parall
...
feat/pull-
| Author | SHA1 | Date | |
|---|---|---|---|
| ea37904c75 |
1
.agent/skills
Symbolic link
1
.agent/skills
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../.claude/skills
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/component-refactoring
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/frontend-code-review
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/frontend-testing
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/orpc-contract-first
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/skill-creator
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/vercel-react-best-practices
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/web-design-guidelines
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
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)
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
---
|
|
||||||
title: Avoid Layout Thrashing
|
|
||||||
impact: MEDIUM
|
|
||||||
impactDescription: prevents forced synchronous layouts and reduces performance bottlenecks
|
|
||||||
tags: javascript, dom, css, performance, reflow, layout-thrashing
|
|
||||||
---
|
|
||||||
|
|
||||||
## Avoid Layout Thrashing
|
|
||||||
|
|
||||||
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 layoutThrashing(element: HTMLElement) {
|
|
||||||
element.style.width = '100px'
|
|
||||||
const width = element.offsetWidth // Forces reflow
|
|
||||||
element.style.height = '200px'
|
|
||||||
const height = element.offsetHeight // Forces another reflow
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct (batch writes, then read once):**
|
|
||||||
```typescript
|
|
||||||
function updateElementStyles(element: HTMLElement) {
|
|
||||||
// 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 avoidThrashing(element: HTMLElement) {
|
|
||||||
// Read phase - all layout queries first
|
|
||||||
const rect1 = element.getBoundingClientRect()
|
|
||||||
const offsetWidth = element.offsetWidth
|
|
||||||
const offsetHeight = element.offsetHeight
|
|
||||||
|
|
||||||
// Write phase - all style changes after
|
|
||||||
element.style.width = '100px'
|
|
||||||
element.style.height = '200px'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Better: use CSS classes**
|
|
||||||
```css
|
|
||||||
.highlighted-box {
|
|
||||||
width: 100px;
|
|
||||||
height: 200px;
|
|
||||||
background-color: blue;
|
|
||||||
border: 1px solid black;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
```typescript
|
|
||||||
function updateElementStyles(element: HTMLElement) {
|
|
||||||
element.classList.add('highlighted-box')
|
|
||||||
|
|
||||||
const { width, height } = element.getBoundingClientRect()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**React example:**
|
|
||||||
```tsx
|
|
||||||
// Incorrect: interleaving style changes with layout queries
|
|
||||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
|
||||||
const ref = useRef<HTMLDivElement>(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 <div ref={ref}>Content</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Correct: toggle class
|
|
||||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
|
||||||
return (
|
|
||||||
<div className={isHighlighted ? 'highlighted-box' : ''}>
|
|
||||||
Content
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
|
|
||||||
|
|
||||||
See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
---
|
|
||||||
title: Use useTransition Over Manual Loading States
|
|
||||||
impact: LOW
|
|
||||||
impactDescription: reduces re-renders and improves code clarity
|
|
||||||
tags: rendering, transitions, useTransition, loading, state
|
|
||||||
---
|
|
||||||
|
|
||||||
## Use useTransition Over Manual Loading States
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<input onChange={(e) => handleSearch(e.target.value)} />
|
|
||||||
{isLoading && <Spinner />}
|
|
||||||
<ResultsList results={results} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**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 (
|
|
||||||
<>
|
|
||||||
<input onChange={(e) => handleSearch(e.target.value)} />
|
|
||||||
{isPending && <Spinner />}
|
|
||||||
<ResultsList results={results} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**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: [useTransition](https://react.dev/reference/react/useTransition)
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
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)
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
|
||||||
impact: MEDIUM
|
|
||||||
impactDescription: restores memoization by using a constant for default value
|
|
||||||
tags: rerender, memo, optimization
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
|
||||||
|
|
||||||
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
|
|
||||||
<UserAvatar />
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct (stable default value):**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const NOOP = () => {};
|
|
||||||
|
|
||||||
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
|
|
||||||
// ...
|
|
||||||
})
|
|
||||||
|
|
||||||
// Used without optional onClick
|
|
||||||
<UserAvatar />
|
|
||||||
```
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
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)
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
title: Do not wrap a simple expression with a primitive result type in useMemo
|
|
||||||
impact: LOW-MEDIUM
|
|
||||||
impactDescription: wasted computation on every render
|
|
||||||
tags: rerender, useMemo, optimization
|
|
||||||
---
|
|
||||||
|
|
||||||
## Do not wrap a simple expression with a primitive result type in useMemo
|
|
||||||
|
|
||||||
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 <Skeleton />
|
|
||||||
// return some markup
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct:**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
function Header({ user, notifications }: Props) {
|
|
||||||
const isLoading = user.isLoading || notifications.isLoading
|
|
||||||
|
|
||||||
if (isLoading) return <Skeleton />
|
|
||||||
// return some markup
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
---
|
|
||||||
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)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
---
|
|
||||||
title: Authenticate Server Actions Like API Routes
|
|
||||||
impact: CRITICAL
|
|
||||||
impactDescription: prevents unauthorized access to server mutations
|
|
||||||
tags: server, server-actions, authentication, security, authorization
|
|
||||||
---
|
|
||||||
|
|
||||||
## Authenticate Server Actions Like API Routes
|
|
||||||
|
|
||||||
**Impact: CRITICAL (prevents unauthorized access to server mutations)**
|
|
||||||
|
|
||||||
Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
|
|
||||||
|
|
||||||
Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
|
|
||||||
|
|
||||||
**Incorrect (no authentication check):**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
'use server'
|
|
||||||
|
|
||||||
export async function deleteUser(userId: string) {
|
|
||||||
// Anyone can call this! No auth check
|
|
||||||
await db.user.delete({ where: { id: userId } })
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct (authentication inside the action):**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
'use server'
|
|
||||||
|
|
||||||
import { verifySession } from '@/lib/auth'
|
|
||||||
import { unauthorized } from '@/lib/errors'
|
|
||||||
|
|
||||||
export async function deleteUser(userId: string) {
|
|
||||||
// Always check auth inside the action
|
|
||||||
const session = await verifySession()
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
throw unauthorized('Must be logged in')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check authorization too
|
|
||||||
if (session.user.role !== 'admin' && session.user.id !== userId) {
|
|
||||||
throw unauthorized('Cannot delete other users')
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.user.delete({ where: { id: userId } })
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**With input validation:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
'use server'
|
|
||||||
|
|
||||||
import { verifySession } from '@/lib/auth'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
const updateProfileSchema = z.object({
|
|
||||||
userId: z.string().uuid(),
|
|
||||||
name: z.string().min(1).max(100),
|
|
||||||
email: z.string().email()
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function updateProfile(data: unknown) {
|
|
||||||
// Validate input first
|
|
||||||
const validated = updateProfileSchema.parse(data)
|
|
||||||
|
|
||||||
// Then authenticate
|
|
||||||
const session = await verifySession()
|
|
||||||
if (!session) {
|
|
||||||
throw new Error('Unauthorized')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then authorize
|
|
||||||
if (session.user.id !== validated.userId) {
|
|
||||||
throw new Error('Can only update own profile')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally perform the mutation
|
|
||||||
await db.user.update({
|
|
||||||
where: { id: validated.userId },
|
|
||||||
data: {
|
|
||||||
name: validated.name,
|
|
||||||
email: validated.email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
title: Avoid Duplicate Serialization in RSC Props
|
|
||||||
impact: LOW
|
|
||||||
impactDescription: reduces network payload by avoiding duplicate serialization
|
|
||||||
tags: server, rsc, serialization, props, client-components
|
|
||||||
---
|
|
||||||
|
|
||||||
## Avoid Duplicate Serialization in RSC Props
|
|
||||||
|
|
||||||
**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
|
|
||||||
|
|
||||||
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
|
|
||||||
|
|
||||||
**Incorrect (duplicates array):**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// RSC: sends 6 strings (2 arrays × 3 items)
|
|
||||||
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct (sends 3 strings):**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// RSC: send once
|
|
||||||
<ClientList usernames={usernames} />
|
|
||||||
|
|
||||||
// Client: transform there
|
|
||||||
'use client'
|
|
||||||
const sorted = useMemo(() => [...usernames].sort(), [usernames])
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nested deduplication behavior:**
|
|
||||||
|
|
||||||
Deduplication works recursively. Impact varies by data type:
|
|
||||||
|
|
||||||
- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
|
|
||||||
- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// string[] - duplicates everything
|
|
||||||
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
|
|
||||||
|
|
||||||
// object[] - duplicates array structure only
|
|
||||||
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Operations breaking deduplication (create new references):**
|
|
||||||
|
|
||||||
- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
|
|
||||||
- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
|
|
||||||
|
|
||||||
**More examples:**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// ❌ Bad
|
|
||||||
<C users={users} active={users.filter(u => u.active)} />
|
|
||||||
<C product={product} productName={product.name} />
|
|
||||||
|
|
||||||
// ✅ Good
|
|
||||||
<C users={users} />
|
|
||||||
<C product={product} />
|
|
||||||
// Do filtering/destructuring in client
|
|
||||||
```
|
|
||||||
|
|
||||||
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
name: web-design-guidelines
|
|
||||||
description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
|
|
||||||
metadata:
|
|
||||||
author: vercel
|
|
||||||
version: "1.0.0"
|
|
||||||
argument-hint: <file-or-pattern>
|
|
||||||
---
|
|
||||||
|
|
||||||
# Web Interface Guidelines
|
|
||||||
|
|
||||||
Review files for compliance with Web Interface Guidelines.
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. Fetch the latest guidelines from the source URL below
|
|
||||||
2. Read the specified files (or prompt user for files/pattern)
|
|
||||||
3. Check against all rules in the fetched guidelines
|
|
||||||
4. Output findings in the terse `file:line` format
|
|
||||||
|
|
||||||
## Guidelines Source
|
|
||||||
|
|
||||||
Fetch fresh guidelines before each review:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
When a user provides a file or pattern argument:
|
|
||||||
1. Fetch guidelines from the source URL above
|
|
||||||
2. Read the specified files
|
|
||||||
3. Apply all rules from the fetched guidelines
|
|
||||||
4. Output findings using the format specified in the guidelines
|
|
||||||
|
|
||||||
If no files specified, ask the user which files to review.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/component-refactoring
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/frontend-code-review
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/frontend-testing
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/orpc-contract-first
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/skill-creator
|
|
||||||
@ -4,6 +4,7 @@ Quick validation script for skills - minimal version
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import yaml
|
import yaml
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/vercel-react-best-practices
|
|
||||||
@ -33,43 +33,34 @@ Comprehensive performance optimization guide for React and Next.js applications,
|
|||||||
- 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
|
- 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
|
||||||
- 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
|
- 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
|
||||||
3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
|
3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
|
||||||
- 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
|
- 3.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching)
|
||||||
- 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
|
- 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries)
|
||||||
- 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)
|
- 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition)
|
||||||
- 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries)
|
- 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache)
|
||||||
- 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition)
|
- 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations)
|
||||||
- 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache)
|
|
||||||
- 3.7 [Use after() for Non-Blocking Operations](#37-use-after-for-non-blocking-operations)
|
|
||||||
4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
|
4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
|
||||||
- 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
|
- 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
|
||||||
- 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
|
- 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
|
||||||
- 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 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
|
- 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point)
|
||||||
- 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
|
- 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components)
|
||||||
- 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.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies)
|
||||||
- 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.4 [Subscribe to Derived State](#54-subscribe-to-derived-state)
|
||||||
- 5.5 [Extract to Memoized Components](#55-extract-to-memoized-components)
|
- 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates)
|
||||||
- 5.6 [Narrow Effect Dependencies](#56-narrow-effect-dependencies)
|
- 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization)
|
||||||
- 5.7 [Put Interaction Logic in Event Handlers](#57-put-interaction-logic-in-event-handlers)
|
- 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates)
|
||||||
- 5.8 [Subscribe to Derived State](#58-subscribe-to-derived-state)
|
|
||||||
- 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 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
|
- 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide)
|
||||||
- 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
|
- 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering)
|
||||||
- 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 [Batch DOM CSS Changes](#71-batch-dom-css-changes)
|
||||||
- 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)
|
||||||
- 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
|
- 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
|
||||||
- 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
|
- 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
|
||||||
@ -82,9 +73,8 @@ 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 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
|
- 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs)
|
||||||
- 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)
|
- 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs)
|
||||||
- 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -200,21 +190,6 @@ 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)
|
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
||||||
|
|
||||||
### 1.3 Prevent Waterfall Chains in API Routes
|
### 1.3 Prevent Waterfall Chains in API Routes
|
||||||
@ -593,158 +568,7 @@ The `typeof window !== 'undefined'` check prevents bundling preloaded modules fo
|
|||||||
|
|
||||||
Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
|
Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
|
||||||
|
|
||||||
### 3.1 Authenticate Server Actions Like API Routes
|
### 3.1 Cross-Request LRU Caching
|
||||||
|
|
||||||
**Impact: CRITICAL (prevents unauthorized access to server mutations)**
|
|
||||||
|
|
||||||
Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
|
|
||||||
|
|
||||||
Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
|
|
||||||
|
|
||||||
**Incorrect: no authentication check**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
'use server'
|
|
||||||
|
|
||||||
export async function deleteUser(userId: string) {
|
|
||||||
// Anyone can call this! No auth check
|
|
||||||
await db.user.delete({ where: { id: userId } })
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct: authentication inside the action**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
'use server'
|
|
||||||
|
|
||||||
import { verifySession } from '@/lib/auth'
|
|
||||||
import { unauthorized } from '@/lib/errors'
|
|
||||||
|
|
||||||
export async function deleteUser(userId: string) {
|
|
||||||
// Always check auth inside the action
|
|
||||||
const session = await verifySession()
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
throw unauthorized('Must be logged in')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check authorization too
|
|
||||||
if (session.user.role !== 'admin' && session.user.id !== userId) {
|
|
||||||
throw unauthorized('Cannot delete other users')
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.user.delete({ where: { id: userId } })
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**With input validation:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
'use server'
|
|
||||||
|
|
||||||
import { verifySession } from '@/lib/auth'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
const updateProfileSchema = z.object({
|
|
||||||
userId: z.string().uuid(),
|
|
||||||
name: z.string().min(1).max(100),
|
|
||||||
email: z.string().email()
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function updateProfile(data: unknown) {
|
|
||||||
// Validate input first
|
|
||||||
const validated = updateProfileSchema.parse(data)
|
|
||||||
|
|
||||||
// Then authenticate
|
|
||||||
const session = await verifySession()
|
|
||||||
if (!session) {
|
|
||||||
throw new Error('Unauthorized')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then authorize
|
|
||||||
if (session.user.id !== validated.userId) {
|
|
||||||
throw new Error('Can only update own profile')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally perform the mutation
|
|
||||||
await db.user.update({
|
|
||||||
where: { id: validated.userId },
|
|
||||||
data: {
|
|
||||||
name: validated.name,
|
|
||||||
email: validated.email
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
|
|
||||||
|
|
||||||
### 3.2 Avoid Duplicate Serialization in RSC Props
|
|
||||||
|
|
||||||
**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
|
|
||||||
|
|
||||||
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
|
|
||||||
|
|
||||||
**Incorrect: duplicates array**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// RSC: sends 6 strings (2 arrays × 3 items)
|
|
||||||
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct: sends 3 strings**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// RSC: send once
|
|
||||||
<ClientList usernames={usernames} />
|
|
||||||
|
|
||||||
// Client: transform there
|
|
||||||
'use client'
|
|
||||||
const sorted = useMemo(() => [...usernames].sort(), [usernames])
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nested deduplication behavior:**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// string[] - duplicates everything
|
|
||||||
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
|
|
||||||
|
|
||||||
// object[] - duplicates array structure only
|
|
||||||
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
|
|
||||||
```
|
|
||||||
|
|
||||||
Deduplication works recursively. Impact varies by data type:
|
|
||||||
|
|
||||||
- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
|
|
||||||
|
|
||||||
- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
|
|
||||||
|
|
||||||
**Operations breaking deduplication: create new references**
|
|
||||||
|
|
||||||
- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
|
|
||||||
|
|
||||||
- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
|
|
||||||
|
|
||||||
**More examples:**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// ❌ Bad
|
|
||||||
<C users={users} active={users.filter(u => u.active)} />
|
|
||||||
<C product={product} productName={product.name} />
|
|
||||||
|
|
||||||
// ✅ Good
|
|
||||||
<C users={users} />
|
|
||||||
<C product={product} />
|
|
||||||
// Do filtering/destructuring in client
|
|
||||||
```
|
|
||||||
|
|
||||||
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
|
|
||||||
|
|
||||||
### 3.3 Cross-Request LRU Caching
|
|
||||||
|
|
||||||
**Impact: HIGH (caches across requests)**
|
**Impact: HIGH (caches across requests)**
|
||||||
|
|
||||||
@ -781,7 +605,7 @@ Use when sequential user actions hit multiple endpoints needing the same data wi
|
|||||||
|
|
||||||
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
|
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
|
||||||
|
|
||||||
### 3.4 Minimize Serialization at RSC Boundaries
|
### 3.2 Minimize Serialization at RSC Boundaries
|
||||||
|
|
||||||
**Impact: HIGH (reduces data transfer size)**
|
**Impact: HIGH (reduces data transfer size)**
|
||||||
|
|
||||||
@ -815,7 +639,7 @@ function Profile({ name }: { name: string }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.5 Parallel Data Fetching with Component Composition
|
### 3.3 Parallel Data Fetching with Component Composition
|
||||||
|
|
||||||
**Impact: CRITICAL (eliminates server-side waterfalls)**
|
**Impact: CRITICAL (eliminates server-side waterfalls)**
|
||||||
|
|
||||||
@ -894,7 +718,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.6 Per-Request Deduplication with React.cache()
|
### 3.4 Per-Request Deduplication with React.cache()
|
||||||
|
|
||||||
**Impact: MEDIUM (deduplicates within request)**
|
**Impact: MEDIUM (deduplicates within request)**
|
||||||
|
|
||||||
@ -960,7 +784,7 @@ Use `React.cache()` to deduplicate these operations across your component tree.
|
|||||||
|
|
||||||
Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
|
Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
|
||||||
|
|
||||||
### 3.7 Use after() for Non-Blocking Operations
|
### 3.5 Use after() for Non-Blocking Operations
|
||||||
|
|
||||||
**Impact: MEDIUM (faster response times)**
|
**Impact: MEDIUM (faster response times)**
|
||||||
|
|
||||||
@ -1283,43 +1107,7 @@ 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 Calculate Derived State During Rendering
|
### 5.1 Defer State Reads to Usage Point
|
||||||
|
|
||||||
**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)**
|
||||||
|
|
||||||
@ -1354,71 +1142,7 @@ function ShareButton({ chatId }: { chatId: string }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
|
### 5.2 Extract to Memoized Components
|
||||||
|
|
||||||
**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 <Skeleton />
|
|
||||||
// return some markup
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct:**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
function Header({ user, notifications }: Props) {
|
|
||||||
const isLoading = user.isLoading || notifications.isLoading
|
|
||||||
|
|
||||||
if (isLoading) return <Skeleton />
|
|
||||||
// return some markup
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.4 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
|
|
||||||
<UserAvatar />
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct: stable default value**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const NOOP = () => {};
|
|
||||||
|
|
||||||
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
|
|
||||||
// ...
|
|
||||||
})
|
|
||||||
|
|
||||||
// Used without optional onClick
|
|
||||||
<UserAvatar />
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.5 Extract to Memoized Components
|
|
||||||
|
|
||||||
**Impact: MEDIUM (enables early returns)**
|
**Impact: MEDIUM (enables early returns)**
|
||||||
|
|
||||||
@ -1458,7 +1182,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.6 Narrow Effect Dependencies
|
### 5.3 Narrow Effect Dependencies
|
||||||
|
|
||||||
**Impact: LOW (minimizes effect re-runs)**
|
**Impact: LOW (minimizes effect re-runs)**
|
||||||
|
|
||||||
@ -1499,48 +1223,7 @@ useEffect(() => {
|
|||||||
}, [isMobile])
|
}, [isMobile])
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.7 Put Interaction Logic in Event Handlers
|
### 5.4 Subscribe to Derived State
|
||||||
|
|
||||||
**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)**
|
||||||
|
|
||||||
@ -1565,7 +1248,7 @@ function Sidebar() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5.9 Use Functional setState Updates
|
### 5.5 Use Functional setState Updates
|
||||||
|
|
||||||
**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
|
**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
|
||||||
|
|
||||||
@ -1643,7 +1326,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.10 Use Lazy State Initialization
|
### 5.6 Use Lazy State Initialization
|
||||||
|
|
||||||
**Impact: MEDIUM (wasted computation on every render)**
|
**Impact: MEDIUM (wasted computation on every render)**
|
||||||
|
|
||||||
@ -1697,7 +1380,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.11 Use Transitions for Non-Urgent Updates
|
### 5.7 Use Transitions for Non-Urgent Updates
|
||||||
|
|
||||||
**Impact: MEDIUM (maintains UI responsiveness)**
|
**Impact: MEDIUM (maintains UI responsiveness)**
|
||||||
|
|
||||||
@ -1733,75 +1416,6 @@ 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
|
||||||
@ -2031,33 +1645,7 @@ 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 Suppress Expected Hydration Mismatches
|
### 6.6 Use Activity Component for Show/Hide
|
||||||
|
|
||||||
**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)**
|
||||||
|
|
||||||
@ -2079,7 +1667,7 @@ function Dropdown({ isOpen }: Props) {
|
|||||||
|
|
||||||
Avoids expensive re-renders and state loss.
|
Avoids expensive re-renders and state loss.
|
||||||
|
|
||||||
### 6.8 Use Explicit Conditional Rendering
|
### 6.7 Use Explicit Conditional Rendering
|
||||||
|
|
||||||
**Impact: LOW (prevents rendering 0 or NaN)**
|
**Impact: LOW (prevents rendering 0 or NaN)**
|
||||||
|
|
||||||
@ -2115,80 +1703,6 @@ 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.9 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 (
|
|
||||||
<>
|
|
||||||
<input onChange={(e) => handleSearch(e.target.value)} />
|
|
||||||
{isLoading && <Spinner />}
|
|
||||||
<ResultsList results={results} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**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 (
|
|
||||||
<>
|
|
||||||
<input onChange={(e) => handleSearch(e.target.value)} />
|
|
||||||
{isPending && <Spinner />}
|
|
||||||
<ResultsList results={results} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**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
|
## 7. JavaScript Performance
|
||||||
@ -2197,17 +1711,17 @@ Reference: [https://react.dev/reference/react/useTransition](https://react.dev/r
|
|||||||
|
|
||||||
Micro-optimizations for hot paths can add up to meaningful improvements.
|
Micro-optimizations for hot paths can add up to meaningful improvements.
|
||||||
|
|
||||||
### 7.1 Avoid Layout Thrashing
|
### 7.1 Batch DOM CSS Changes
|
||||||
|
|
||||||
**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
|
**Impact: MEDIUM (reduces reflows/repaints)**
|
||||||
|
|
||||||
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.
|
Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.
|
||||||
|
|
||||||
**This is OK: browser batches style changes**
|
**Incorrect: multiple reflows**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
// Each line invalidates style, but browser batches the recalculation
|
// Each line triggers a reflow
|
||||||
element.style.width = '100px'
|
element.style.width = '100px'
|
||||||
element.style.height = '200px'
|
element.style.height = '200px'
|
||||||
element.style.backgroundColor = 'blue'
|
element.style.backgroundColor = 'blue'
|
||||||
@ -2215,56 +1729,48 @@ function updateElementStyles(element: HTMLElement) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Incorrect: interleaved reads and writes force reflows**
|
**Correct: add class - single reflow**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function layoutThrashing(element: HTMLElement) {
|
// CSS file
|
||||||
element.style.width = '100px'
|
.highlighted-box {
|
||||||
const width = element.offsetWidth // Forces reflow
|
width: 100px;
|
||||||
element.style.height = '200px'
|
height: 200px;
|
||||||
const height = element.offsetHeight // Forces another reflow
|
background-color: blue;
|
||||||
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
**Correct: batch writes, then read once**
|
// JavaScript
|
||||||
|
|
||||||
```typescript
|
|
||||||
function updateElementStyles(element: HTMLElement) {
|
|
||||||
// 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) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
element.classList.add('highlighted-box')
|
element.classList.add('highlighted-box')
|
||||||
|
|
||||||
const { width, height } = element.getBoundingClientRect()
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Better: use CSS classes**
|
**Correct: change cssText - single reflow**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function updateElementStyles(element: HTMLElement) {
|
||||||
|
element.style.cssText = `
|
||||||
|
width: 100px;
|
||||||
|
height: 200px;
|
||||||
|
background-color: blue;
|
||||||
|
border: 1px solid black;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**React example:**
|
**React example:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// Incorrect: interleaving style changes with layout queries
|
// Incorrect: changing styles one by one
|
||||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current && isHighlighted) {
|
if (ref.current && isHighlighted) {
|
||||||
ref.current.style.width = '100px'
|
ref.current.style.width = '100px'
|
||||||
const width = ref.current.offsetWidth // Forces layout
|
|
||||||
ref.current.style.height = '200px'
|
ref.current.style.height = '200px'
|
||||||
|
ref.current.style.backgroundColor = 'blue'
|
||||||
}
|
}
|
||||||
}, [isHighlighted])
|
}, [isHighlighted])
|
||||||
|
|
||||||
@ -2281,9 +1787,7 @@ function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
|
Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.
|
||||||
|
|
||||||
See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
|
|
||||||
|
|
||||||
### 7.2 Build Index Maps for Repeated Lookups
|
### 7.2 Build Index Maps for Repeated Lookups
|
||||||
|
|
||||||
@ -2540,7 +2044,7 @@ function hasChanges(current: string[], original: string[]) {
|
|||||||
if (current.length !== original.length) {
|
if (current.length !== original.length) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Only sort when lengths match
|
// Only sort/join when lengths match
|
||||||
const currentSorted = current.toSorted()
|
const currentSorted = current.toSorted()
|
||||||
const originalSorted = original.toSorted()
|
const originalSorted = original.toSorted()
|
||||||
for (let i = 0; i < currentSorted.length; i++) {
|
for (let i = 0; i < currentSorted.length; i++) {
|
||||||
@ -2725,7 +2229,7 @@ const min = Math.min(...numbers)
|
|||||||
const max = Math.max(...numbers)
|
const max = Math.max(...numbers)
|
||||||
```
|
```
|
||||||
|
|
||||||
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
|
This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability.
|
||||||
|
|
||||||
### 7.11 Use Set/Map for O(1) Lookups
|
### 7.11 Use Set/Map for O(1) Lookups
|
||||||
|
|
||||||
@ -2812,45 +2316,7 @@ 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 Initialize App Once, Not Per Mount
|
### 8.1 Store Event Handlers in Refs
|
||||||
|
|
||||||
**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)**
|
||||||
|
|
||||||
@ -2859,7 +2325,7 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
|
|||||||
**Incorrect: re-subscribes on every render**
|
**Incorrect: re-subscribes on every render**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: () => void) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(event, handler)
|
window.addEventListener(event, handler)
|
||||||
return () => window.removeEventListener(event, handler)
|
return () => window.removeEventListener(event, handler)
|
||||||
@ -2872,7 +2338,7 @@ function useWindowEvent(event: string, handler: (e) => void) {
|
|||||||
```tsx
|
```tsx
|
||||||
import { useEffectEvent } from 'react'
|
import { useEffectEvent } from 'react'
|
||||||
|
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: () => void) {
|
||||||
const onEvent = useEffectEvent(handler)
|
const onEvent = useEffectEvent(handler)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -2886,12 +2352,24 @@ 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.3 useEffectEvent for Stable Callback Refs
|
### 8.2 useLatest for Stable Callback Refs
|
||||||
|
|
||||||
**Impact: LOW (prevents effect re-runs)**
|
**Impact: LOW (prevents effect re-runs)**
|
||||||
|
|
||||||
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function useLatest<T>(value: T) {
|
||||||
|
const ref = useRef(value)
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value
|
||||||
|
}, [value])
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Incorrect: effect re-runs on every callback change**
|
**Incorrect: effect re-runs on every callback change**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@ -2905,17 +2383,15 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct: using React's useEffectEvent**
|
**Correct: stable effect, fresh callback**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useEffectEvent } from 'react';
|
|
||||||
|
|
||||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const onSearchEvent = useEffectEvent(onSearch)
|
const onSearchRef = useLatest(onSearch)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => onSearchEvent(query), 300)
|
const timeout = setTimeout(() => onSearchRef.current(query), 300)
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}, [query])
|
}, [query])
|
||||||
}
|
}
|
||||||
@ -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 57 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 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
|
||||||
|
|
||||||
## When to Apply
|
## When to Apply
|
||||||
|
|
||||||
@ -53,10 +53,8 @@ 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
|
||||||
@ -65,23 +63,16 @@ 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)
|
||||||
|
|
||||||
@ -90,10 +81,8 @@ 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)
|
||||||
|
|
||||||
@ -113,7 +102,6 @@ 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
|
||||||
@ -123,6 +111,7 @@ 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:
|
||||||
@ -1,14 +1,26 @@
|
|||||||
---
|
---
|
||||||
title: useEffectEvent for Stable Callback Refs
|
title: useLatest for Stable Callback Refs
|
||||||
impact: LOW
|
impact: LOW
|
||||||
impactDescription: prevents effect re-runs
|
impactDescription: prevents effect re-runs
|
||||||
tags: advanced, hooks, useEffectEvent, refs, optimization
|
tags: advanced, hooks, useLatest, refs, optimization
|
||||||
---
|
---
|
||||||
|
|
||||||
## useEffectEvent for Stable Callback Refs
|
## useLatest for Stable Callback Refs
|
||||||
|
|
||||||
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function useLatest<T>(value: T) {
|
||||||
|
const ref = useRef(value)
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
ref.current = value
|
||||||
|
}, [value])
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Incorrect (effect re-runs on every callback change):**
|
**Incorrect (effect re-runs on every callback change):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@ -22,17 +34,15 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (using React's useEffectEvent):**
|
**Correct (stable effect, fresh callback):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useEffectEvent } from 'react';
|
|
||||||
|
|
||||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const onSearchEvent = useEffectEvent(onSearch)
|
const onSearchRef = useLatest(onSearch)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => onSearchEvent(query), 300)
|
const timeout = setTimeout(() => onSearchRef.current(query), 300)
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}, [query])
|
}, [query])
|
||||||
}
|
}
|
||||||
@ -33,19 +33,4 @@ const { user, config, profile } = await all({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
**Alternative without extra dependencies:**
|
|
||||||
|
|
||||||
We can also create all the promises first, and do `Promise.all()` at the end.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const userPromise = fetchUser()
|
|
||||||
const profilePromise = userPromise.then(user => fetchProfile(user.id))
|
|
||||||
|
|
||||||
const [user, config, profile] = await Promise.all([
|
|
||||||
userPromise,
|
|
||||||
fetchConfig(),
|
|
||||||
profilePromise
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Batch DOM CSS Changes
|
||||||
|
impact: MEDIUM
|
||||||
|
impactDescription: reduces reflows/repaints
|
||||||
|
tags: javascript, dom, css, performance, reflow
|
||||||
|
---
|
||||||
|
|
||||||
|
## Batch DOM CSS Changes
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Incorrect (interleaved reads and writes force reflows):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function updateElementStyles(element: HTMLElement) {
|
||||||
|
element.style.width = '100px'
|
||||||
|
const width = element.offsetWidth // Forces reflow
|
||||||
|
element.style.height = '200px'
|
||||||
|
const height = element.offsetHeight // Forces another reflow
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct (batch writes, then read once):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function updateElementStyles(element: HTMLElement) {
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Better: use CSS classes**
|
||||||
|
|
||||||
|
```css
|
||||||
|
.highlighted-box {
|
||||||
|
width: 100px;
|
||||||
|
height: 200px;
|
||||||
|
background-color: blue;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function updateElementStyles(element: HTMLElement) {
|
||||||
|
element.classList.add('highlighted-box')
|
||||||
|
|
||||||
|
const { width, height } = element.getBoundingClientRect()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user