format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ description: React and Next.js performance optimization guidelines from Vercel E
|
||||
license: MIT
|
||||
metadata:
|
||||
author: vercel
|
||||
version: "1.0.0"
|
||||
version: '1.0.0'
|
||||
---
|
||||
|
||||
# Vercel React Best Practices
|
||||
@@ -14,6 +14,7 @@ Comprehensive performance optimization guide for React and Next.js applications,
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
|
||||
- Writing new React components or Next.js pages
|
||||
- Implementing data fetching (client or server-side)
|
||||
- Reviewing code for performance issues
|
||||
@@ -22,16 +23,16 @@ Reference these guidelines when:
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||
| Priority | Category | Impact | Prefix |
|
||||
| -------- | ------------------------- | ----------- | ------------ |
|
||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@@ -126,6 +127,7 @@ rules/bundle-barrel-imports.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
|
||||
+15
-15
@@ -14,9 +14,9 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, handler)
|
||||
return () => window.removeEventListener(event, handler)
|
||||
}, [event, handler])
|
||||
window.addEventListener(event, handler);
|
||||
return () => window.removeEventListener(event, handler);
|
||||
}, [event, handler]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -24,31 +24,31 @@ function useWindowEvent(event: string, handler: (e) => void) {
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
const handlerRef = useRef(handler)
|
||||
const handlerRef = useRef(handler);
|
||||
useEffect(() => {
|
||||
handlerRef.current = handler
|
||||
}, [handler])
|
||||
handlerRef.current = handler;
|
||||
}, [handler]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e) => handlerRef.current(e)
|
||||
window.addEventListener(event, listener)
|
||||
return () => window.removeEventListener(event, listener)
|
||||
}, [event])
|
||||
const listener = (e) => handlerRef.current(e);
|
||||
window.addEventListener(event, listener);
|
||||
return () => window.removeEventListener(event, listener);
|
||||
}, [event]);
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative: use `useEffectEvent` if you're on latest React:**
|
||||
|
||||
```tsx
|
||||
import { useEffectEvent } from 'react'
|
||||
import { useEffectEvent } from 'react';
|
||||
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
const onEvent = useEffectEvent(handler)
|
||||
const onEvent = useEffectEvent(handler);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, onEvent)
|
||||
return () => window.removeEventListener(event, onEvent)
|
||||
}, [event])
|
||||
window.addEventListener(event, onEvent);
|
||||
return () => window.removeEventListener(event, onEvent);
|
||||
}, [event]);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+9
-9
@@ -14,9 +14,9 @@ Do not put app-wide initialization that must run once per app load inside `useEf
|
||||
```tsx
|
||||
function Comp() {
|
||||
useEffect(() => {
|
||||
loadFromStorage()
|
||||
checkAuthToken()
|
||||
}, [])
|
||||
loadFromStorage();
|
||||
checkAuthToken();
|
||||
}, []);
|
||||
|
||||
// ...
|
||||
}
|
||||
@@ -25,15 +25,15 @@ function Comp() {
|
||||
**Correct (once per app load):**
|
||||
|
||||
```tsx
|
||||
let didInit = false
|
||||
let didInit = false;
|
||||
|
||||
function Comp() {
|
||||
useEffect(() => {
|
||||
if (didInit) return
|
||||
didInit = true
|
||||
loadFromStorage()
|
||||
checkAuthToken()
|
||||
}, [])
|
||||
if (didInit) return;
|
||||
didInit = true;
|
||||
loadFromStorage();
|
||||
checkAuthToken();
|
||||
}, []);
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
+9
-9
@@ -13,12 +13,12 @@ Access latest values in callbacks without adding them to dependency arrays. Prev
|
||||
|
||||
```tsx
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearch(query), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query, onSearch])
|
||||
const timeout = setTimeout(() => onSearch(query), 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [query, onSearch]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,12 +28,12 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
import { useEffectEvent } from 'react';
|
||||
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const onSearchEvent = useEffectEvent(onSearch)
|
||||
const [query, setQuery] = useState('');
|
||||
const onSearchEvent = useEffectEvent(onSearch);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearchEvent(query), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query])
|
||||
const timeout = setTimeout(() => onSearchEvent(query), 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [query]);
|
||||
}
|
||||
```
|
||||
|
||||
+9
-12
@@ -13,10 +13,10 @@ In API routes and Server Actions, start independent operations immediately, even
|
||||
|
||||
```typescript
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth()
|
||||
const config = await fetchConfig()
|
||||
const data = await fetchData(session.user.id)
|
||||
return Response.json({ data, config })
|
||||
const session = await auth();
|
||||
const config = await fetchConfig();
|
||||
const data = await fetchData(session.user.id);
|
||||
return Response.json({ data, config });
|
||||
}
|
||||
```
|
||||
|
||||
@@ -24,14 +24,11 @@ export async function GET(request: Request) {
|
||||
|
||||
```typescript
|
||||
export async function GET(request: Request) {
|
||||
const sessionPromise = auth()
|
||||
const configPromise = fetchConfig()
|
||||
const session = await sessionPromise
|
||||
const [config, data] = await Promise.all([
|
||||
configPromise,
|
||||
fetchData(session.user.id)
|
||||
])
|
||||
return Response.json({ data, config })
|
||||
const sessionPromise = auth();
|
||||
const configPromise = fetchConfig();
|
||||
const session = await sessionPromise;
|
||||
const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);
|
||||
return Response.json({ data, config });
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+26
-26
@@ -13,15 +13,15 @@ Move `await` operations into the branches where they're actually used to avoid b
|
||||
|
||||
```typescript
|
||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
const userData = await fetchUserData(userId)
|
||||
|
||||
const userData = await fetchUserData(userId);
|
||||
|
||||
if (skipProcessing) {
|
||||
// Returns immediately but still waited for userData
|
||||
return { skipped: true }
|
||||
return { skipped: true };
|
||||
}
|
||||
|
||||
|
||||
// Only this branch uses userData
|
||||
return processUserData(userData)
|
||||
return processUserData(userData);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,12 +31,12 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
if (skipProcessing) {
|
||||
// Returns immediately without waiting
|
||||
return { skipped: true }
|
||||
return { skipped: true };
|
||||
}
|
||||
|
||||
|
||||
// Fetch only when needed
|
||||
const userData = await fetchUserData(userId)
|
||||
return processUserData(userData)
|
||||
const userData = await fetchUserData(userId);
|
||||
return processUserData(userData);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -45,35 +45,35 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
```typescript
|
||||
// Incorrect: always fetches permissions
|
||||
async function updateResource(resourceId: string, userId: string) {
|
||||
const permissions = await fetchPermissions(userId)
|
||||
const resource = await getResource(resourceId)
|
||||
|
||||
const permissions = await fetchPermissions(userId);
|
||||
const resource = await getResource(resourceId);
|
||||
|
||||
if (!resource) {
|
||||
return { error: 'Not found' }
|
||||
return { error: 'Not found' };
|
||||
}
|
||||
|
||||
|
||||
if (!permissions.canEdit) {
|
||||
return { error: 'Forbidden' }
|
||||
return { error: 'Forbidden' };
|
||||
}
|
||||
|
||||
return await updateResourceData(resource, permissions)
|
||||
|
||||
return await updateResourceData(resource, permissions);
|
||||
}
|
||||
|
||||
// Correct: fetches only when needed
|
||||
async function updateResource(resourceId: string, userId: string) {
|
||||
const resource = await getResource(resourceId)
|
||||
|
||||
const resource = await getResource(resourceId);
|
||||
|
||||
if (!resource) {
|
||||
return { error: 'Not found' }
|
||||
return { error: 'Not found' };
|
||||
}
|
||||
|
||||
const permissions = await fetchPermissions(userId)
|
||||
|
||||
|
||||
const permissions = await fetchPermissions(userId);
|
||||
|
||||
if (!permissions.canEdit) {
|
||||
return { error: 'Forbidden' }
|
||||
return { error: 'Forbidden' };
|
||||
}
|
||||
|
||||
return await updateResourceData(resource, permissions)
|
||||
|
||||
return await updateResourceData(resource, permissions);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+15
-18
@@ -12,25 +12,26 @@ For operations with partial dependencies, use `better-all` to maximize paralleli
|
||||
**Incorrect (profile waits for config unnecessarily):**
|
||||
|
||||
```typescript
|
||||
const [user, config] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchConfig()
|
||||
])
|
||||
const profile = await fetchProfile(user.id)
|
||||
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
|
||||
const profile = await fetchProfile(user.id);
|
||||
```
|
||||
|
||||
**Correct (config and profile run in parallel):**
|
||||
|
||||
```typescript
|
||||
import { all } from 'better-all'
|
||||
import { all } from 'better-all';
|
||||
|
||||
const { user, config, profile } = await all({
|
||||
async user() { return fetchUser() },
|
||||
async config() { return fetchConfig() },
|
||||
async user() {
|
||||
return fetchUser();
|
||||
},
|
||||
async config() {
|
||||
return fetchConfig();
|
||||
},
|
||||
async profile() {
|
||||
return fetchProfile((await this.$.user).id)
|
||||
}
|
||||
})
|
||||
return fetchProfile((await this.$.user).id);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Alternative without extra dependencies:**
|
||||
@@ -38,14 +39,10 @@ const { user, config, profile } = await all({
|
||||
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 userPromise = fetchUser();
|
||||
const profilePromise = userPromise.then((user) => fetchProfile(user.id));
|
||||
|
||||
const [user, config, profile] = await Promise.all([
|
||||
userPromise,
|
||||
fetchConfig(),
|
||||
profilePromise
|
||||
])
|
||||
const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise]);
|
||||
```
|
||||
|
||||
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
||||
|
||||
+4
-8
@@ -12,17 +12,13 @@ When async operations have no interdependencies, execute them concurrently using
|
||||
**Incorrect (sequential execution, 3 round trips):**
|
||||
|
||||
```typescript
|
||||
const user = await fetchUser()
|
||||
const posts = await fetchPosts()
|
||||
const comments = await fetchComments()
|
||||
const user = await fetchUser();
|
||||
const posts = await fetchPosts();
|
||||
const comments = await fetchComments();
|
||||
```
|
||||
|
||||
**Correct (parallel execution, 1 round trip):**
|
||||
|
||||
```typescript
|
||||
const [user, posts, comments] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchPosts(),
|
||||
fetchComments()
|
||||
])
|
||||
const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);
|
||||
```
|
||||
|
||||
+13
-13
@@ -13,8 +13,8 @@ Instead of awaiting data in async components before returning JSX, use Suspense
|
||||
|
||||
```tsx
|
||||
async function Page() {
|
||||
const data = await fetchData() // Blocks entire page
|
||||
|
||||
const data = await fetchData(); // Blocks entire page
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
@@ -24,7 +24,7 @@ async function Page() {
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -45,12 +45,12 @@ function Page() {
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function DataDisplay() {
|
||||
const data = await fetchData() // Only blocks this component
|
||||
return <div>{data.content}</div>
|
||||
const data = await fetchData(); // Only blocks this component
|
||||
return <div>{data.content}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -61,8 +61,8 @@ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
|
||||
```tsx
|
||||
function Page() {
|
||||
// Start fetch immediately, but don't await
|
||||
const dataPromise = fetchData()
|
||||
|
||||
const dataPromise = fetchData();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
@@ -73,17 +73,17 @@ function Page() {
|
||||
</Suspense>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise) // Unwraps the promise
|
||||
return <div>{data.content}</div>
|
||||
const data = use(dataPromise); // Unwraps the promise
|
||||
return <div>{data.content}</div>;
|
||||
}
|
||||
|
||||
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise) // Reuses the same promise
|
||||
return <div>{data.summary}</div>
|
||||
const data = use(dataPromise); // Reuses the same promise
|
||||
return <div>{data.summary}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+11
-11
@@ -16,24 +16,24 @@ Popular icon and component libraries can have **up to 10,000 re-exports** in the
|
||||
**Incorrect (imports entire library):**
|
||||
|
||||
```tsx
|
||||
import { Check, X, Menu } from 'lucide-react'
|
||||
import { Check, X, Menu } from 'lucide-react';
|
||||
// Loads 1,583 modules, takes ~2.8s extra in dev
|
||||
// Runtime cost: 200-800ms on every cold start
|
||||
|
||||
import { Button, TextField } from '@mui/material'
|
||||
import { Button, TextField } from '@mui/material';
|
||||
// Loads 2,225 modules, takes ~4.2s extra in dev
|
||||
```
|
||||
|
||||
**Correct (imports only what you need):**
|
||||
|
||||
```tsx
|
||||
import Check from 'lucide-react/dist/esm/icons/check'
|
||||
import X from 'lucide-react/dist/esm/icons/x'
|
||||
import Menu from 'lucide-react/dist/esm/icons/menu'
|
||||
import Check from 'lucide-react/dist/esm/icons/check';
|
||||
import X from 'lucide-react/dist/esm/icons/x';
|
||||
import Menu from 'lucide-react/dist/esm/icons/menu';
|
||||
// Loads only 3 modules (~2KB vs ~1MB)
|
||||
|
||||
import Button from '@mui/material/Button'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
// Loads only what you use
|
||||
```
|
||||
|
||||
@@ -43,12 +43,12 @@ import TextField from '@mui/material/TextField'
|
||||
// next.config.js - use optimizePackageImports
|
||||
module.exports = {
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react', '@mui/material']
|
||||
}
|
||||
}
|
||||
optimizePackageImports: ['lucide-react', '@mui/material'],
|
||||
},
|
||||
};
|
||||
|
||||
// Then you can keep the ergonomic barrel imports:
|
||||
import { Check, X, Menu } from 'lucide-react'
|
||||
import { Check, X, Menu } from 'lucide-react';
|
||||
// Automatically transformed to direct imports at build time
|
||||
```
|
||||
|
||||
|
||||
+13
-7
@@ -12,19 +12,25 @@ Load large data or modules only when a feature is activated.
|
||||
**Example (lazy-load animation frames):**
|
||||
|
||||
```tsx
|
||||
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
|
||||
const [frames, setFrames] = useState<Frame[] | null>(null)
|
||||
function AnimationPlayer({
|
||||
enabled,
|
||||
setEnabled,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const [frames, setFrames] = useState<Frame[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && !frames && typeof window !== 'undefined') {
|
||||
import('./animation-frames.js')
|
||||
.then(mod => setFrames(mod.frames))
|
||||
.catch(() => setEnabled(false))
|
||||
.then((mod) => setFrames(mod.frames))
|
||||
.catch(() => setEnabled(false));
|
||||
}
|
||||
}, [enabled, frames, setEnabled])
|
||||
}, [enabled, frames, setEnabled]);
|
||||
|
||||
if (!frames) return <Skeleton />
|
||||
return <Canvas frames={frames} />
|
||||
if (!frames) return <Skeleton />;
|
||||
return <Canvas frames={frames} />;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+7
-8
@@ -12,7 +12,7 @@ Analytics, logging, and error tracking don't block user interaction. Load them a
|
||||
**Incorrect (blocks initial bundle):**
|
||||
|
||||
```tsx
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
@@ -22,19 +22,18 @@ export default function RootLayout({ children }) {
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (loads after hydration):**
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic'
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Analytics = dynamic(
|
||||
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
||||
{ ssr: false }
|
||||
)
|
||||
const Analytics = dynamic(() => import('@vercel/analytics/react').then((m) => m.Analytics), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
@@ -44,6 +43,6 @@ export default function RootLayout({ children }) {
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
+7
-8
@@ -12,24 +12,23 @@ Use `next/dynamic` to lazy-load large components not needed on initial render.
|
||||
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
||||
|
||||
```tsx
|
||||
import { MonacoEditor } from './monaco-editor'
|
||||
import { MonacoEditor } from './monaco-editor';
|
||||
|
||||
function CodePanel({ code }: { code: string }) {
|
||||
return <MonacoEditor value={code} />
|
||||
return <MonacoEditor value={code} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (Monaco loads on demand):**
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic'
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const MonacoEditor = dynamic(
|
||||
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
||||
{ ssr: false }
|
||||
)
|
||||
const MonacoEditor = dynamic(() => import('./monaco-editor').then((m) => m.MonacoEditor), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
function CodePanel({ code }: { code: string }) {
|
||||
return <MonacoEditor value={code} />
|
||||
return <MonacoEditor value={code} />;
|
||||
}
|
||||
```
|
||||
|
||||
+7
-13
@@ -15,19 +15,15 @@ Preload heavy bundles before they're needed to reduce perceived latency.
|
||||
function EditorButton({ onClick }: { onClick: () => void }) {
|
||||
const preload = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
void import('./monaco-editor')
|
||||
void import('./monaco-editor');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={preload}
|
||||
onFocus={preload}
|
||||
onClick={onClick}
|
||||
>
|
||||
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
|
||||
Open Editor
|
||||
</button>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -37,13 +33,11 @@ function EditorButton({ onClick }: { onClick: () => void }) {
|
||||
function FlagsProvider({ children, flags }: Props) {
|
||||
useEffect(() => {
|
||||
if (flags.editorEnabled && typeof window !== 'undefined') {
|
||||
void import('./monaco-editor').then(mod => mod.init())
|
||||
void import('./monaco-editor').then((mod) => mod.init());
|
||||
}
|
||||
}, [flags.editorEnabled])
|
||||
}, [flags.editorEnabled]);
|
||||
|
||||
return <FlagsContext.Provider value={flags}>
|
||||
{children}
|
||||
</FlagsContext.Provider>
|
||||
return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+25
-21
@@ -16,12 +16,12 @@ function useKeyboardShortcut(key: string, callback: () => void) {
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && e.key === key) {
|
||||
callback()
|
||||
callback();
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [key, callback])
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [key, callback]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,45 +30,49 @@ When using the `useKeyboardShortcut` hook multiple times, each instance will reg
|
||||
**Correct (N instances = 1 listener):**
|
||||
|
||||
```tsx
|
||||
import useSWRSubscription from 'swr/subscription'
|
||||
import useSWRSubscription from 'swr/subscription';
|
||||
|
||||
// Module-level Map to track callbacks per key
|
||||
const keyCallbacks = new Map<string, Set<() => void>>()
|
||||
const keyCallbacks = new Map<string, Set<() => void>>();
|
||||
|
||||
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||
// Register this callback in the Map
|
||||
useEffect(() => {
|
||||
if (!keyCallbacks.has(key)) {
|
||||
keyCallbacks.set(key, new Set())
|
||||
keyCallbacks.set(key, new Set());
|
||||
}
|
||||
keyCallbacks.get(key)!.add(callback)
|
||||
keyCallbacks.get(key)!.add(callback);
|
||||
|
||||
return () => {
|
||||
const set = keyCallbacks.get(key)
|
||||
const set = keyCallbacks.get(key);
|
||||
if (set) {
|
||||
set.delete(callback)
|
||||
set.delete(callback);
|
||||
if (set.size === 0) {
|
||||
keyCallbacks.delete(key)
|
||||
keyCallbacks.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [key, callback])
|
||||
};
|
||||
}, [key, callback]);
|
||||
|
||||
useSWRSubscription('global-keydown', () => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && keyCallbacks.has(e.key)) {
|
||||
keyCallbacks.get(e.key)!.forEach(cb => cb())
|
||||
keyCallbacks.get(e.key)!.forEach((cb) => cb());
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
})
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
});
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
// Multiple shortcuts will share the same listener
|
||||
useKeyboardShortcut('p', () => { /* ... */ })
|
||||
useKeyboardShortcut('k', () => { /* ... */ })
|
||||
useKeyboardShortcut('p', () => {
|
||||
/* ... */
|
||||
});
|
||||
useKeyboardShortcut('k', () => {
|
||||
/* ... */
|
||||
});
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
+18
-15
@@ -13,18 +13,18 @@ Add version prefix to keys and store only needed fields. Prevents schema conflic
|
||||
|
||||
```typescript
|
||||
// No version, stores everything, no error handling
|
||||
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
|
||||
const data = localStorage.getItem('userConfig')
|
||||
localStorage.setItem('userConfig', JSON.stringify(fullUserObject));
|
||||
const data = localStorage.getItem('userConfig');
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
const VERSION = 'v2'
|
||||
const VERSION = 'v2';
|
||||
|
||||
function saveConfig(config: { theme: string; language: string }) {
|
||||
try {
|
||||
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
|
||||
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
|
||||
} catch {
|
||||
// Throws in incognito/private browsing, quota exceeded, or disabled
|
||||
}
|
||||
@@ -32,21 +32,21 @@ function saveConfig(config: { theme: string; language: string }) {
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const data = localStorage.getItem(`userConfig:${VERSION}`)
|
||||
return data ? JSON.parse(data) : null
|
||||
const data = localStorage.getItem(`userConfig:${VERSION}`);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Migration from v1 to v2
|
||||
function migrate() {
|
||||
try {
|
||||
const v1 = localStorage.getItem('userConfig:v1')
|
||||
const v1 = localStorage.getItem('userConfig:v1');
|
||||
if (v1) {
|
||||
const old = JSON.parse(v1)
|
||||
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
|
||||
localStorage.removeItem('userConfig:v1')
|
||||
const old = JSON.parse(v1);
|
||||
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang });
|
||||
localStorage.removeItem('userConfig:v1');
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
@@ -58,10 +58,13 @@ function migrate() {
|
||||
// User object has 20+ fields, only store what UI needs
|
||||
function cachePrefs(user: FullUser) {
|
||||
try {
|
||||
localStorage.setItem('prefs:v1', JSON.stringify({
|
||||
theme: user.preferences.theme,
|
||||
notifications: user.preferences.notifications
|
||||
}))
|
||||
localStorage.setItem(
|
||||
'prefs:v1',
|
||||
JSON.stringify({
|
||||
theme: user.preferences.theme,
|
||||
notifications: user.preferences.notifications,
|
||||
}),
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
```
|
||||
|
||||
+20
-20
@@ -13,34 +13,34 @@ Add `{ passive: true }` to touch and wheel event listeners to enable immediate s
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
||||
|
||||
document.addEventListener('touchstart', handleTouch)
|
||||
document.addEventListener('wheel', handleWheel)
|
||||
|
||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
|
||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
|
||||
|
||||
document.addEventListener('touchstart', handleTouch);
|
||||
document.addEventListener('wheel', handleWheel);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('touchstart', handleTouch)
|
||||
document.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}, [])
|
||||
document.removeEventListener('touchstart', handleTouch);
|
||||
document.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
||||
|
||||
document.addEventListener('touchstart', handleTouch, { passive: true })
|
||||
document.addEventListener('wheel', handleWheel, { passive: true })
|
||||
|
||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
|
||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
|
||||
|
||||
document.addEventListener('touchstart', handleTouch, { passive: true });
|
||||
document.addEventListener('wheel', handleWheel, { passive: true });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('touchstart', handleTouch)
|
||||
document.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}, [])
|
||||
document.removeEventListener('touchstart', handleTouch);
|
||||
document.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
|
||||
|
||||
+11
-11
@@ -13,43 +13,43 @@ SWR enables request deduplication, caching, and revalidation across component in
|
||||
|
||||
```tsx
|
||||
function UserList() {
|
||||
const [users, setUsers] = useState([])
|
||||
const [users, setUsers] = useState([]);
|
||||
useEffect(() => {
|
||||
fetch('/api/users')
|
||||
.then(r => r.json())
|
||||
.then(setUsers)
|
||||
}, [])
|
||||
.then((r) => r.json())
|
||||
.then(setUsers);
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (multiple instances share one request):**
|
||||
|
||||
```tsx
|
||||
import useSWR from 'swr'
|
||||
import useSWR from 'swr';
|
||||
|
||||
function UserList() {
|
||||
const { data: users } = useSWR('/api/users', fetcher)
|
||||
const { data: users } = useSWR('/api/users', fetcher);
|
||||
}
|
||||
```
|
||||
|
||||
**For immutable data:**
|
||||
|
||||
```tsx
|
||||
import { useImmutableSWR } from '@/lib/swr'
|
||||
import { useImmutableSWR } from '@/lib/swr';
|
||||
|
||||
function StaticContent() {
|
||||
const { data } = useImmutableSWR('/api/config', fetcher)
|
||||
const { data } = useImmutableSWR('/api/config', fetcher);
|
||||
}
|
||||
```
|
||||
|
||||
**For mutations:**
|
||||
|
||||
```tsx
|
||||
import { useSWRMutation } from 'swr/mutation'
|
||||
import { useSWRMutation } from 'swr/mutation';
|
||||
|
||||
function UpdateButton() {
|
||||
const { trigger } = useSWRMutation('/api/user', updateUser)
|
||||
return <button onClick={() => trigger()}>Update</button>
|
||||
const { trigger } = useSWRMutation('/api/user', updateUser);
|
||||
return <button onClick={() => trigger()}>Update</button>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+39
-36
@@ -10,55 +10,60 @@ tags: javascript, dom, css, performance, reflow, 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'
|
||||
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
|
||||
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'
|
||||
|
||||
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()
|
||||
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
|
||||
|
||||
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'
|
||||
element.style.width = '100px';
|
||||
element.style.height = '200px';
|
||||
}
|
||||
```
|
||||
|
||||
**Better: use CSS classes**
|
||||
|
||||
```css
|
||||
.highlighted-box {
|
||||
width: 100px;
|
||||
@@ -67,38 +72,36 @@ function avoidThrashing(element: HTMLElement) {
|
||||
border: 1px solid black;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.classList.add('highlighted-box')
|
||||
|
||||
const { width, height } = element.getBoundingClientRect()
|
||||
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)
|
||||
|
||||
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'
|
||||
ref.current.style.width = '100px';
|
||||
const width = ref.current.offsetWidth; // Forces layout
|
||||
ref.current.style.height = '200px';
|
||||
}
|
||||
}, [isHighlighted])
|
||||
|
||||
return <div ref={ref}>Content</div>
|
||||
}, [isHighlighted]);
|
||||
|
||||
return <div ref={ref}>Content</div>;
|
||||
}
|
||||
|
||||
// Correct: toggle class
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
return (
|
||||
<div className={isHighlighted ? 'highlighted-box' : ''}>
|
||||
Content
|
||||
</div>
|
||||
)
|
||||
return <div className={isHighlighted ? 'highlighted-box' : ''}>Content</div>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+8
-8
@@ -18,7 +18,7 @@ function ProjectList({ projects }: { projects: Project[] }) {
|
||||
{projects.map(project => {
|
||||
// slugify() called 100+ times for same project names
|
||||
const slug = slugify(project.name)
|
||||
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@ function ProjectList({ projects }: { projects: Project[] }) {
|
||||
{projects.map(project => {
|
||||
// Computed only once per unique project name
|
||||
const slug = cachedSlugify(project.name)
|
||||
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
@@ -58,20 +58,20 @@ function ProjectList({ projects }: { projects: Project[] }) {
|
||||
**Simpler pattern for single-value functions:**
|
||||
|
||||
```typescript
|
||||
let isLoggedInCache: boolean | null = null
|
||||
let isLoggedInCache: boolean | null = null;
|
||||
|
||||
function isLoggedIn(): boolean {
|
||||
if (isLoggedInCache !== null) {
|
||||
return isLoggedInCache
|
||||
return isLoggedInCache;
|
||||
}
|
||||
|
||||
isLoggedInCache = document.cookie.includes('auth=')
|
||||
return isLoggedInCache
|
||||
|
||||
isLoggedInCache = document.cookie.includes('auth=');
|
||||
return isLoggedInCache;
|
||||
}
|
||||
|
||||
// Clear cache when auth changes
|
||||
function onAuthChange() {
|
||||
isLoggedInCache = null
|
||||
isLoggedInCache = null;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+4
-4
@@ -13,16 +13,16 @@ Cache object property lookups in hot paths.
|
||||
|
||||
```typescript
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
process(obj.config.settings.value)
|
||||
process(obj.config.settings.value);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (1 lookup total):**
|
||||
|
||||
```typescript
|
||||
const value = obj.config.settings.value
|
||||
const len = arr.length
|
||||
const value = obj.config.settings.value;
|
||||
const len = arr.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
process(value)
|
||||
process(value);
|
||||
}
|
||||
```
|
||||
|
||||
+13
-15
@@ -13,7 +13,7 @@ tags: javascript, localStorage, storage, caching, performance
|
||||
|
||||
```typescript
|
||||
function getTheme() {
|
||||
return localStorage.getItem('theme') ?? 'light'
|
||||
return localStorage.getItem('theme') ?? 'light';
|
||||
}
|
||||
// Called 10 times = 10 storage reads
|
||||
```
|
||||
@@ -21,18 +21,18 @@ function getTheme() {
|
||||
**Correct (Map cache):**
|
||||
|
||||
```typescript
|
||||
const storageCache = new Map<string, string | null>()
|
||||
const storageCache = new Map<string, string | null>();
|
||||
|
||||
function getLocalStorage(key: string) {
|
||||
if (!storageCache.has(key)) {
|
||||
storageCache.set(key, localStorage.getItem(key))
|
||||
storageCache.set(key, localStorage.getItem(key));
|
||||
}
|
||||
return storageCache.get(key)
|
||||
return storageCache.get(key);
|
||||
}
|
||||
|
||||
function setLocalStorage(key: string, value: string) {
|
||||
localStorage.setItem(key, value)
|
||||
storageCache.set(key, value) // keep cache in sync
|
||||
localStorage.setItem(key, value);
|
||||
storageCache.set(key, value); // keep cache in sync
|
||||
}
|
||||
```
|
||||
|
||||
@@ -41,15 +41,13 @@ Use a Map (not a hook) so it works everywhere: utilities, event handlers, not ju
|
||||
**Cookie caching:**
|
||||
|
||||
```typescript
|
||||
let cookieCache: Record<string, string> | null = null
|
||||
let cookieCache: Record<string, string> | null = null;
|
||||
|
||||
function getCookie(name: string) {
|
||||
if (!cookieCache) {
|
||||
cookieCache = Object.fromEntries(
|
||||
document.cookie.split('; ').map(c => c.split('='))
|
||||
)
|
||||
cookieCache = Object.fromEntries(document.cookie.split('; ').map((c) => c.split('=')));
|
||||
}
|
||||
return cookieCache[name]
|
||||
return cookieCache[name];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -59,12 +57,12 @@ If storage can change externally (another tab, server-set cookies), invalidate c
|
||||
|
||||
```typescript
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key) storageCache.delete(e.key)
|
||||
})
|
||||
if (e.key) storageCache.delete(e.key);
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
storageCache.clear()
|
||||
storageCache.clear();
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
+9
-9
@@ -12,21 +12,21 @@ Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine
|
||||
**Incorrect (3 iterations):**
|
||||
|
||||
```typescript
|
||||
const admins = users.filter(u => u.isAdmin)
|
||||
const testers = users.filter(u => u.isTester)
|
||||
const inactive = users.filter(u => !u.isActive)
|
||||
const admins = users.filter((u) => u.isAdmin);
|
||||
const testers = users.filter((u) => u.isTester);
|
||||
const inactive = users.filter((u) => !u.isActive);
|
||||
```
|
||||
|
||||
**Correct (1 iteration):**
|
||||
|
||||
```typescript
|
||||
const admins: User[] = []
|
||||
const testers: User[] = []
|
||||
const inactive: User[] = []
|
||||
const admins: User[] = [];
|
||||
const testers: User[] = [];
|
||||
const inactive: User[] = [];
|
||||
|
||||
for (const user of users) {
|
||||
if (user.isAdmin) admins.push(user)
|
||||
if (user.isTester) testers.push(user)
|
||||
if (!user.isActive) inactive.push(user)
|
||||
if (user.isAdmin) admins.push(user);
|
||||
if (user.isTester) testers.push(user);
|
||||
if (!user.isActive) inactive.push(user);
|
||||
}
|
||||
```
|
||||
|
||||
+12
-12
@@ -13,22 +13,22 @@ Return early when result is determined to skip unnecessary processing.
|
||||
|
||||
```typescript
|
||||
function validateUsers(users: User[]) {
|
||||
let hasError = false
|
||||
let errorMessage = ''
|
||||
|
||||
let hasError = false;
|
||||
let errorMessage = '';
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
hasError = true
|
||||
errorMessage = 'Email required'
|
||||
hasError = true;
|
||||
errorMessage = 'Email required';
|
||||
}
|
||||
if (!user.name) {
|
||||
hasError = true
|
||||
errorMessage = 'Name required'
|
||||
hasError = true;
|
||||
errorMessage = 'Name required';
|
||||
}
|
||||
// Continues checking all users even after error found
|
||||
}
|
||||
|
||||
return hasError ? { valid: false, error: errorMessage } : { valid: true }
|
||||
|
||||
return hasError ? { valid: false, error: errorMessage } : { valid: true };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -38,13 +38,13 @@ function validateUsers(users: User[]) {
|
||||
function validateUsers(users: User[]) {
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
return { valid: false, error: 'Email required' }
|
||||
return { valid: false, error: 'Email required' };
|
||||
}
|
||||
if (!user.name) {
|
||||
return { valid: false, error: 'Name required' }
|
||||
return { valid: false, error: 'Name required' };
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
return { valid: true };
|
||||
}
|
||||
```
|
||||
|
||||
+3
-3
@@ -39,7 +39,7 @@ function Highlighter({ text, query }: Props) {
|
||||
Global regex (`/g`) has mutable `lastIndex` state:
|
||||
|
||||
```typescript
|
||||
const regex = /foo/g
|
||||
regex.test('foo') // true, lastIndex = 3
|
||||
regex.test('foo') // false, lastIndex = 0
|
||||
const regex = /foo/g;
|
||||
regex.test('foo'); // true, lastIndex = 3
|
||||
regex.test('foo'); // false, lastIndex = 0
|
||||
```
|
||||
|
||||
@@ -13,10 +13,10 @@ Multiple `.find()` calls by the same key should use a Map.
|
||||
|
||||
```typescript
|
||||
function processOrders(orders: Order[], users: User[]) {
|
||||
return orders.map(order => ({
|
||||
return orders.map((order) => ({
|
||||
...order,
|
||||
user: users.find(u => u.id === order.userId)
|
||||
}))
|
||||
user: users.find((u) => u.id === order.userId),
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
@@ -24,12 +24,12 @@ function processOrders(orders: Order[], users: User[]) {
|
||||
|
||||
```typescript
|
||||
function processOrders(orders: Order[], users: User[]) {
|
||||
const userById = new Map(users.map(u => [u.id, u]))
|
||||
const userById = new Map(users.map((u) => [u.id, u]));
|
||||
|
||||
return orders.map(order => ({
|
||||
return orders.map((order) => ({
|
||||
...order,
|
||||
user: userById.get(order.userId)
|
||||
}))
|
||||
user: userById.get(order.userId),
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+7
-6
@@ -16,7 +16,7 @@ In real-world applications, this optimization is especially valuable when the co
|
||||
```typescript
|
||||
function hasChanges(current: string[], original: string[]) {
|
||||
// Always sorts and joins, even when lengths differ
|
||||
return current.sort().join() !== original.sort().join()
|
||||
return current.sort().join() !== original.sort().join();
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,21 +28,22 @@ Two O(n log n) sorts run even when `current.length` is 5 and `original.length` i
|
||||
function hasChanges(current: string[], original: string[]) {
|
||||
// Early return if lengths differ
|
||||
if (current.length !== original.length) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
// Only sort when lengths match
|
||||
const currentSorted = current.toSorted()
|
||||
const originalSorted = original.toSorted()
|
||||
const currentSorted = current.toSorted();
|
||||
const originalSorted = original.toSorted();
|
||||
for (let i = 0; i < currentSorted.length; i++) {
|
||||
if (currentSorted[i] !== originalSorted[i]) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
This new approach is more efficient because:
|
||||
|
||||
- It avoids the overhead of sorting and joining the arrays when lengths differ
|
||||
- It avoids consuming memory for the joined strings (especially important for large arrays)
|
||||
- It avoids mutating the original arrays
|
||||
|
||||
+26
-26
@@ -13,14 +13,14 @@ Finding the smallest or largest element only requires a single pass through the
|
||||
|
||||
```typescript
|
||||
interface Project {
|
||||
id: string
|
||||
name: string
|
||||
updatedAt: number
|
||||
id: string;
|
||||
name: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
function getLatestProject(projects: Project[]) {
|
||||
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
return sorted[0]
|
||||
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
return sorted[0];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,8 +30,8 @@ Sorts the entire array just to find the maximum value.
|
||||
|
||||
```typescript
|
||||
function getOldestAndNewest(projects: Project[]) {
|
||||
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
|
||||
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
|
||||
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
|
||||
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -41,31 +41,31 @@ Still sorts unnecessarily when only min/max are needed.
|
||||
|
||||
```typescript
|
||||
function getLatestProject(projects: Project[]) {
|
||||
if (projects.length === 0) return null
|
||||
|
||||
let latest = projects[0]
|
||||
|
||||
if (projects.length === 0) return null;
|
||||
|
||||
let latest = projects[0];
|
||||
|
||||
for (let i = 1; i < projects.length; i++) {
|
||||
if (projects[i].updatedAt > latest.updatedAt) {
|
||||
latest = projects[i]
|
||||
latest = projects[i];
|
||||
}
|
||||
}
|
||||
|
||||
return latest
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
function getOldestAndNewest(projects: Project[]) {
|
||||
if (projects.length === 0) return { oldest: null, newest: null }
|
||||
|
||||
let oldest = projects[0]
|
||||
let newest = projects[0]
|
||||
|
||||
if (projects.length === 0) return { oldest: null, newest: null };
|
||||
|
||||
let oldest = projects[0];
|
||||
let newest = projects[0];
|
||||
|
||||
for (let i = 1; i < projects.length; i++) {
|
||||
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
|
||||
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
|
||||
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
|
||||
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i];
|
||||
}
|
||||
|
||||
return { oldest, newest }
|
||||
|
||||
return { oldest, newest };
|
||||
}
|
||||
```
|
||||
|
||||
@@ -74,9 +74,9 @@ Single pass through the array, no copying, no sorting.
|
||||
**Alternative (Math.min/Math.max for small arrays):**
|
||||
|
||||
```typescript
|
||||
const numbers = [5, 2, 8, 1, 9]
|
||||
const min = Math.min(...numbers)
|
||||
const max = Math.max(...numbers)
|
||||
const numbers = [5, 2, 8, 1, 9];
|
||||
const min = Math.min(...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.
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ function UserList({ users }: { users: User[] }) {
|
||||
|
||||
```typescript
|
||||
// Fallback for older browsers
|
||||
const sorted = [...items].sort((a, b) => a.value - b.value)
|
||||
const sorted = [...items].sort((a, b) => a.value - b.value);
|
||||
```
|
||||
|
||||
**Other immutable array methods:**
|
||||
|
||||
+2
-2
@@ -12,14 +12,14 @@ Use React's `<Activity>` to preserve state/DOM for expensive components that fre
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Activity } from 'react'
|
||||
import { Activity } from 'react';
|
||||
|
||||
function Dropdown({ isOpen }: Props) {
|
||||
return (
|
||||
<Activity mode={isOpen ? 'visible' : 'hidden'}>
|
||||
<ExpensiveMenu />
|
||||
</Activity>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+4
-13
@@ -14,15 +14,10 @@ Many browsers don't have hardware acceleration for CSS3 animations on SVG elemen
|
||||
```tsx
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<svg
|
||||
className="animate-spin"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -32,15 +27,11 @@ function LoadingSpinner() {
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<div className="animate-spin">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+2
-10
@@ -13,11 +13,7 @@ Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering
|
||||
|
||||
```tsx
|
||||
function Badge({ count }: { count: number }) {
|
||||
return (
|
||||
<div>
|
||||
{count && <span className="badge">{count}</span>}
|
||||
</div>
|
||||
)
|
||||
return <div>{count && <span className="badge">{count}</span>}</div>;
|
||||
}
|
||||
|
||||
// When count = 0, renders: <div>0</div>
|
||||
@@ -28,11 +24,7 @@ function Badge({ count }: { count: number }) {
|
||||
|
||||
```tsx
|
||||
function Badge({ count }: { count: number }) {
|
||||
return (
|
||||
<div>
|
||||
{count > 0 ? <span className="badge">{count}</span> : null}
|
||||
</div>
|
||||
)
|
||||
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
|
||||
}
|
||||
|
||||
// When count = 0, renders: <div></div>
|
||||
|
||||
+2
-2
@@ -24,14 +24,14 @@ Apply `content-visibility: auto` to defer off-screen rendering.
|
||||
function MessageList({ messages }: { messages: Message[] }) {
|
||||
return (
|
||||
<div className="overflow-y-auto h-screen">
|
||||
{messages.map(msg => (
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className="message-item">
|
||||
<Avatar user={msg.author} />
|
||||
<div>{msg.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+4
-14
@@ -13,31 +13,21 @@ Extract static JSX outside components to avoid re-creation.
|
||||
|
||||
```tsx
|
||||
function LoadingSkeleton() {
|
||||
return <div className="animate-pulse h-20 bg-gray-200" />
|
||||
return <div className="animate-pulse h-20 bg-gray-200" />;
|
||||
}
|
||||
|
||||
function Container() {
|
||||
return (
|
||||
<div>
|
||||
{loading && <LoadingSkeleton />}
|
||||
</div>
|
||||
)
|
||||
return <div>{loading && <LoadingSkeleton />}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (reuses same element):**
|
||||
|
||||
```tsx
|
||||
const loadingSkeleton = (
|
||||
<div className="animate-pulse h-20 bg-gray-200" />
|
||||
)
|
||||
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
|
||||
|
||||
function Container() {
|
||||
return (
|
||||
<div>
|
||||
{loading && loadingSkeleton}
|
||||
</div>
|
||||
)
|
||||
return <div>{loading && loadingSkeleton}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+12
-22
@@ -14,13 +14,9 @@ When rendering content that depends on client-side storage (localStorage, cookie
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
// localStorage is not available on server - throws error
|
||||
const theme = localStorage.getItem('theme') || 'light'
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
|
||||
return <div className={theme}>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,21 +26,17 @@ Server-side rendering will fail because `localStorage` is undefined.
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState('light')
|
||||
|
||||
const [theme, setTheme] = useState('light');
|
||||
|
||||
useEffect(() => {
|
||||
// Runs after hydration - causes visible flash
|
||||
const stored = localStorage.getItem('theme')
|
||||
const stored = localStorage.getItem('theme');
|
||||
if (stored) {
|
||||
setTheme(stored)
|
||||
setTheme(stored);
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}, []);
|
||||
|
||||
return <div className={theme}>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -56,9 +48,7 @@ Component first renders with default value (`light`), then updates after hydrati
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div id="theme-wrapper">
|
||||
{children}
|
||||
</div>
|
||||
<div id="theme-wrapper">{children}</div>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@@ -73,7 +63,7 @@ function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+3
-7
@@ -7,13 +7,13 @@ 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.
|
||||
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>
|
||||
return <span>{new Date().toLocaleString()}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -21,10 +21,6 @@ function Timestamp() {
|
||||
|
||||
```tsx
|
||||
function Timestamp() {
|
||||
return (
|
||||
<span suppressHydrationWarning>
|
||||
{new Date().toLocaleString()}
|
||||
</span>
|
||||
)
|
||||
return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
+21
-21
@@ -13,17 +13,17 @@ Use `useTransition` instead of manual `useState` for loading states. This provid
|
||||
|
||||
```tsx
|
||||
function SearchResults() {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
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)
|
||||
}
|
||||
setIsLoading(true);
|
||||
setQuery(value);
|
||||
const data = await fetchResults(value);
|
||||
setResults(data);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -31,29 +31,29 @@ function SearchResults() {
|
||||
{isLoading && <Spinner />}
|
||||
<ResultsList results={results} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (useTransition with built-in pending state):**
|
||||
|
||||
```tsx
|
||||
import { useTransition, useState } from 'react'
|
||||
import { useTransition, useState } from 'react';
|
||||
|
||||
function SearchResults() {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState([])
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setQuery(value) // Update input immediately
|
||||
|
||||
setQuery(value); // Update input immediately
|
||||
|
||||
startTransition(async () => {
|
||||
// Fetch and update results
|
||||
const data = await fetchResults(value)
|
||||
setResults(data)
|
||||
})
|
||||
}
|
||||
const data = await fetchResults(value);
|
||||
setResults(data);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -61,7 +61,7 @@ function SearchResults() {
|
||||
{isPending && <Spinner />}
|
||||
<ResultsList results={results} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+10
-10
@@ -13,14 +13,14 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i
|
||||
|
||||
```tsx
|
||||
function ShareButton({ chatId }: { chatId: string }) {
|
||||
const searchParams = useSearchParams()
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const handleShare = () => {
|
||||
const ref = searchParams.get('ref')
|
||||
shareChat(chatId, { ref })
|
||||
}
|
||||
const ref = searchParams.get('ref');
|
||||
shareChat(chatId, { ref });
|
||||
};
|
||||
|
||||
return <button onClick={handleShare}>Share</button>
|
||||
return <button onClick={handleShare}>Share</button>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -29,11 +29,11 @@ function ShareButton({ chatId }: { chatId: string }) {
|
||||
```tsx
|
||||
function ShareButton({ chatId }: { chatId: string }) {
|
||||
const handleShare = () => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const ref = params.get('ref')
|
||||
shareChat(chatId, { ref })
|
||||
}
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const ref = params.get('ref');
|
||||
shareChat(chatId, { ref });
|
||||
};
|
||||
|
||||
return <button onClick={handleShare}>Share</button>
|
||||
return <button onClick={handleShare}>Share</button>;
|
||||
}
|
||||
```
|
||||
|
||||
+9
-9
@@ -13,16 +13,16 @@ Specify primitive dependencies instead of objects to minimize effect re-runs.
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
console.log(user.id)
|
||||
}, [user])
|
||||
console.log(user.id);
|
||||
}, [user]);
|
||||
```
|
||||
|
||||
**Correct (re-runs only when id changes):**
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
console.log(user.id)
|
||||
}, [user.id])
|
||||
console.log(user.id);
|
||||
}, [user.id]);
|
||||
```
|
||||
|
||||
**For derived state, compute outside effect:**
|
||||
@@ -31,15 +31,15 @@ useEffect(() => {
|
||||
// Incorrect: runs on width=767, 766, 765...
|
||||
useEffect(() => {
|
||||
if (width < 768) {
|
||||
enableMobileMode()
|
||||
enableMobileMode();
|
||||
}
|
||||
}, [width])
|
||||
}, [width]);
|
||||
|
||||
// Correct: runs only on boolean transition
|
||||
const isMobile = width < 768
|
||||
const isMobile = width < 768;
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
enableMobileMode()
|
||||
enableMobileMode();
|
||||
}
|
||||
}, [isMobile])
|
||||
}, [isMobile]);
|
||||
```
|
||||
|
||||
+10
-10
@@ -13,15 +13,15 @@ If a value can be computed from current props/state, do not store it in state or
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState('First')
|
||||
const [lastName, setLastName] = useState('Last')
|
||||
const [fullName, setFullName] = useState('')
|
||||
const [firstName, setFirstName] = useState('First');
|
||||
const [lastName, setLastName] = useState('Last');
|
||||
const [fullName, setFullName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName)
|
||||
}, [firstName, lastName])
|
||||
setFullName(firstName + ' ' + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <p>{fullName}</p>
|
||||
return <p>{fullName}</p>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -29,11 +29,11 @@ function Form() {
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState('First')
|
||||
const [lastName, setLastName] = useState('Last')
|
||||
const fullName = firstName + ' ' + lastName
|
||||
const [firstName, setFirstName] = useState('First');
|
||||
const [lastName, setLastName] = useState('Last');
|
||||
const fullName = firstName + ' ' + lastName;
|
||||
|
||||
return <p>{fullName}</p>
|
||||
return <p>{fullName}</p>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+5
-5
@@ -13,9 +13,9 @@ Subscribe to derived boolean state instead of continuous values to reduce re-ren
|
||||
|
||||
```tsx
|
||||
function Sidebar() {
|
||||
const width = useWindowWidth() // updates continuously
|
||||
const isMobile = width < 768
|
||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
||||
const width = useWindowWidth(); // updates continuously
|
||||
const isMobile = width < 768;
|
||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -23,7 +23,7 @@ function Sidebar() {
|
||||
|
||||
```tsx
|
||||
function Sidebar() {
|
||||
const isMobile = useMediaQuery('(max-width: 767px)')
|
||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
||||
const isMobile = useMediaQuery('(max-width: 767px)');
|
||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />;
|
||||
}
|
||||
```
|
||||
|
||||
+22
-19
@@ -13,19 +13,22 @@ When updating state based on the current state value, use the functional update
|
||||
|
||||
```tsx
|
||||
function TodoList() {
|
||||
const [items, setItems] = useState(initialItems)
|
||||
|
||||
const [items, setItems] = useState(initialItems);
|
||||
|
||||
// Callback must depend on items, recreated on every items change
|
||||
const addItems = useCallback((newItems: Item[]) => {
|
||||
setItems([...items, ...newItems])
|
||||
}, [items]) // ❌ items dependency causes recreations
|
||||
|
||||
const addItems = useCallback(
|
||||
(newItems: Item[]) => {
|
||||
setItems([...items, ...newItems]);
|
||||
},
|
||||
[items],
|
||||
); // ❌ items dependency causes recreations
|
||||
|
||||
// Risk of stale closure if dependency is forgotten
|
||||
const removeItem = useCallback((id: string) => {
|
||||
setItems(items.filter(item => item.id !== id))
|
||||
}, []) // ❌ Missing items dependency - will use stale items!
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
||||
setItems(items.filter((item) => item.id !== id));
|
||||
}, []); // ❌ Missing items dependency - will use stale items!
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -35,19 +38,19 @@ The first callback is recreated every time `items` changes, which can cause chil
|
||||
|
||||
```tsx
|
||||
function TodoList() {
|
||||
const [items, setItems] = useState(initialItems)
|
||||
|
||||
const [items, setItems] = useState(initialItems);
|
||||
|
||||
// Stable callback, never recreated
|
||||
const addItems = useCallback((newItems: Item[]) => {
|
||||
setItems(curr => [...curr, ...newItems])
|
||||
}, []) // ✅ No dependencies needed
|
||||
|
||||
setItems((curr) => [...curr, ...newItems]);
|
||||
}, []); // ✅ No dependencies needed
|
||||
|
||||
// Always uses latest state, no stale closure risk
|
||||
const removeItem = useCallback((id: string) => {
|
||||
setItems(curr => curr.filter(item => item.id !== id))
|
||||
}, []) // ✅ Safe and stable
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
||||
setItems((curr) => curr.filter((item) => item.id !== id));
|
||||
}, []); // ✅ Safe and stable
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+16
-18
@@ -14,20 +14,18 @@ Pass a function to `useState` for expensive initial values. Without the function
|
||||
```tsx
|
||||
function FilteredList({ items }: { items: Item[] }) {
|
||||
// buildSearchIndex() runs on EVERY render, even after initialization
|
||||
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
// When query changes, buildSearchIndex runs again unnecessarily
|
||||
return <SearchResults index={searchIndex} query={query} />
|
||||
return <SearchResults index={searchIndex} query={query} />;
|
||||
}
|
||||
|
||||
function UserProfile() {
|
||||
// JSON.parse runs on every render
|
||||
const [settings, setSettings] = useState(
|
||||
JSON.parse(localStorage.getItem('settings') || '{}')
|
||||
)
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
||||
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem('settings') || '{}'));
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -36,20 +34,20 @@ function UserProfile() {
|
||||
```tsx
|
||||
function FilteredList({ items }: { items: Item[] }) {
|
||||
// buildSearchIndex() runs ONLY on initial render
|
||||
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
return <SearchResults index={searchIndex} query={query} />
|
||||
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
return <SearchResults index={searchIndex} query={query} />;
|
||||
}
|
||||
|
||||
function UserProfile() {
|
||||
// JSON.parse runs only on initial render
|
||||
const [settings, setSettings] = useState(() => {
|
||||
const stored = localStorage.getItem('settings')
|
||||
return stored ? JSON.parse(stored) : {}
|
||||
})
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
||||
const stored = localStorage.getItem('settings');
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
});
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
-2
@@ -1,10 +1,8 @@
|
||||
---
|
||||
|
||||
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
|
||||
|
||||
+10
-10
@@ -14,12 +14,12 @@ Extract expensive work into memoized components to enable early returns before c
|
||||
```tsx
|
||||
function Profile({ user, loading }: Props) {
|
||||
const avatar = useMemo(() => {
|
||||
const id = computeAvatarId(user)
|
||||
return <Avatar id={id} />
|
||||
}, [user])
|
||||
const id = computeAvatarId(user);
|
||||
return <Avatar id={id} />;
|
||||
}, [user]);
|
||||
|
||||
if (loading) return <Skeleton />
|
||||
return <div>{avatar}</div>
|
||||
if (loading) return <Skeleton />;
|
||||
return <div>{avatar}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -27,17 +27,17 @@ function Profile({ user, loading }: Props) {
|
||||
|
||||
```tsx
|
||||
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
||||
const id = useMemo(() => computeAvatarId(user), [user])
|
||||
return <Avatar id={id} />
|
||||
})
|
||||
const id = useMemo(() => computeAvatarId(user), [user]);
|
||||
return <Avatar id={id} />;
|
||||
});
|
||||
|
||||
function Profile({ user, loading }: Props) {
|
||||
if (loading) return <Skeleton />
|
||||
if (loading) return <Skeleton />;
|
||||
return (
|
||||
<div>
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+10
-10
@@ -13,17 +13,17 @@ If a side effect is triggered by a specific user action (submit, click, drag), r
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const theme = useContext(ThemeContext)
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (submitted) {
|
||||
post('/api/register')
|
||||
showToast('Registered', theme)
|
||||
post('/api/register');
|
||||
showToast('Registered', theme);
|
||||
}
|
||||
}, [submitted, theme])
|
||||
}, [submitted, theme]);
|
||||
|
||||
return <button onClick={() => setSubmitted(true)}>Submit</button>
|
||||
return <button onClick={() => setSubmitted(true)}>Submit</button>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,14 +31,14 @@ function Form() {
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const theme = useContext(ThemeContext)
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
function handleSubmit() {
|
||||
post('/api/register')
|
||||
showToast('Registered', theme)
|
||||
post('/api/register');
|
||||
showToast('Registered', theme);
|
||||
}
|
||||
|
||||
return <button onClick={handleSubmit}>Submit</button>
|
||||
return <button onClick={handleSubmit}>Submit</button>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+5
-5
@@ -15,10 +15,10 @@ Calling `useMemo` and comparing hook dependencies may consume more resources tha
|
||||
```tsx
|
||||
function Header({ user, notifications }: Props) {
|
||||
const isLoading = useMemo(() => {
|
||||
return user.isLoading || notifications.isLoading
|
||||
}, [user.isLoading, notifications.isLoading])
|
||||
return user.isLoading || notifications.isLoading;
|
||||
}, [user.isLoading, notifications.isLoading]);
|
||||
|
||||
if (isLoading) return <Skeleton />
|
||||
if (isLoading) return <Skeleton />;
|
||||
// return some markup
|
||||
}
|
||||
```
|
||||
@@ -27,9 +27,9 @@ function Header({ user, notifications }: Props) {
|
||||
|
||||
```tsx
|
||||
function Header({ user, notifications }: Props) {
|
||||
const isLoading = user.isLoading || notifications.isLoading
|
||||
const isLoading = user.isLoading || notifications.isLoading;
|
||||
|
||||
if (isLoading) return <Skeleton />
|
||||
if (isLoading) return <Skeleton />;
|
||||
// return some markup
|
||||
}
|
||||
```
|
||||
|
||||
+12
-12
@@ -13,28 +13,28 @@ Mark frequent, non-urgent state updates as transitions to maintain UI responsive
|
||||
|
||||
```tsx
|
||||
function ScrollTracker() {
|
||||
const [scrollY, setScrollY] = useState(0)
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
useEffect(() => {
|
||||
const handler = () => setScrollY(window.scrollY)
|
||||
window.addEventListener('scroll', handler, { passive: true })
|
||||
return () => window.removeEventListener('scroll', handler)
|
||||
}, [])
|
||||
const handler = () => setScrollY(window.scrollY);
|
||||
window.addEventListener('scroll', handler, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handler);
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (non-blocking updates):**
|
||||
|
||||
```tsx
|
||||
import { startTransition } from 'react'
|
||||
import { startTransition } from 'react';
|
||||
|
||||
function ScrollTracker() {
|
||||
const [scrollY, setScrollY] = useState(0)
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
startTransition(() => setScrollY(window.scrollY))
|
||||
}
|
||||
window.addEventListener('scroll', handler, { passive: true })
|
||||
return () => window.removeEventListener('scroll', handler)
|
||||
}, [])
|
||||
startTransition(() => setScrollY(window.scrollY));
|
||||
};
|
||||
window.addEventListener('scroll', handler, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handler);
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
|
||||
+16
-16
@@ -13,13 +13,13 @@ When a value changes frequently and you don't want a re-render on every update (
|
||||
|
||||
```tsx
|
||||
function Tracker() {
|
||||
const [lastX, setLastX] = useState(0)
|
||||
const [lastX, setLastX] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => setLastX(e.clientX)
|
||||
window.addEventListener('mousemove', onMove)
|
||||
return () => window.removeEventListener('mousemove', onMove)
|
||||
}, [])
|
||||
const onMove = (e: MouseEvent) => setLastX(e.clientX);
|
||||
window.addEventListener('mousemove', onMove);
|
||||
return () => window.removeEventListener('mousemove', onMove);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -32,7 +32,7 @@ function Tracker() {
|
||||
background: 'black',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -40,20 +40,20 @@ function Tracker() {
|
||||
|
||||
```tsx
|
||||
function Tracker() {
|
||||
const lastXRef = useRef(0)
|
||||
const dotRef = useRef<HTMLDivElement>(null)
|
||||
const lastXRef = useRef(0);
|
||||
const dotRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => {
|
||||
lastXRef.current = e.clientX
|
||||
const node = dotRef.current
|
||||
lastXRef.current = e.clientX;
|
||||
const node = dotRef.current;
|
||||
if (node) {
|
||||
node.style.transform = `translateX(${e.clientX}px)`
|
||||
node.style.transform = `translateX(${e.clientX}px)`;
|
||||
}
|
||||
}
|
||||
window.addEventListener('mousemove', onMove)
|
||||
return () => window.removeEventListener('mousemove', onMove)
|
||||
}, [])
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
return () => window.removeEventListener('mousemove', onMove);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -68,6 +68,6 @@ function Tracker() {
|
||||
transform: 'translateX(0px)',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
+21
-21
@@ -12,46 +12,46 @@ Use Next.js's `after()` to schedule work that should execute after a response is
|
||||
**Incorrect (blocks response):**
|
||||
|
||||
```tsx
|
||||
import { logUserAction } from '@/app/utils'
|
||||
import { logUserAction } from '@/app/utils';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Perform mutation
|
||||
await updateDatabase(request)
|
||||
|
||||
await updateDatabase(request);
|
||||
|
||||
// Logging blocks the response
|
||||
const userAgent = request.headers.get('user-agent') || 'unknown'
|
||||
await logUserAction({ userAgent })
|
||||
|
||||
const userAgent = request.headers.get('user-agent') || 'unknown';
|
||||
await logUserAction({ userAgent });
|
||||
|
||||
return new Response(JSON.stringify({ status: 'success' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (non-blocking):**
|
||||
|
||||
```tsx
|
||||
import { after } from 'next/server'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
import { logUserAction } from '@/app/utils'
|
||||
import { after } from 'next/server';
|
||||
import { headers, cookies } from 'next/headers';
|
||||
import { logUserAction } from '@/app/utils';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Perform mutation
|
||||
await updateDatabase(request)
|
||||
|
||||
await updateDatabase(request);
|
||||
|
||||
// Log after response is sent
|
||||
after(async () => {
|
||||
const userAgent = (await headers()).get('user-agent') || 'unknown'
|
||||
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
|
||||
|
||||
logUserAction({ sessionCookie, userAgent })
|
||||
})
|
||||
|
||||
const userAgent = (await headers()).get('user-agent') || 'unknown';
|
||||
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous';
|
||||
|
||||
logUserAction({ sessionCookie, userAgent });
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ status: 'success' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+31
-31
@@ -16,80 +16,80 @@ Next.js documentation explicitly states: "Treat Server Actions with the same sec
|
||||
**Incorrect (no authentication check):**
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
'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 }
|
||||
await db.user.delete({ where: { id: userId } });
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (authentication inside the action):**
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
'use server';
|
||||
|
||||
import { verifySession } from '@/lib/auth'
|
||||
import { unauthorized } from '@/lib/errors'
|
||||
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()
|
||||
|
||||
const session = await verifySession();
|
||||
|
||||
if (!session) {
|
||||
throw unauthorized('Must be logged in')
|
||||
throw unauthorized('Must be logged in');
|
||||
}
|
||||
|
||||
|
||||
// Check authorization too
|
||||
if (session.user.role !== 'admin' && session.user.id !== userId) {
|
||||
throw unauthorized('Cannot delete other users')
|
||||
throw unauthorized('Cannot delete other users');
|
||||
}
|
||||
|
||||
await db.user.delete({ where: { id: userId } })
|
||||
return { success: true }
|
||||
|
||||
await db.user.delete({ where: { id: userId } });
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
**With input validation:**
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
'use server';
|
||||
|
||||
import { verifySession } from '@/lib/auth'
|
||||
import { z } from 'zod'
|
||||
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()
|
||||
})
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export async function updateProfile(data: unknown) {
|
||||
// Validate input first
|
||||
const validated = updateProfileSchema.parse(data)
|
||||
|
||||
const validated = updateProfileSchema.parse(data);
|
||||
|
||||
// Then authenticate
|
||||
const session = await verifySession()
|
||||
const session = await verifySession();
|
||||
if (!session) {
|
||||
throw new Error('Unauthorized')
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
|
||||
// Then authorize
|
||||
if (session.user.id !== validated.userId) {
|
||||
throw new Error('Can only update own profile')
|
||||
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 }
|
||||
email: validated.email,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+8
-8
@@ -12,20 +12,20 @@ tags: server, cache, lru, cross-request
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import { LRUCache } from 'lru-cache';
|
||||
|
||||
const cache = new LRUCache<string, any>({
|
||||
max: 1000,
|
||||
ttl: 5 * 60 * 1000 // 5 minutes
|
||||
})
|
||||
ttl: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
|
||||
export async function getUser(id: string) {
|
||||
const cached = cache.get(id)
|
||||
if (cached) return cached
|
||||
const cached = cache.get(id);
|
||||
if (cached) return cached;
|
||||
|
||||
const user = await db.user.findUnique({ where: { id } })
|
||||
cache.set(id, user)
|
||||
return user
|
||||
const user = await db.user.findUnique({ where: { id } });
|
||||
cache.set(id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
// Request 1: DB query, result cached
|
||||
|
||||
+17
-17
@@ -12,15 +12,15 @@ Use `React.cache()` for server-side request deduplication. Authentication and da
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { cache } from 'react'
|
||||
import { cache } from 'react';
|
||||
|
||||
export const getCurrentUser = cache(async () => {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return null
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
return await db.user.findUnique({
|
||||
where: { id: session.user.id }
|
||||
})
|
||||
})
|
||||
where: { id: session.user.id },
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
|
||||
@@ -33,32 +33,32 @@ Within a single request, multiple calls to `getCurrentUser()` execute the query
|
||||
|
||||
```typescript
|
||||
const getUser = cache(async (params: { uid: number }) => {
|
||||
return await db.user.findUnique({ where: { id: params.uid } })
|
||||
})
|
||||
return await db.user.findUnique({ where: { id: params.uid } });
|
||||
});
|
||||
|
||||
// Each call creates new object, never hits cache
|
||||
getUser({ uid: 1 })
|
||||
getUser({ uid: 1 }) // Cache miss, runs query again
|
||||
getUser({ uid: 1 });
|
||||
getUser({ uid: 1 }); // Cache miss, runs query again
|
||||
```
|
||||
|
||||
**Correct (cache hit):**
|
||||
|
||||
```typescript
|
||||
const getUser = cache(async (uid: number) => {
|
||||
return await db.user.findUnique({ where: { id: uid } })
|
||||
})
|
||||
return await db.user.findUnique({ where: { id: uid } });
|
||||
});
|
||||
|
||||
// Primitive args use value equality
|
||||
getUser(1)
|
||||
getUser(1) // Cache hit, returns cached result
|
||||
getUser(1);
|
||||
getUser(1); // Cache hit, returns cached result
|
||||
```
|
||||
|
||||
If you must pass objects, pass the same reference:
|
||||
|
||||
```typescript
|
||||
const params = { uid: 1 }
|
||||
getUser(params) // Query runs
|
||||
getUser(params) // Cache hit (same reference)
|
||||
const params = { uid: 1 };
|
||||
getUser(params); // Query runs
|
||||
getUser(params); // Cache hit (same reference)
|
||||
```
|
||||
|
||||
**Next.js-Specific Note:**
|
||||
|
||||
+3
-3
@@ -22,11 +22,11 @@ RSC→client serialization deduplicates by object reference, not value. Same ref
|
||||
|
||||
```tsx
|
||||
// RSC: send once
|
||||
<ClientList usernames={usernames} />
|
||||
<ClientList usernames={usernames} />;
|
||||
|
||||
// Client: transform there
|
||||
'use client'
|
||||
const sorted = useMemo(() => [...usernames].sort(), [usernames])
|
||||
('use client');
|
||||
const sorted = useMemo(() => [...usernames].sort(), [usernames]);
|
||||
```
|
||||
|
||||
**Nested deduplication behavior:**
|
||||
|
||||
+15
-15
@@ -13,18 +13,18 @@ React Server Components execute sequentially within a tree. Restructure with com
|
||||
|
||||
```tsx
|
||||
export default async function Page() {
|
||||
const header = await fetchHeader()
|
||||
const header = await fetchHeader();
|
||||
return (
|
||||
<div>
|
||||
<div>{header}</div>
|
||||
<Sidebar />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
const items = await fetchSidebarItems()
|
||||
return <nav>{items.map(renderItem)}</nav>
|
||||
const items = await fetchSidebarItems();
|
||||
return <nav>{items.map(renderItem)}</nav>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -32,13 +32,13 @@ async function Sidebar() {
|
||||
|
||||
```tsx
|
||||
async function Header() {
|
||||
const data = await fetchHeader()
|
||||
return <div>{data}</div>
|
||||
const data = await fetchHeader();
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
const items = await fetchSidebarItems()
|
||||
return <nav>{items.map(renderItem)}</nav>
|
||||
const items = await fetchSidebarItems();
|
||||
return <nav>{items.map(renderItem)}</nav>;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
@@ -47,7 +47,7 @@ export default function Page() {
|
||||
<Header />
|
||||
<Sidebar />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -55,13 +55,13 @@ export default function Page() {
|
||||
|
||||
```tsx
|
||||
async function Header() {
|
||||
const data = await fetchHeader()
|
||||
return <div>{data}</div>
|
||||
const data = await fetchHeader();
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
const items = await fetchSidebarItems()
|
||||
return <nav>{items.map(renderItem)}</nav>
|
||||
const items = await fetchSidebarItems();
|
||||
return <nav>{items.map(renderItem)}</nav>;
|
||||
}
|
||||
|
||||
function Layout({ children }: { children: ReactNode }) {
|
||||
@@ -70,7 +70,7 @@ function Layout({ children }: { children: ReactNode }) {
|
||||
<Header />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
@@ -78,6 +78,6 @@ export default function Page() {
|
||||
<Layout>
|
||||
<Sidebar />
|
||||
</Layout>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
+8
-8
@@ -13,13 +13,13 @@ The React Server/Client boundary serializes all object properties into strings a
|
||||
|
||||
```tsx
|
||||
async function Page() {
|
||||
const user = await fetchUser() // 50 fields
|
||||
return <Profile user={user} />
|
||||
const user = await fetchUser(); // 50 fields
|
||||
return <Profile user={user} />;
|
||||
}
|
||||
|
||||
'use client'
|
||||
('use client');
|
||||
function Profile({ user }: { user: User }) {
|
||||
return <div>{user.name}</div> // uses 1 field
|
||||
return <div>{user.name}</div>; // uses 1 field
|
||||
}
|
||||
```
|
||||
|
||||
@@ -27,12 +27,12 @@ function Profile({ user }: { user: User }) {
|
||||
|
||||
```tsx
|
||||
async function Page() {
|
||||
const user = await fetchUser()
|
||||
return <Profile name={user.name} />
|
||||
const user = await fetchUser();
|
||||
return <Profile name={user.name} />;
|
||||
}
|
||||
|
||||
'use client'
|
||||
('use client');
|
||||
function Profile({ name }: { name: string }) {
|
||||
return <div>{name}</div>
|
||||
return <div>{name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user