mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-02-13 12:55:42 +01:00
fix: resolve Prettier markdown code block parsing errors
- Fix syntax errors in skills markdown files (.github/skills, .opencode/skills) - Change typescript to tsx for code blocks with JSX - Replace ellipsis (...) in array examples with valid syntax - Separate CSS from TypeScript into distinct code blocks - Convert JavaScript object examples to valid JSON in docs - Fix enum definitions with proper comma separation
This commit is contained in:
1018
.github/skills/vercel-react-best-practices/AGENTS.md
vendored
1018
.github/skills/vercel-react-best-practices/AGENTS.md
vendored
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ Comprehensive performance optimization guide for React and Next.js applications,
|
|||||||
## When to Apply
|
## When to Apply
|
||||||
|
|
||||||
Reference these guidelines when:
|
Reference these guidelines when:
|
||||||
|
|
||||||
- Writing new React components or Next.js pages
|
- Writing new React components or Next.js pages
|
||||||
- Implementing data fetching (client or server-side)
|
- Implementing data fetching (client or server-side)
|
||||||
- Reviewing code for performance issues
|
- Reviewing code for performance issues
|
||||||
@@ -22,16 +23,16 @@ Reference these guidelines when:
|
|||||||
|
|
||||||
## Rule Categories by Priority
|
## Rule Categories by Priority
|
||||||
|
|
||||||
| Priority | Category | Impact | Prefix |
|
| Priority | Category | Impact | Prefix |
|
||||||
|----------|----------|--------|--------|
|
| -------- | ------------------------- | ----------- | ------------ |
|
||||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
@@ -115,6 +116,7 @@ rules/_sections.md
|
|||||||
```
|
```
|
||||||
|
|
||||||
Each rule file contains:
|
Each rule file contains:
|
||||||
|
|
||||||
- Brief explanation of why it matters
|
- Brief explanation of why it matters
|
||||||
- Incorrect code example with explanation
|
- Incorrect code example with explanation
|
||||||
- Correct code example with explanation
|
- Correct code example with explanation
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
|
|||||||
```tsx
|
```tsx
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: (e) => void) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(event, handler)
|
window.addEventListener(event, handler);
|
||||||
return () => window.removeEventListener(event, handler)
|
return () => window.removeEventListener(event, handler);
|
||||||
}, [event, handler])
|
}, [event, handler]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -24,31 +24,31 @@ function useWindowEvent(event: string, handler: (e) => void) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: (e) => void) {
|
||||||
const handlerRef = useRef(handler)
|
const handlerRef = useRef(handler);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handlerRef.current = handler
|
handlerRef.current = handler;
|
||||||
}, [handler])
|
}, [handler]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (e) => handlerRef.current(e)
|
const listener = (e) => handlerRef.current(e);
|
||||||
window.addEventListener(event, listener)
|
window.addEventListener(event, listener);
|
||||||
return () => window.removeEventListener(event, listener)
|
return () => window.removeEventListener(event, listener);
|
||||||
}, [event])
|
}, [event]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Alternative: use `useEffectEvent` if you're on latest React:**
|
**Alternative: use `useEffectEvent` if you're on latest React:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useEffectEvent } from 'react'
|
import { useEffectEvent } from "react";
|
||||||
|
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: (e) => void) {
|
||||||
const onEvent = useEffectEvent(handler)
|
const onEvent = useEffectEvent(handler);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(event, onEvent)
|
window.addEventListener(event, onEvent);
|
||||||
return () => window.removeEventListener(event, onEvent)
|
return () => window.removeEventListener(event, onEvent);
|
||||||
}, [event])
|
}, [event]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ Access latest values in callbacks without adding them to dependency arrays. Prev
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function useLatest<T>(value: T) {
|
function useLatest<T>(value: T) {
|
||||||
const ref = useRef(value)
|
const ref = useRef(value);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
ref.current = value
|
ref.current = value;
|
||||||
}, [value])
|
}, [value]);
|
||||||
return ref
|
return ref;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -25,12 +25,12 @@ function useLatest<T>(value: T) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => onSearch(query), 300)
|
const timeout = setTimeout(() => onSearch(query), 300);
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout);
|
||||||
}, [query, onSearch])
|
}, [query, onSearch]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,12 +38,12 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState("");
|
||||||
const onSearchRef = useLatest(onSearch)
|
const onSearchRef = useLatest(onSearch);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => onSearchRef.current(query), 300)
|
const timeout = setTimeout(() => onSearchRef.current(query), 300);
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout);
|
||||||
}, [query])
|
}, [query]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ In API routes and Server Actions, start independent operations immediately, even
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
const config = await fetchConfig()
|
const config = await fetchConfig();
|
||||||
const data = await fetchData(session.user.id)
|
const data = await fetchData(session.user.id);
|
||||||
return Response.json({ data, config })
|
return Response.json({ data, config });
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -24,14 +24,11 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const sessionPromise = auth()
|
const sessionPromise = auth();
|
||||||
const configPromise = fetchConfig()
|
const configPromise = fetchConfig();
|
||||||
const session = await sessionPromise
|
const session = await sessionPromise;
|
||||||
const [config, data] = await Promise.all([
|
const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);
|
||||||
configPromise,
|
return Response.json({ data, config });
|
||||||
fetchData(session.user.id)
|
|
||||||
])
|
|
||||||
return Response.json({ data, config })
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ Move `await` operations into the branches where they're actually used to avoid b
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||||
const userData = await fetchUserData(userId)
|
const userData = await fetchUserData(userId);
|
||||||
|
|
||||||
if (skipProcessing) {
|
if (skipProcessing) {
|
||||||
// Returns immediately but still waited for userData
|
// Returns immediately but still waited for userData
|
||||||
return { skipped: true }
|
return { skipped: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only this branch uses userData
|
// 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) {
|
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||||
if (skipProcessing) {
|
if (skipProcessing) {
|
||||||
// Returns immediately without waiting
|
// Returns immediately without waiting
|
||||||
return { skipped: true }
|
return { skipped: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch only when needed
|
// Fetch only when needed
|
||||||
const userData = await fetchUserData(userId)
|
const userData = await fetchUserData(userId);
|
||||||
return processUserData(userData)
|
return processUserData(userData);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,35 +45,35 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
|
|||||||
```typescript
|
```typescript
|
||||||
// Incorrect: always fetches permissions
|
// Incorrect: always fetches permissions
|
||||||
async function updateResource(resourceId: string, userId: string) {
|
async function updateResource(resourceId: string, userId: string) {
|
||||||
const permissions = await fetchPermissions(userId)
|
const permissions = await fetchPermissions(userId);
|
||||||
const resource = await getResource(resourceId)
|
const resource = await getResource(resourceId);
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return { error: 'Not found' }
|
return { error: "Not found" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!permissions.canEdit) {
|
if (!permissions.canEdit) {
|
||||||
return { error: 'Forbidden' }
|
return { error: "Forbidden" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return await updateResourceData(resource, permissions)
|
return await updateResourceData(resource, permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Correct: fetches only when needed
|
// Correct: fetches only when needed
|
||||||
async function updateResource(resourceId: string, userId: string) {
|
async function updateResource(resourceId: string, userId: string) {
|
||||||
const resource = await getResource(resourceId)
|
const resource = await getResource(resourceId);
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return { error: 'Not found' }
|
return { error: "Not found" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = await fetchPermissions(userId)
|
const permissions = await fetchPermissions(userId);
|
||||||
|
|
||||||
if (!permissions.canEdit) {
|
if (!permissions.canEdit) {
|
||||||
return { error: 'Forbidden' }
|
return { error: "Forbidden" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return await updateResourceData(resource, permissions)
|
return await updateResourceData(resource, permissions);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,25 +12,26 @@ For operations with partial dependencies, use `better-all` to maximize paralleli
|
|||||||
**Incorrect (profile waits for config unnecessarily):**
|
**Incorrect (profile waits for config unnecessarily):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const [user, config] = await Promise.all([
|
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
|
||||||
fetchUser(),
|
const profile = await fetchProfile(user.id);
|
||||||
fetchConfig()
|
|
||||||
])
|
|
||||||
const profile = await fetchProfile(user.id)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (config and profile run in parallel):**
|
**Correct (config and profile run in parallel):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { all } from 'better-all'
|
import { all } from "better-all";
|
||||||
|
|
||||||
const { user, config, profile } = await all({
|
const { user, config, profile } = await all({
|
||||||
async user() { return fetchUser() },
|
async user() {
|
||||||
async config() { return fetchConfig() },
|
return fetchUser();
|
||||||
|
},
|
||||||
|
async config() {
|
||||||
|
return fetchConfig();
|
||||||
|
},
|
||||||
async profile() {
|
async profile() {
|
||||||
return fetchProfile((await this.$.user).id)
|
return fetchProfile((await this.$.user).id);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -12,17 +12,13 @@ When async operations have no interdependencies, execute them concurrently using
|
|||||||
**Incorrect (sequential execution, 3 round trips):**
|
**Incorrect (sequential execution, 3 round trips):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const user = await fetchUser()
|
const user = await fetchUser();
|
||||||
const posts = await fetchPosts()
|
const posts = await fetchPosts();
|
||||||
const comments = await fetchComments()
|
const comments = await fetchComments();
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (parallel execution, 1 round trip):**
|
**Correct (parallel execution, 1 round trip):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const [user, posts, comments] = await Promise.all([
|
const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);
|
||||||
fetchUser(),
|
|
||||||
fetchPosts(),
|
|
||||||
fetchComments()
|
|
||||||
])
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Instead of awaiting data in async components before returning JSX, use Suspense
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const data = await fetchData() // Blocks entire page
|
const data = await fetchData(); // Blocks entire page
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -24,7 +24,7 @@ async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<div>Footer</div>
|
<div>Footer</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,12 +45,12 @@ function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<div>Footer</div>
|
<div>Footer</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function DataDisplay() {
|
async function DataDisplay() {
|
||||||
const data = await fetchData() // Only blocks this component
|
const data = await fetchData(); // Only blocks this component
|
||||||
return <div>{data.content}</div>
|
return <div>{data.content}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
|
|||||||
```tsx
|
```tsx
|
||||||
function Page() {
|
function Page() {
|
||||||
// Start fetch immediately, but don't await
|
// Start fetch immediately, but don't await
|
||||||
const dataPromise = fetchData()
|
const dataPromise = fetchData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -73,17 +73,17 @@ function Page() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
<div>Footer</div>
|
<div>Footer</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||||
const data = use(dataPromise) // Unwraps the promise
|
const data = use(dataPromise); // Unwraps the promise
|
||||||
return <div>{data.content}</div>
|
return <div>{data.content}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||||
const data = use(dataPromise) // Reuses the same promise
|
const data = use(dataPromise); // Reuses the same promise
|
||||||
return <div>{data.summary}</div>
|
return <div>{data.summary}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,24 +16,24 @@ Popular icon and component libraries can have **up to 10,000 re-exports** in the
|
|||||||
**Incorrect (imports entire library):**
|
**Incorrect (imports entire library):**
|
||||||
|
|
||||||
```tsx
|
```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
|
// Loads 1,583 modules, takes ~2.8s extra in dev
|
||||||
// Runtime cost: 200-800ms on every cold start
|
// 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
|
// Loads 2,225 modules, takes ~4.2s extra in dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (imports only what you need):**
|
**Correct (imports only what you need):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import Check from 'lucide-react/dist/esm/icons/check'
|
import Check from "lucide-react/dist/esm/icons/check";
|
||||||
import X from 'lucide-react/dist/esm/icons/x'
|
import X from "lucide-react/dist/esm/icons/x";
|
||||||
import Menu from 'lucide-react/dist/esm/icons/menu'
|
import Menu from "lucide-react/dist/esm/icons/menu";
|
||||||
// Loads only 3 modules (~2KB vs ~1MB)
|
// Loads only 3 modules (~2KB vs ~1MB)
|
||||||
|
|
||||||
import Button from '@mui/material/Button'
|
import Button from "@mui/material/Button";
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from "@mui/material/TextField";
|
||||||
// Loads only what you use
|
// Loads only what you use
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -43,12 +43,12 @@ import TextField from '@mui/material/TextField'
|
|||||||
// next.config.js - use optimizePackageImports
|
// next.config.js - use optimizePackageImports
|
||||||
module.exports = {
|
module.exports = {
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizePackageImports: ['lucide-react', '@mui/material']
|
optimizePackageImports: ["lucide-react", "@mui/material"],
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// Then you can keep the ergonomic barrel imports:
|
// 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
|
// Automatically transformed to direct imports at build time
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,19 +12,25 @@ Load large data or modules only when a feature is activated.
|
|||||||
**Example (lazy-load animation frames):**
|
**Example (lazy-load animation frames):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
|
function AnimationPlayer({
|
||||||
const [frames, setFrames] = useState<Frame[] | null>(null)
|
enabled,
|
||||||
|
setEnabled,
|
||||||
|
}: {
|
||||||
|
enabled: boolean;
|
||||||
|
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}) {
|
||||||
|
const [frames, setFrames] = useState<Frame[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enabled && !frames && typeof window !== 'undefined') {
|
if (enabled && !frames && typeof window !== "undefined") {
|
||||||
import('./animation-frames.js')
|
import("./animation-frames.js")
|
||||||
.then(mod => setFrames(mod.frames))
|
.then((mod) => setFrames(mod.frames))
|
||||||
.catch(() => setEnabled(false))
|
.catch(() => setEnabled(false));
|
||||||
}
|
}
|
||||||
}, [enabled, frames, setEnabled])
|
}, [enabled, frames, setEnabled]);
|
||||||
|
|
||||||
if (!frames) return <Skeleton />
|
if (!frames) return <Skeleton />;
|
||||||
return <Canvas frames={frames} />
|
return <Canvas frames={frames} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Analytics, logging, and error tracking don't block user interaction. Load them a
|
|||||||
**Incorrect (blocks initial bundle):**
|
**Incorrect (blocks initial bundle):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Analytics } from '@vercel/analytics/react'
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
return (
|
return (
|
||||||
@@ -22,19 +22,18 @@ export default function RootLayout({ children }) {
|
|||||||
<Analytics />
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (loads after hydration):**
|
**Correct (loads after hydration):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const Analytics = dynamic(
|
const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), {
|
||||||
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
ssr: false,
|
||||||
{ ssr: false }
|
});
|
||||||
)
|
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
return (
|
return (
|
||||||
@@ -44,6 +43,6 @@ export default function RootLayout({ children }) {
|
|||||||
<Analytics />
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,24 +12,23 @@ Use `next/dynamic` to lazy-load large components not needed on initial render.
|
|||||||
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { MonacoEditor } from './monaco-editor'
|
import { MonacoEditor } from "./monaco-editor";
|
||||||
|
|
||||||
function CodePanel({ code }: { code: string }) {
|
function CodePanel({ code }: { code: string }) {
|
||||||
return <MonacoEditor value={code} />
|
return <MonacoEditor value={code} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (Monaco loads on demand):**
|
**Correct (Monaco loads on demand):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const MonacoEditor = dynamic(
|
const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), {
|
||||||
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
ssr: false,
|
||||||
{ ssr: false }
|
});
|
||||||
)
|
|
||||||
|
|
||||||
function CodePanel({ code }: { code: string }) {
|
function CodePanel({ code }: { code: string }) {
|
||||||
return <MonacoEditor value={code} />
|
return <MonacoEditor value={code} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -14,20 +14,16 @@ Preload heavy bundles before they're needed to reduce perceived latency.
|
|||||||
```tsx
|
```tsx
|
||||||
function EditorButton({ onClick }: { onClick: () => void }) {
|
function EditorButton({ onClick }: { onClick: () => void }) {
|
||||||
const preload = () => {
|
const preload = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
void import('./monaco-editor')
|
void import("./monaco-editor");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
|
||||||
onMouseEnter={preload}
|
|
||||||
onFocus={preload}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
Open Editor
|
Open Editor
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,14 +32,12 @@ function EditorButton({ onClick }: { onClick: () => void }) {
|
|||||||
```tsx
|
```tsx
|
||||||
function FlagsProvider({ children, flags }: Props) {
|
function FlagsProvider({ children, flags }: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.editorEnabled && typeof window !== 'undefined') {
|
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}>
|
return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
|
||||||
{children}
|
|
||||||
</FlagsContext.Provider>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ function useKeyboardShortcut(key: string, callback: () => void) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
if (e.metaKey && e.key === key) {
|
if (e.metaKey && e.key === key) {
|
||||||
callback()
|
callback();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
window.addEventListener('keydown', handler)
|
window.addEventListener("keydown", handler);
|
||||||
return () => window.removeEventListener('keydown', handler)
|
return () => window.removeEventListener("keydown", handler);
|
||||||
}, [key, callback])
|
}, [key, callback]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,45 +30,49 @@ When using the `useKeyboardShortcut` hook multiple times, each instance will reg
|
|||||||
**Correct (N instances = 1 listener):**
|
**Correct (N instances = 1 listener):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import useSWRSubscription from 'swr/subscription'
|
import useSWRSubscription from "swr/subscription";
|
||||||
|
|
||||||
// Module-level Map to track callbacks per key
|
// 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) {
|
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||||
// Register this callback in the Map
|
// Register this callback in the Map
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!keyCallbacks.has(key)) {
|
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 () => {
|
return () => {
|
||||||
const set = keyCallbacks.get(key)
|
const set = keyCallbacks.get(key);
|
||||||
if (set) {
|
if (set) {
|
||||||
set.delete(callback)
|
set.delete(callback);
|
||||||
if (set.size === 0) {
|
if (set.size === 0) {
|
||||||
keyCallbacks.delete(key)
|
keyCallbacks.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [key, callback])
|
}, [key, callback]);
|
||||||
|
|
||||||
useSWRSubscription('global-keydown', () => {
|
useSWRSubscription("global-keydown", () => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
if (e.metaKey && keyCallbacks.has(e.key)) {
|
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)
|
window.addEventListener("keydown", handler);
|
||||||
return () => window.removeEventListener('keydown', handler)
|
return () => window.removeEventListener("keydown", handler);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function Profile() {
|
function Profile() {
|
||||||
// Multiple shortcuts will share the same listener
|
// Multiple shortcuts will share the same listener
|
||||||
useKeyboardShortcut('p', () => { /* ... */ })
|
useKeyboardShortcut("p", () => {
|
||||||
useKeyboardShortcut('k', () => { /* ... */ })
|
/* ... */
|
||||||
|
});
|
||||||
|
useKeyboardShortcut("k", () => {
|
||||||
|
/* ... */
|
||||||
|
});
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ Add version prefix to keys and store only needed fields. Prevents schema conflic
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// No version, stores everything, no error handling
|
// No version, stores everything, no error handling
|
||||||
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
|
localStorage.setItem("userConfig", JSON.stringify(fullUserObject));
|
||||||
const data = localStorage.getItem('userConfig')
|
const data = localStorage.getItem("userConfig");
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct:**
|
**Correct:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const VERSION = 'v2'
|
const VERSION = "v2";
|
||||||
|
|
||||||
function saveConfig(config: { theme: string; language: string }) {
|
function saveConfig(config: { theme: string; language: string }) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
|
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
|
||||||
} catch {
|
} catch {
|
||||||
// Throws in incognito/private browsing, quota exceeded, or disabled
|
// Throws in incognito/private browsing, quota exceeded, or disabled
|
||||||
}
|
}
|
||||||
@@ -32,21 +32,21 @@ function saveConfig(config: { theme: string; language: string }) {
|
|||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
try {
|
try {
|
||||||
const data = localStorage.getItem(`userConfig:${VERSION}`)
|
const data = localStorage.getItem(`userConfig:${VERSION}`);
|
||||||
return data ? JSON.parse(data) : null
|
return data ? JSON.parse(data) : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migration from v1 to v2
|
// Migration from v1 to v2
|
||||||
function migrate() {
|
function migrate() {
|
||||||
try {
|
try {
|
||||||
const v1 = localStorage.getItem('userConfig:v1')
|
const v1 = localStorage.getItem("userConfig:v1");
|
||||||
if (v1) {
|
if (v1) {
|
||||||
const old = JSON.parse(v1)
|
const old = JSON.parse(v1);
|
||||||
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
|
saveConfig({ theme: old.darkMode ? "dark" : "light", language: old.lang });
|
||||||
localStorage.removeItem('userConfig:v1')
|
localStorage.removeItem("userConfig:v1");
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,13 @@ function migrate() {
|
|||||||
// User object has 20+ fields, only store what UI needs
|
// User object has 20+ fields, only store what UI needs
|
||||||
function cachePrefs(user: FullUser) {
|
function cachePrefs(user: FullUser) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('prefs:v1', JSON.stringify({
|
localStorage.setItem(
|
||||||
theme: user.preferences.theme,
|
"prefs:v1",
|
||||||
notifications: user.preferences.notifications
|
JSON.stringify({
|
||||||
}))
|
theme: user.preferences.theme,
|
||||||
|
notifications: user.preferences.notifications,
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,34 +13,34 @@ Add `{ passive: true }` to touch and wheel event listeners to enable immediate s
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
|
||||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
|
||||||
|
|
||||||
document.addEventListener('touchstart', handleTouch)
|
document.addEventListener("touchstart", handleTouch);
|
||||||
document.addEventListener('wheel', handleWheel)
|
document.addEventListener("wheel", handleWheel);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('touchstart', handleTouch)
|
document.removeEventListener("touchstart", handleTouch);
|
||||||
document.removeEventListener('wheel', handleWheel)
|
document.removeEventListener("wheel", handleWheel);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct:**
|
**Correct:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
|
||||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
|
||||||
|
|
||||||
document.addEventListener('touchstart', handleTouch, { passive: true })
|
document.addEventListener("touchstart", handleTouch, { passive: true });
|
||||||
document.addEventListener('wheel', handleWheel, { passive: true })
|
document.addEventListener("wheel", handleWheel, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('touchstart', handleTouch)
|
document.removeEventListener("touchstart", handleTouch);
|
||||||
document.removeEventListener('wheel', handleWheel)
|
document.removeEventListener("wheel", handleWheel);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
|
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
|
||||||
|
|||||||
@@ -13,43 +13,43 @@ SWR enables request deduplication, caching, and revalidation across component in
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function UserList() {
|
function UserList() {
|
||||||
const [users, setUsers] = useState([])
|
const [users, setUsers] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/users')
|
fetch("/api/users")
|
||||||
.then(r => r.json())
|
.then((r) => r.json())
|
||||||
.then(setUsers)
|
.then(setUsers);
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (multiple instances share one request):**
|
**Correct (multiple instances share one request):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import useSWR from 'swr'
|
import useSWR from "swr";
|
||||||
|
|
||||||
function UserList() {
|
function UserList() {
|
||||||
const { data: users } = useSWR('/api/users', fetcher)
|
const { data: users } = useSWR("/api/users", fetcher);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**For immutable data:**
|
**For immutable data:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useImmutableSWR } from '@/lib/swr'
|
import { useImmutableSWR } from "@/lib/swr";
|
||||||
|
|
||||||
function StaticContent() {
|
function StaticContent() {
|
||||||
const { data } = useImmutableSWR('/api/config', fetcher)
|
const { data } = useImmutableSWR("/api/config", fetcher);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**For mutations:**
|
**For mutations:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useSWRMutation } from 'swr/mutation'
|
import { useSWRMutation } from "swr/mutation";
|
||||||
|
|
||||||
function UpdateButton() {
|
function UpdateButton() {
|
||||||
const { trigger } = useSWRMutation('/api/user', updateUser)
|
const { trigger } = useSWRMutation("/api/user", updateUser);
|
||||||
return <button onClick={() => trigger()}>Update</button>
|
return <button onClick={() => trigger()}>Update</button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ Avoid interleaving style writes with layout reads. When you read a layout proper
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
element.style.width = '100px'
|
element.style.width = "100px";
|
||||||
const width = element.offsetWidth // Forces reflow
|
const width = element.offsetWidth; // Forces reflow
|
||||||
element.style.height = '200px'
|
element.style.height = "200px";
|
||||||
const height = element.offsetHeight // Forces another reflow
|
const height = element.offsetHeight; // Forces another reflow
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -25,13 +25,13 @@ function updateElementStyles(element: HTMLElement) {
|
|||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
// Batch all writes together
|
// Batch all writes together
|
||||||
element.style.width = '100px'
|
element.style.width = "100px";
|
||||||
element.style.height = '200px'
|
element.style.height = "200px";
|
||||||
element.style.backgroundColor = 'blue'
|
element.style.backgroundColor = "blue";
|
||||||
element.style.border = '1px solid black'
|
element.style.border = "1px solid black";
|
||||||
|
|
||||||
// Read after all writes are done (single reflow)
|
// Read after all writes are done (single reflow)
|
||||||
const { width, height } = element.getBoundingClientRect()
|
const { width, height } = element.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -48,9 +48,9 @@ function updateElementStyles(element: HTMLElement) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
element.classList.add('highlighted-box')
|
element.classList.add("highlighted-box");
|
||||||
|
|
||||||
const { width, height } = element.getBoundingClientRect()
|
const { width, height } = element.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -11,67 +11,67 @@ Use a module-level Map to cache function results when the same function is calle
|
|||||||
|
|
||||||
**Incorrect (redundant computation):**
|
**Incorrect (redundant computation):**
|
||||||
|
|
||||||
```typescript
|
```tsx
|
||||||
function ProjectList({ projects }: { projects: Project[] }) {
|
function ProjectList({ projects }: { projects: Project[] }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{projects.map(project => {
|
{projects.map((project) => {
|
||||||
// slugify() called 100+ times for same project names
|
// slugify() called 100+ times for same project names
|
||||||
const slug = slugify(project.name)
|
const slug = slugify(project.name);
|
||||||
|
|
||||||
return <ProjectCard key={project.id} slug={slug} />
|
return <ProjectCard key={project.id} slug={slug} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (cached results):**
|
**Correct (cached results):**
|
||||||
|
|
||||||
```typescript
|
```tsx
|
||||||
// Module-level cache
|
// Module-level cache
|
||||||
const slugifyCache = new Map<string, string>()
|
const slugifyCache = new Map<string, string>();
|
||||||
|
|
||||||
function cachedSlugify(text: string): string {
|
function cachedSlugify(text: string): string {
|
||||||
if (slugifyCache.has(text)) {
|
if (slugifyCache.has(text)) {
|
||||||
return slugifyCache.get(text)!
|
return slugifyCache.get(text)!;
|
||||||
}
|
}
|
||||||
const result = slugify(text)
|
const result = slugify(text);
|
||||||
slugifyCache.set(text, result)
|
slugifyCache.set(text, result);
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectList({ projects }: { projects: Project[] }) {
|
function ProjectList({ projects }: { projects: Project[] }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{projects.map(project => {
|
{projects.map((project) => {
|
||||||
// Computed only once per unique project name
|
// Computed only once per unique project name
|
||||||
const slug = cachedSlugify(project.name)
|
const slug = cachedSlugify(project.name);
|
||||||
|
|
||||||
return <ProjectCard key={project.id} slug={slug} />
|
return <ProjectCard key={project.id} slug={slug} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Simpler pattern for single-value functions:**
|
**Simpler pattern for single-value functions:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
let isLoggedInCache: boolean | null = null
|
let isLoggedInCache: boolean | null = null;
|
||||||
|
|
||||||
function isLoggedIn(): boolean {
|
function isLoggedIn(): boolean {
|
||||||
if (isLoggedInCache !== null) {
|
if (isLoggedInCache !== null) {
|
||||||
return isLoggedInCache
|
return isLoggedInCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedInCache = document.cookie.includes('auth=')
|
isLoggedInCache = document.cookie.includes("auth=");
|
||||||
return isLoggedInCache
|
return isLoggedInCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cache when auth changes
|
// Clear cache when auth changes
|
||||||
function onAuthChange() {
|
function onAuthChange() {
|
||||||
isLoggedInCache = null
|
isLoggedInCache = null;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ Cache object property lookups in hot paths.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
for (let i = 0; i < arr.length; i++) {
|
for (let i = 0; i < arr.length; i++) {
|
||||||
process(obj.config.settings.value)
|
process(obj.config.settings.value);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (1 lookup total):**
|
**Correct (1 lookup total):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const value = obj.config.settings.value
|
const value = obj.config.settings.value;
|
||||||
const len = arr.length
|
const len = arr.length;
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
process(value)
|
process(value);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ tags: javascript, localStorage, storage, caching, performance
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getTheme() {
|
function getTheme() {
|
||||||
return localStorage.getItem('theme') ?? 'light'
|
return localStorage.getItem("theme") ?? "light";
|
||||||
}
|
}
|
||||||
// Called 10 times = 10 storage reads
|
// Called 10 times = 10 storage reads
|
||||||
```
|
```
|
||||||
@@ -21,18 +21,18 @@ function getTheme() {
|
|||||||
**Correct (Map cache):**
|
**Correct (Map cache):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const storageCache = new Map<string, string | null>()
|
const storageCache = new Map<string, string | null>();
|
||||||
|
|
||||||
function getLocalStorage(key: string) {
|
function getLocalStorage(key: string) {
|
||||||
if (!storageCache.has(key)) {
|
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) {
|
function setLocalStorage(key: string, value: string) {
|
||||||
localStorage.setItem(key, value)
|
localStorage.setItem(key, value);
|
||||||
storageCache.set(key, value) // keep cache in sync
|
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:**
|
**Cookie caching:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
let cookieCache: Record<string, string> | null = null
|
let cookieCache: Record<string, string> | null = null;
|
||||||
|
|
||||||
function getCookie(name: string) {
|
function getCookie(name: string) {
|
||||||
if (!cookieCache) {
|
if (!cookieCache) {
|
||||||
cookieCache = Object.fromEntries(
|
cookieCache = Object.fromEntries(document.cookie.split("; ").map((c) => c.split("=")));
|
||||||
document.cookie.split('; ').map(c => c.split('='))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return cookieCache[name]
|
return cookieCache[name];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -58,13 +56,13 @@ function getCookie(name: string) {
|
|||||||
If storage can change externally (another tab, server-set cookies), invalidate cache:
|
If storage can change externally (another tab, server-set cookies), invalidate cache:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
window.addEventListener('storage', (e) => {
|
window.addEventListener("storage", (e) => {
|
||||||
if (e.key) storageCache.delete(e.key)
|
if (e.key) storageCache.delete(e.key);
|
||||||
})
|
});
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener("visibilitychange", () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === "visible") {
|
||||||
storageCache.clear()
|
storageCache.clear();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,21 +12,21 @@ Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine
|
|||||||
**Incorrect (3 iterations):**
|
**Incorrect (3 iterations):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const admins = users.filter(u => u.isAdmin)
|
const admins = users.filter((u) => u.isAdmin);
|
||||||
const testers = users.filter(u => u.isTester)
|
const testers = users.filter((u) => u.isTester);
|
||||||
const inactive = users.filter(u => !u.isActive)
|
const inactive = users.filter((u) => !u.isActive);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (1 iteration):**
|
**Correct (1 iteration):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const admins: User[] = []
|
const admins: User[] = [];
|
||||||
const testers: User[] = []
|
const testers: User[] = [];
|
||||||
const inactive: User[] = []
|
const inactive: User[] = [];
|
||||||
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (user.isAdmin) admins.push(user)
|
if (user.isAdmin) admins.push(user);
|
||||||
if (user.isTester) testers.push(user)
|
if (user.isTester) testers.push(user);
|
||||||
if (!user.isActive) inactive.push(user)
|
if (!user.isActive) inactive.push(user);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,22 +13,22 @@ Return early when result is determined to skip unnecessary processing.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function validateUsers(users: User[]) {
|
function validateUsers(users: User[]) {
|
||||||
let hasError = false
|
let hasError = false;
|
||||||
let errorMessage = ''
|
let errorMessage = "";
|
||||||
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (!user.email) {
|
if (!user.email) {
|
||||||
hasError = true
|
hasError = true;
|
||||||
errorMessage = 'Email required'
|
errorMessage = "Email required";
|
||||||
}
|
}
|
||||||
if (!user.name) {
|
if (!user.name) {
|
||||||
hasError = true
|
hasError = true;
|
||||||
errorMessage = 'Name required'
|
errorMessage = "Name required";
|
||||||
}
|
}
|
||||||
// Continues checking all users even after error found
|
// 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[]) {
|
function validateUsers(users: User[]) {
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (!user.email) {
|
if (!user.email) {
|
||||||
return { valid: false, error: 'Email required' }
|
return { valid: false, error: "Email required" };
|
||||||
}
|
}
|
||||||
if (!user.name) {
|
if (!user.name) {
|
||||||
return { valid: false, error: 'Name required' }
|
return { valid: false, error: "Name required" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true }
|
return { valid: true };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,24 +13,21 @@ Don't create RegExp inside render. Hoist to module scope or memoize with `useMem
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Highlighter({ text, query }: Props) {
|
function Highlighter({ text, query }: Props) {
|
||||||
const regex = new RegExp(`(${query})`, 'gi')
|
const regex = new RegExp(`(${query})`, "gi");
|
||||||
const parts = text.split(regex)
|
const parts = text.split(regex);
|
||||||
return <>{parts.map((part, i) => ...)}</>
|
return <>{parts.map((part, i) => part)}</>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (memoize or hoist):**
|
**Correct (memoize or hoist):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
function Highlighter({ text, query }: Props) {
|
function Highlighter({ text, query }: Props) {
|
||||||
const regex = useMemo(
|
const regex = useMemo(() => new RegExp(`(${escapeRegex(query)})`, "gi"), [query]);
|
||||||
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
|
const parts = text.split(regex);
|
||||||
[query]
|
return <>{parts.map((part, i) => part)}</>;
|
||||||
)
|
|
||||||
const parts = text.split(regex)
|
|
||||||
return <>{parts.map((part, i) => ...)}</>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -39,7 +36,7 @@ function Highlighter({ text, query }: Props) {
|
|||||||
Global regex (`/g`) has mutable `lastIndex` state:
|
Global regex (`/g`) has mutable `lastIndex` state:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const regex = /foo/g
|
const regex = /foo/g;
|
||||||
regex.test('foo') // true, lastIndex = 3
|
regex.test("foo"); // true, lastIndex = 3
|
||||||
regex.test('foo') // false, lastIndex = 0
|
regex.test("foo"); // false, lastIndex = 0
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ Multiple `.find()` calls by the same key should use a Map.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function processOrders(orders: Order[], users: User[]) {
|
function processOrders(orders: Order[], users: User[]) {
|
||||||
return orders.map(order => ({
|
return orders.map((order) => ({
|
||||||
...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
|
```typescript
|
||||||
function processOrders(orders: Order[], users: User[]) {
|
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,
|
...order,
|
||||||
user: userById.get(order.userId)
|
user: userById.get(order.userId),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ In real-world applications, this optimization is especially valuable when the co
|
|||||||
```typescript
|
```typescript
|
||||||
function hasChanges(current: string[], original: string[]) {
|
function hasChanges(current: string[], original: string[]) {
|
||||||
// Always sorts and joins, even when lengths differ
|
// 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[]) {
|
function hasChanges(current: string[], original: string[]) {
|
||||||
// Early return if lengths differ
|
// Early return if lengths differ
|
||||||
if (current.length !== original.length) {
|
if (current.length !== original.length) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
// Only sort when lengths match
|
// Only sort 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++) {
|
||||||
if (currentSorted[i] !== originalSorted[i]) {
|
if (currentSorted[i] !== originalSorted[i]) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This new approach is more efficient because:
|
This new approach is more efficient because:
|
||||||
|
|
||||||
- It avoids the overhead of sorting and joining the arrays when lengths differ
|
- 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 consuming memory for the joined strings (especially important for large arrays)
|
||||||
- It avoids mutating the original arrays
|
- It avoids mutating the original arrays
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ Finding the smallest or largest element only requires a single pass through the
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Project {
|
interface Project {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
updatedAt: number
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestProject(projects: Project[]) {
|
function getLatestProject(projects: Project[]) {
|
||||||
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
|
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||||
return sorted[0]
|
return sorted[0];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Sorts the entire array just to find the maximum value.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getOldestAndNewest(projects: Project[]) {
|
function getOldestAndNewest(projects: Project[]) {
|
||||||
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
|
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
|
||||||
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
|
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -41,31 +41,31 @@ Still sorts unnecessarily when only min/max are needed.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getLatestProject(projects: Project[]) {
|
function getLatestProject(projects: Project[]) {
|
||||||
if (projects.length === 0) return null
|
if (projects.length === 0) return null;
|
||||||
|
|
||||||
let latest = projects[0]
|
let latest = projects[0];
|
||||||
|
|
||||||
for (let i = 1; i < projects.length; i++) {
|
for (let i = 1; i < projects.length; i++) {
|
||||||
if (projects[i].updatedAt > latest.updatedAt) {
|
if (projects[i].updatedAt > latest.updatedAt) {
|
||||||
latest = projects[i]
|
latest = projects[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return latest
|
return latest;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOldestAndNewest(projects: Project[]) {
|
function getOldestAndNewest(projects: Project[]) {
|
||||||
if (projects.length === 0) return { oldest: null, newest: null }
|
if (projects.length === 0) return { oldest: null, newest: null };
|
||||||
|
|
||||||
let oldest = projects[0]
|
let oldest = projects[0];
|
||||||
let newest = projects[0]
|
let newest = projects[0];
|
||||||
|
|
||||||
for (let i = 1; i < projects.length; i++) {
|
for (let i = 1; i < projects.length; i++) {
|
||||||
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
|
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
|
||||||
if (projects[i].updatedAt > newest.updatedAt) newest = 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):**
|
**Alternative (Math.min/Math.max for small arrays):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const numbers = [5, 2, 8, 1, 9]
|
const numbers = [5, 2, 8, 1, 9];
|
||||||
const min = Math.min(...numbers)
|
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 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.
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ Convert arrays to Set/Map for repeated membership checks.
|
|||||||
**Incorrect (O(n) per check):**
|
**Incorrect (O(n) per check):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const allowedIds = ['a', 'b', 'c', ...]
|
const allowedIds = ["a", "b", "c"];
|
||||||
items.filter(item => allowedIds.includes(item.id))
|
items.filter((item) => allowedIds.includes(item.id));
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (O(1) per check):**
|
**Correct (O(1) per check):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const allowedIds = new Set(['a', 'b', 'c', ...])
|
const allowedIds = new Set(["a", "b", "c"]);
|
||||||
items.filter(item => allowedIds.has(item.id))
|
items.filter((item) => allowedIds.has(item.id));
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,27 +11,21 @@ tags: javascript, arrays, immutability, react, state, mutation
|
|||||||
|
|
||||||
**Incorrect (mutates original array):**
|
**Incorrect (mutates original array):**
|
||||||
|
|
||||||
```typescript
|
```tsx
|
||||||
function UserList({ users }: { users: User[] }) {
|
function UserList({ users }: { users: User[] }) {
|
||||||
// Mutates the users prop array!
|
// Mutates the users prop array!
|
||||||
const sorted = useMemo(
|
const sorted = useMemo(() => users.sort((a, b) => a.name.localeCompare(b.name)), [users]);
|
||||||
() => users.sort((a, b) => a.name.localeCompare(b.name)),
|
return <div>{sorted.map(renderUser)}</div>;
|
||||||
[users]
|
|
||||||
)
|
|
||||||
return <div>{sorted.map(renderUser)}</div>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (creates new array):**
|
**Correct (creates new array):**
|
||||||
|
|
||||||
```typescript
|
```tsx
|
||||||
function UserList({ users }: { users: User[] }) {
|
function UserList({ users }: { users: User[] }) {
|
||||||
// Creates new sorted array, original unchanged
|
// Creates new sorted array, original unchanged
|
||||||
const sorted = useMemo(
|
const sorted = useMemo(() => users.toSorted((a, b) => a.name.localeCompare(b.name)), [users]);
|
||||||
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
|
return <div>{sorted.map(renderUser)}</div>;
|
||||||
[users]
|
|
||||||
)
|
|
||||||
return <div>{sorted.map(renderUser)}</div>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -46,7 +40,7 @@ function UserList({ users }: { users: User[] }) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Fallback for older browsers
|
// 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:**
|
**Other immutable array methods:**
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ Use React's `<Activity>` to preserve state/DOM for expensive components that fre
|
|||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Activity } from 'react'
|
import { Activity } from "react";
|
||||||
|
|
||||||
function Dropdown({ isOpen }: Props) {
|
function Dropdown({ isOpen }: Props) {
|
||||||
return (
|
return (
|
||||||
<Activity mode={isOpen ? 'visible' : 'hidden'}>
|
<Activity mode={isOpen ? "visible" : "hidden"}>
|
||||||
<ExpensiveMenu />
|
<ExpensiveMenu />
|
||||||
</Activity>
|
</Activity>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ Many browsers don't have hardware acceleration for CSS3 animations on SVG elemen
|
|||||||
```tsx
|
```tsx
|
||||||
function LoadingSpinner() {
|
function LoadingSpinner() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
|
||||||
className="animate-spin"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,15 +27,11 @@ function LoadingSpinner() {
|
|||||||
function LoadingSpinner() {
|
function LoadingSpinner() {
|
||||||
return (
|
return (
|
||||||
<div className="animate-spin">
|
<div className="animate-spin">
|
||||||
<svg
|
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Badge({ count }: { count: number }) {
|
function Badge({ count }: { count: number }) {
|
||||||
return (
|
return <div>{count && <span className="badge">{count}</span>}</div>;
|
||||||
<div>
|
|
||||||
{count && <span className="badge">{count}</span>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When count = 0, renders: <div>0</div>
|
// When count = 0, renders: <div>0</div>
|
||||||
@@ -28,11 +24,7 @@ function Badge({ count }: { count: number }) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Badge({ count }: { count: number }) {
|
function Badge({ count }: { count: number }) {
|
||||||
return (
|
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
|
||||||
<div>
|
|
||||||
{count > 0 ? <span className="badge">{count}</span> : null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When count = 0, renders: <div></div>
|
// When count = 0, renders: <div></div>
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ Apply `content-visibility: auto` to defer off-screen rendering.
|
|||||||
function MessageList({ messages }: { messages: Message[] }) {
|
function MessageList({ messages }: { messages: Message[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-y-auto h-screen">
|
<div className="overflow-y-auto h-screen">
|
||||||
{messages.map(msg => (
|
{messages.map((msg) => (
|
||||||
<div key={msg.id} className="message-item">
|
<div key={msg.id} className="message-item">
|
||||||
<Avatar user={msg.author} />
|
<Avatar user={msg.author} />
|
||||||
<div>{msg.content}</div>
|
<div>{msg.content}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,31 +13,21 @@ Extract static JSX outside components to avoid re-creation.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function LoadingSkeleton() {
|
function LoadingSkeleton() {
|
||||||
return <div className="animate-pulse h-20 bg-gray-200" />
|
return <div className="animate-pulse h-20 bg-gray-200" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Container() {
|
function Container() {
|
||||||
return (
|
return <div>{loading && <LoadingSkeleton />}</div>;
|
||||||
<div>
|
|
||||||
{loading && <LoadingSkeleton />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (reuses same element):**
|
**Correct (reuses same element):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const loadingSkeleton = (
|
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
|
||||||
<div className="animate-pulse h-20 bg-gray-200" />
|
|
||||||
)
|
|
||||||
|
|
||||||
function Container() {
|
function Container() {
|
||||||
return (
|
return <div>{loading && loadingSkeleton}</div>;
|
||||||
<div>
|
|
||||||
{loading && loadingSkeleton}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,9 @@ When rendering content that depends on client-side storage (localStorage, cookie
|
|||||||
```tsx
|
```tsx
|
||||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||||
// localStorage is not available on server - throws error
|
// localStorage is not available on server - throws error
|
||||||
const theme = localStorage.getItem('theme') || 'light'
|
const theme = localStorage.getItem("theme") || "light";
|
||||||
|
|
||||||
return (
|
return <div className={theme}>{children}</div>;
|
||||||
<div className={theme}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,21 +26,17 @@ Server-side rendering will fail because `localStorage` is undefined.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||||
const [theme, setTheme] = useState('light')
|
const [theme, setTheme] = useState("light");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Runs after hydration - causes visible flash
|
// Runs after hydration - causes visible flash
|
||||||
const stored = localStorage.getItem('theme')
|
const stored = localStorage.getItem("theme");
|
||||||
if (stored) {
|
if (stored) {
|
||||||
setTheme(stored)
|
setTheme(stored);
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return <div className={theme}>{children}</div>;
|
||||||
<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 }) {
|
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="theme-wrapper">
|
<div id="theme-wrapper">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
@@ -73,7 +63,7 @@ function ThemeWrapper({ children }: { children: ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ShareButton({ chatId }: { chatId: string }) {
|
function ShareButton({ chatId }: { chatId: string }) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
const ref = searchParams.get('ref')
|
const ref = searchParams.get("ref");
|
||||||
shareChat(chatId, { 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
|
```tsx
|
||||||
function ShareButton({ chatId }: { chatId: string }) {
|
function ShareButton({ chatId }: { chatId: string }) {
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search);
|
||||||
const ref = params.get('ref')
|
const ref = params.get("ref");
|
||||||
shareChat(chatId, { ref })
|
shareChat(chatId, { ref });
|
||||||
}
|
};
|
||||||
|
|
||||||
return <button onClick={handleShare}>Share</button>
|
return <button onClick={handleShare}>Share</button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ Specify primitive dependencies instead of objects to minimize effect re-runs.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(user.id)
|
console.log(user.id);
|
||||||
}, [user])
|
}, [user]);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (re-runs only when id changes):**
|
**Correct (re-runs only when id changes):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(user.id)
|
console.log(user.id);
|
||||||
}, [user.id])
|
}, [user.id]);
|
||||||
```
|
```
|
||||||
|
|
||||||
**For derived state, compute outside effect:**
|
**For derived state, compute outside effect:**
|
||||||
@@ -31,15 +31,15 @@ useEffect(() => {
|
|||||||
// Incorrect: runs on width=767, 766, 765...
|
// Incorrect: runs on width=767, 766, 765...
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (width < 768) {
|
if (width < 768) {
|
||||||
enableMobileMode()
|
enableMobileMode();
|
||||||
}
|
}
|
||||||
}, [width])
|
}, [width]);
|
||||||
|
|
||||||
// Correct: runs only on boolean transition
|
// Correct: runs only on boolean transition
|
||||||
const isMobile = width < 768
|
const isMobile = width < 768;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
enableMobileMode()
|
enableMobileMode();
|
||||||
}
|
}
|
||||||
}, [isMobile])
|
}, [isMobile]);
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ Subscribe to derived boolean state instead of continuous values to reduce re-ren
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const width = useWindowWidth() // updates continuously
|
const width = useWindowWidth(); // updates continuously
|
||||||
const isMobile = width < 768
|
const isMobile = width < 768;
|
||||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
return <nav className={isMobile ? "mobile" : "desktop"} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ function Sidebar() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const isMobile = useMediaQuery('(max-width: 767px)')
|
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
return <nav className={isMobile ? "mobile" : "desktop"} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,19 +13,22 @@ When updating state based on the current state value, use the functional update
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function TodoList() {
|
function TodoList() {
|
||||||
const [items, setItems] = useState(initialItems)
|
const [items, setItems] = useState(initialItems);
|
||||||
|
|
||||||
// Callback must depend on items, recreated on every items change
|
// Callback must depend on items, recreated on every items change
|
||||||
const addItems = useCallback((newItems: Item[]) => {
|
const addItems = useCallback(
|
||||||
setItems([...items, ...newItems])
|
(newItems: Item[]) => {
|
||||||
}, [items]) // ❌ items dependency causes recreations
|
setItems([...items, ...newItems]);
|
||||||
|
},
|
||||||
|
[items]
|
||||||
|
); // ❌ items dependency causes recreations
|
||||||
|
|
||||||
// Risk of stale closure if dependency is forgotten
|
// Risk of stale closure if dependency is forgotten
|
||||||
const removeItem = useCallback((id: string) => {
|
const removeItem = useCallback((id: string) => {
|
||||||
setItems(items.filter(item => item.id !== id))
|
setItems(items.filter((item) => item.id !== id));
|
||||||
}, []) // ❌ Missing items dependency - will use stale items!
|
}, []); // ❌ Missing items dependency - will use stale items!
|
||||||
|
|
||||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
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
|
```tsx
|
||||||
function TodoList() {
|
function TodoList() {
|
||||||
const [items, setItems] = useState(initialItems)
|
const [items, setItems] = useState(initialItems);
|
||||||
|
|
||||||
// Stable callback, never recreated
|
// Stable callback, never recreated
|
||||||
const addItems = useCallback((newItems: Item[]) => {
|
const addItems = useCallback((newItems: Item[]) => {
|
||||||
setItems(curr => [...curr, ...newItems])
|
setItems((curr) => [...curr, ...newItems]);
|
||||||
}, []) // ✅ No dependencies needed
|
}, []); // ✅ No dependencies needed
|
||||||
|
|
||||||
// Always uses latest state, no stale closure risk
|
// Always uses latest state, no stale closure risk
|
||||||
const removeItem = useCallback((id: string) => {
|
const removeItem = useCallback((id: string) => {
|
||||||
setItems(curr => curr.filter(item => item.id !== id))
|
setItems((curr) => curr.filter((item) => item.id !== id));
|
||||||
}, []) // ✅ Safe and stable
|
}, []); // ✅ Safe and stable
|
||||||
|
|
||||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,20 +14,18 @@ Pass a function to `useState` for expensive initial values. Without the function
|
|||||||
```tsx
|
```tsx
|
||||||
function FilteredList({ items }: { items: Item[] }) {
|
function FilteredList({ items }: { items: Item[] }) {
|
||||||
// buildSearchIndex() runs on EVERY render, even after initialization
|
// buildSearchIndex() runs on EVERY render, even after initialization
|
||||||
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
|
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
// When query changes, buildSearchIndex runs again unnecessarily
|
// When query changes, buildSearchIndex runs again unnecessarily
|
||||||
return <SearchResults index={searchIndex} query={query} />
|
return <SearchResults index={searchIndex} query={query} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserProfile() {
|
function UserProfile() {
|
||||||
// JSON.parse runs on every render
|
// JSON.parse runs on every render
|
||||||
const [settings, setSettings] = useState(
|
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings") || "{}"));
|
||||||
JSON.parse(localStorage.getItem('settings') || '{}')
|
|
||||||
)
|
|
||||||
|
|
||||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,20 +34,20 @@ function UserProfile() {
|
|||||||
```tsx
|
```tsx
|
||||||
function FilteredList({ items }: { items: Item[] }) {
|
function FilteredList({ items }: { items: Item[] }) {
|
||||||
// buildSearchIndex() runs ONLY on initial render
|
// buildSearchIndex() runs ONLY on initial render
|
||||||
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
|
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
return <SearchResults index={searchIndex} query={query} />
|
return <SearchResults index={searchIndex} query={query} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserProfile() {
|
function UserProfile() {
|
||||||
// JSON.parse runs only on initial render
|
// JSON.parse runs only on initial render
|
||||||
const [settings, setSettings] = useState(() => {
|
const [settings, setSettings] = useState(() => {
|
||||||
const stored = localStorage.getItem('settings')
|
const stored = localStorage.getItem("settings");
|
||||||
return stored ? JSON.parse(stored) : {}
|
return stored ? JSON.parse(stored) : {};
|
||||||
})
|
});
|
||||||
|
|
||||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ Extract expensive work into memoized components to enable early returns before c
|
|||||||
```tsx
|
```tsx
|
||||||
function Profile({ user, loading }: Props) {
|
function Profile({ user, loading }: Props) {
|
||||||
const avatar = useMemo(() => {
|
const avatar = useMemo(() => {
|
||||||
const id = computeAvatarId(user)
|
const id = computeAvatarId(user);
|
||||||
return <Avatar id={id} />
|
return <Avatar id={id} />;
|
||||||
}, [user])
|
}, [user]);
|
||||||
|
|
||||||
if (loading) return <Skeleton />
|
if (loading) return <Skeleton />;
|
||||||
return <div>{avatar}</div>
|
return <div>{avatar}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,17 +27,17 @@ function Profile({ user, loading }: Props) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
||||||
const id = useMemo(() => computeAvatarId(user), [user])
|
const id = useMemo(() => computeAvatarId(user), [user]);
|
||||||
return <Avatar id={id} />
|
return <Avatar id={id} />;
|
||||||
})
|
});
|
||||||
|
|
||||||
function Profile({ user, loading }: Props) {
|
function Profile({ user, loading }: Props) {
|
||||||
if (loading) return <Skeleton />
|
if (loading) return <Skeleton />;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<UserAvatar user={user} />
|
<UserAvatar user={user} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,28 +13,28 @@ Mark frequent, non-urgent state updates as transitions to maintain UI responsive
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ScrollTracker() {
|
function ScrollTracker() {
|
||||||
const [scrollY, setScrollY] = useState(0)
|
const [scrollY, setScrollY] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => setScrollY(window.scrollY)
|
const handler = () => setScrollY(window.scrollY);
|
||||||
window.addEventListener('scroll', handler, { passive: true })
|
window.addEventListener("scroll", handler, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', handler)
|
return () => window.removeEventListener("scroll", handler);
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (non-blocking updates):**
|
**Correct (non-blocking updates):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { startTransition } from 'react'
|
import { startTransition } from "react";
|
||||||
|
|
||||||
function ScrollTracker() {
|
function ScrollTracker() {
|
||||||
const [scrollY, setScrollY] = useState(0)
|
const [scrollY, setScrollY] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
startTransition(() => setScrollY(window.scrollY))
|
startTransition(() => setScrollY(window.scrollY));
|
||||||
}
|
};
|
||||||
window.addEventListener('scroll', handler, { passive: true })
|
window.addEventListener("scroll", handler, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', handler)
|
return () => window.removeEventListener("scroll", handler);
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,46 +12,46 @@ Use Next.js's `after()` to schedule work that should execute after a response is
|
|||||||
**Incorrect (blocks response):**
|
**Incorrect (blocks response):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { logUserAction } from '@/app/utils'
|
import { logUserAction } from "@/app/utils";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
// Perform mutation
|
// Perform mutation
|
||||||
await updateDatabase(request)
|
await updateDatabase(request);
|
||||||
|
|
||||||
// Logging blocks the response
|
// Logging blocks the response
|
||||||
const userAgent = request.headers.get('user-agent') || 'unknown'
|
const userAgent = request.headers.get("user-agent") || "unknown";
|
||||||
await logUserAction({ userAgent })
|
await logUserAction({ userAgent });
|
||||||
|
|
||||||
return new Response(JSON.stringify({ status: 'success' }), {
|
return new Response(JSON.stringify({ status: "success" }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { "Content-Type": "application/json" },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (non-blocking):**
|
**Correct (non-blocking):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { after } from 'next/server'
|
import { after } from "next/server";
|
||||||
import { headers, cookies } from 'next/headers'
|
import { headers, cookies } from "next/headers";
|
||||||
import { logUserAction } from '@/app/utils'
|
import { logUserAction } from "@/app/utils";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
// Perform mutation
|
// Perform mutation
|
||||||
await updateDatabase(request)
|
await updateDatabase(request);
|
||||||
|
|
||||||
// Log after response is sent
|
// Log after response is sent
|
||||||
after(async () => {
|
after(async () => {
|
||||||
const userAgent = (await headers()).get('user-agent') || 'unknown'
|
const userAgent = (await headers()).get("user-agent") || "unknown";
|
||||||
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
|
const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous";
|
||||||
|
|
||||||
logUserAction({ sessionCookie, userAgent })
|
logUserAction({ sessionCookie, userAgent });
|
||||||
})
|
});
|
||||||
|
|
||||||
return new Response(JSON.stringify({ status: 'success' }), {
|
return new Response(JSON.stringify({ status: "success" }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { "Content-Type": "application/json" },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ tags: server, cache, lru, cross-request
|
|||||||
**Implementation:**
|
**Implementation:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from "lru-cache";
|
||||||
|
|
||||||
const cache = new LRUCache<string, any>({
|
const cache = new LRUCache<string, any>({
|
||||||
max: 1000,
|
max: 1000,
|
||||||
ttl: 5 * 60 * 1000 // 5 minutes
|
ttl: 5 * 60 * 1000, // 5 minutes
|
||||||
})
|
});
|
||||||
|
|
||||||
export async function getUser(id: string) {
|
export async function getUser(id: string) {
|
||||||
const cached = cache.get(id)
|
const cached = cache.get(id);
|
||||||
if (cached) return cached
|
if (cached) return cached;
|
||||||
|
|
||||||
const user = await db.user.findUnique({ where: { id } })
|
const user = await db.user.findUnique({ where: { id } });
|
||||||
cache.set(id, user)
|
cache.set(id, user);
|
||||||
return user
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request 1: DB query, result cached
|
// Request 1: DB query, result cached
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ Use `React.cache()` for server-side request deduplication. Authentication and da
|
|||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { cache } from 'react'
|
import { cache } from "react";
|
||||||
|
|
||||||
export const getCurrentUser = cache(async () => {
|
export const getCurrentUser = cache(async () => {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
if (!session?.user?.id) return null
|
if (!session?.user?.id) return null;
|
||||||
return await db.user.findUnique({
|
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.
|
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
|
```typescript
|
||||||
const getUser = cache(async (params: { uid: number }) => {
|
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
|
// Each call creates new object, never hits cache
|
||||||
getUser({ uid: 1 })
|
getUser({ uid: 1 });
|
||||||
getUser({ uid: 1 }) // Cache miss, runs query again
|
getUser({ uid: 1 }); // Cache miss, runs query again
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (cache hit):**
|
**Correct (cache hit):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const getUser = cache(async (uid: number) => {
|
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
|
// Primitive args use value equality
|
||||||
getUser(1)
|
getUser(1);
|
||||||
getUser(1) // Cache hit, returns cached result
|
getUser(1); // Cache hit, returns cached result
|
||||||
```
|
```
|
||||||
|
|
||||||
If you must pass objects, pass the same reference:
|
If you must pass objects, pass the same reference:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const params = { uid: 1 }
|
const params = { uid: 1 };
|
||||||
getUser(params) // Query runs
|
getUser(params); // Query runs
|
||||||
getUser(params) // Cache hit (same reference)
|
getUser(params); // Cache hit (same reference)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Next.js-Specific Note:**
|
**Next.js-Specific Note:**
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ React Server Components execute sequentially within a tree. Restructure with com
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const header = await fetchHeader()
|
const header = await fetchHeader();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>{header}</div>
|
<div>{header}</div>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Sidebar() {
|
async function Sidebar() {
|
||||||
const items = await fetchSidebarItems()
|
const items = await fetchSidebarItems();
|
||||||
return <nav>{items.map(renderItem)}</nav>
|
return <nav>{items.map(renderItem)}</nav>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,13 +32,13 @@ async function Sidebar() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Header() {
|
async function Header() {
|
||||||
const data = await fetchHeader()
|
const data = await fetchHeader();
|
||||||
return <div>{data}</div>
|
return <div>{data}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Sidebar() {
|
async function Sidebar() {
|
||||||
const items = await fetchSidebarItems()
|
const items = await fetchSidebarItems();
|
||||||
return <nav>{items.map(renderItem)}</nav>
|
return <nav>{items.map(renderItem)}</nav>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -47,7 +47,7 @@ export default function Page() {
|
|||||||
<Header />
|
<Header />
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,13 +55,13 @@ export default function Page() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Header() {
|
async function Header() {
|
||||||
const data = await fetchHeader()
|
const data = await fetchHeader();
|
||||||
return <div>{data}</div>
|
return <div>{data}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Sidebar() {
|
async function Sidebar() {
|
||||||
const items = await fetchSidebarItems()
|
const items = await fetchSidebarItems();
|
||||||
return <nav>{items.map(renderItem)}</nav>
|
return <nav>{items.map(renderItem)}</nav>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Layout({ children }: { children: ReactNode }) {
|
function Layout({ children }: { children: ReactNode }) {
|
||||||
@@ -70,7 +70,7 @@ function Layout({ children }: { children: ReactNode }) {
|
|||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -78,6 +78,6 @@ export default function Page() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ The React Server/Client boundary serializes all object properties into strings a
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const user = await fetchUser() // 50 fields
|
const user = await fetchUser(); // 50 fields
|
||||||
return <Profile user={user} />
|
return <Profile user={user} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
'use client'
|
("use client");
|
||||||
function Profile({ user }: { user: User }) {
|
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
|
```tsx
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const user = await fetchUser()
|
const user = await fetchUser();
|
||||||
return <Profile name={user.name} />
|
return <Profile name={user.name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
'use client'
|
("use client");
|
||||||
function Profile({ name }: { name: string }) {
|
function Profile({ name }: { name: string }) {
|
||||||
return <div>{name}</div>
|
return <div>{name}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ Use WebFetch to retrieve the latest rules. The fetched content contains all the
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
When a user provides a file or pattern argument:
|
When a user provides a file or pattern argument:
|
||||||
|
|
||||||
1. Fetch guidelines from the source URL above
|
1. Fetch guidelines from the source URL above
|
||||||
2. Read the specified files
|
2. Read the specified files
|
||||||
3. Apply all rules from the fetched guidelines
|
3. Apply all rules from the fetched guidelines
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ Comprehensive performance optimization guide for React and Next.js applications,
|
|||||||
## When to Apply
|
## When to Apply
|
||||||
|
|
||||||
Reference these guidelines when:
|
Reference these guidelines when:
|
||||||
|
|
||||||
- Writing new React components or Next.js pages
|
- Writing new React components or Next.js pages
|
||||||
- Implementing data fetching (client or server-side)
|
- Implementing data fetching (client or server-side)
|
||||||
- Reviewing code for performance issues
|
- Reviewing code for performance issues
|
||||||
@@ -22,16 +23,16 @@ Reference these guidelines when:
|
|||||||
|
|
||||||
## Rule Categories by Priority
|
## Rule Categories by Priority
|
||||||
|
|
||||||
| Priority | Category | Impact | Prefix |
|
| Priority | Category | Impact | Prefix |
|
||||||
|----------|----------|--------|--------|
|
| -------- | ------------------------- | ----------- | ------------ |
|
||||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
@@ -115,6 +116,7 @@ rules/_sections.md
|
|||||||
```
|
```
|
||||||
|
|
||||||
Each rule file contains:
|
Each rule file contains:
|
||||||
|
|
||||||
- Brief explanation of why it matters
|
- Brief explanation of why it matters
|
||||||
- Incorrect code example with explanation
|
- Incorrect code example with explanation
|
||||||
- Correct code example with explanation
|
- Correct code example with explanation
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
|
|||||||
```tsx
|
```tsx
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: (e) => void) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(event, handler)
|
window.addEventListener(event, handler);
|
||||||
return () => window.removeEventListener(event, handler)
|
return () => window.removeEventListener(event, handler);
|
||||||
}, [event, handler])
|
}, [event, handler]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -24,31 +24,31 @@ function useWindowEvent(event: string, handler: (e) => void) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: (e) => void) {
|
||||||
const handlerRef = useRef(handler)
|
const handlerRef = useRef(handler);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handlerRef.current = handler
|
handlerRef.current = handler;
|
||||||
}, [handler])
|
}, [handler]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (e) => handlerRef.current(e)
|
const listener = (e) => handlerRef.current(e);
|
||||||
window.addEventListener(event, listener)
|
window.addEventListener(event, listener);
|
||||||
return () => window.removeEventListener(event, listener)
|
return () => window.removeEventListener(event, listener);
|
||||||
}, [event])
|
}, [event]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Alternative: use `useEffectEvent` if you're on latest React:**
|
**Alternative: use `useEffectEvent` if you're on latest React:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useEffectEvent } from 'react'
|
import { useEffectEvent } from "react";
|
||||||
|
|
||||||
function useWindowEvent(event: string, handler: (e) => void) {
|
function useWindowEvent(event: string, handler: (e) => void) {
|
||||||
const onEvent = useEffectEvent(handler)
|
const onEvent = useEffectEvent(handler);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener(event, onEvent)
|
window.addEventListener(event, onEvent);
|
||||||
return () => window.removeEventListener(event, onEvent)
|
return () => window.removeEventListener(event, onEvent);
|
||||||
}, [event])
|
}, [event]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ Access latest values in callbacks without adding them to dependency arrays. Prev
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function useLatest<T>(value: T) {
|
function useLatest<T>(value: T) {
|
||||||
const ref = useRef(value)
|
const ref = useRef(value);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
ref.current = value
|
ref.current = value;
|
||||||
}, [value])
|
}, [value]);
|
||||||
return ref
|
return ref;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -25,12 +25,12 @@ function useLatest<T>(value: T) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => onSearch(query), 300)
|
const timeout = setTimeout(() => onSearch(query), 300);
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout);
|
||||||
}, [query, onSearch])
|
}, [query, onSearch]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,12 +38,12 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState("");
|
||||||
const onSearchRef = useLatest(onSearch)
|
const onSearchRef = useLatest(onSearch);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => onSearchRef.current(query), 300)
|
const timeout = setTimeout(() => onSearchRef.current(query), 300);
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout);
|
||||||
}, [query])
|
}, [query]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ In API routes and Server Actions, start independent operations immediately, even
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
const config = await fetchConfig()
|
const config = await fetchConfig();
|
||||||
const data = await fetchData(session.user.id)
|
const data = await fetchData(session.user.id);
|
||||||
return Response.json({ data, config })
|
return Response.json({ data, config });
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -24,14 +24,11 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const sessionPromise = auth()
|
const sessionPromise = auth();
|
||||||
const configPromise = fetchConfig()
|
const configPromise = fetchConfig();
|
||||||
const session = await sessionPromise
|
const session = await sessionPromise;
|
||||||
const [config, data] = await Promise.all([
|
const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);
|
||||||
configPromise,
|
return Response.json({ data, config });
|
||||||
fetchData(session.user.id)
|
|
||||||
])
|
|
||||||
return Response.json({ data, config })
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ Move `await` operations into the branches where they're actually used to avoid b
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||||
const userData = await fetchUserData(userId)
|
const userData = await fetchUserData(userId);
|
||||||
|
|
||||||
if (skipProcessing) {
|
if (skipProcessing) {
|
||||||
// Returns immediately but still waited for userData
|
// Returns immediately but still waited for userData
|
||||||
return { skipped: true }
|
return { skipped: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only this branch uses userData
|
// 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) {
|
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||||
if (skipProcessing) {
|
if (skipProcessing) {
|
||||||
// Returns immediately without waiting
|
// Returns immediately without waiting
|
||||||
return { skipped: true }
|
return { skipped: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch only when needed
|
// Fetch only when needed
|
||||||
const userData = await fetchUserData(userId)
|
const userData = await fetchUserData(userId);
|
||||||
return processUserData(userData)
|
return processUserData(userData);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,35 +45,35 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
|
|||||||
```typescript
|
```typescript
|
||||||
// Incorrect: always fetches permissions
|
// Incorrect: always fetches permissions
|
||||||
async function updateResource(resourceId: string, userId: string) {
|
async function updateResource(resourceId: string, userId: string) {
|
||||||
const permissions = await fetchPermissions(userId)
|
const permissions = await fetchPermissions(userId);
|
||||||
const resource = await getResource(resourceId)
|
const resource = await getResource(resourceId);
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return { error: 'Not found' }
|
return { error: "Not found" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!permissions.canEdit) {
|
if (!permissions.canEdit) {
|
||||||
return { error: 'Forbidden' }
|
return { error: "Forbidden" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return await updateResourceData(resource, permissions)
|
return await updateResourceData(resource, permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Correct: fetches only when needed
|
// Correct: fetches only when needed
|
||||||
async function updateResource(resourceId: string, userId: string) {
|
async function updateResource(resourceId: string, userId: string) {
|
||||||
const resource = await getResource(resourceId)
|
const resource = await getResource(resourceId);
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return { error: 'Not found' }
|
return { error: "Not found" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = await fetchPermissions(userId)
|
const permissions = await fetchPermissions(userId);
|
||||||
|
|
||||||
if (!permissions.canEdit) {
|
if (!permissions.canEdit) {
|
||||||
return { error: 'Forbidden' }
|
return { error: "Forbidden" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return await updateResourceData(resource, permissions)
|
return await updateResourceData(resource, permissions);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,25 +12,26 @@ For operations with partial dependencies, use `better-all` to maximize paralleli
|
|||||||
**Incorrect (profile waits for config unnecessarily):**
|
**Incorrect (profile waits for config unnecessarily):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const [user, config] = await Promise.all([
|
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
|
||||||
fetchUser(),
|
const profile = await fetchProfile(user.id);
|
||||||
fetchConfig()
|
|
||||||
])
|
|
||||||
const profile = await fetchProfile(user.id)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (config and profile run in parallel):**
|
**Correct (config and profile run in parallel):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { all } from 'better-all'
|
import { all } from "better-all";
|
||||||
|
|
||||||
const { user, config, profile } = await all({
|
const { user, config, profile } = await all({
|
||||||
async user() { return fetchUser() },
|
async user() {
|
||||||
async config() { return fetchConfig() },
|
return fetchUser();
|
||||||
|
},
|
||||||
|
async config() {
|
||||||
|
return fetchConfig();
|
||||||
|
},
|
||||||
async profile() {
|
async profile() {
|
||||||
return fetchProfile((await this.$.user).id)
|
return fetchProfile((await this.$.user).id);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -12,17 +12,13 @@ When async operations have no interdependencies, execute them concurrently using
|
|||||||
**Incorrect (sequential execution, 3 round trips):**
|
**Incorrect (sequential execution, 3 round trips):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const user = await fetchUser()
|
const user = await fetchUser();
|
||||||
const posts = await fetchPosts()
|
const posts = await fetchPosts();
|
||||||
const comments = await fetchComments()
|
const comments = await fetchComments();
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (parallel execution, 1 round trip):**
|
**Correct (parallel execution, 1 round trip):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const [user, posts, comments] = await Promise.all([
|
const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);
|
||||||
fetchUser(),
|
|
||||||
fetchPosts(),
|
|
||||||
fetchComments()
|
|
||||||
])
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Instead of awaiting data in async components before returning JSX, use Suspense
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const data = await fetchData() // Blocks entire page
|
const data = await fetchData(); // Blocks entire page
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -24,7 +24,7 @@ async function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<div>Footer</div>
|
<div>Footer</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,12 +45,12 @@ function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<div>Footer</div>
|
<div>Footer</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function DataDisplay() {
|
async function DataDisplay() {
|
||||||
const data = await fetchData() // Only blocks this component
|
const data = await fetchData(); // Only blocks this component
|
||||||
return <div>{data.content}</div>
|
return <div>{data.content}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
|
|||||||
```tsx
|
```tsx
|
||||||
function Page() {
|
function Page() {
|
||||||
// Start fetch immediately, but don't await
|
// Start fetch immediately, but don't await
|
||||||
const dataPromise = fetchData()
|
const dataPromise = fetchData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -73,17 +73,17 @@ function Page() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
<div>Footer</div>
|
<div>Footer</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||||
const data = use(dataPromise) // Unwraps the promise
|
const data = use(dataPromise); // Unwraps the promise
|
||||||
return <div>{data.content}</div>
|
return <div>{data.content}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||||
const data = use(dataPromise) // Reuses the same promise
|
const data = use(dataPromise); // Reuses the same promise
|
||||||
return <div>{data.summary}</div>
|
return <div>{data.summary}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,24 +16,24 @@ Popular icon and component libraries can have **up to 10,000 re-exports** in the
|
|||||||
**Incorrect (imports entire library):**
|
**Incorrect (imports entire library):**
|
||||||
|
|
||||||
```tsx
|
```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
|
// Loads 1,583 modules, takes ~2.8s extra in dev
|
||||||
// Runtime cost: 200-800ms on every cold start
|
// 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
|
// Loads 2,225 modules, takes ~4.2s extra in dev
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (imports only what you need):**
|
**Correct (imports only what you need):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import Check from 'lucide-react/dist/esm/icons/check'
|
import Check from "lucide-react/dist/esm/icons/check";
|
||||||
import X from 'lucide-react/dist/esm/icons/x'
|
import X from "lucide-react/dist/esm/icons/x";
|
||||||
import Menu from 'lucide-react/dist/esm/icons/menu'
|
import Menu from "lucide-react/dist/esm/icons/menu";
|
||||||
// Loads only 3 modules (~2KB vs ~1MB)
|
// Loads only 3 modules (~2KB vs ~1MB)
|
||||||
|
|
||||||
import Button from '@mui/material/Button'
|
import Button from "@mui/material/Button";
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from "@mui/material/TextField";
|
||||||
// Loads only what you use
|
// Loads only what you use
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -43,12 +43,12 @@ import TextField from '@mui/material/TextField'
|
|||||||
// next.config.js - use optimizePackageImports
|
// next.config.js - use optimizePackageImports
|
||||||
module.exports = {
|
module.exports = {
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizePackageImports: ['lucide-react', '@mui/material']
|
optimizePackageImports: ["lucide-react", "@mui/material"],
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// Then you can keep the ergonomic barrel imports:
|
// 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
|
// Automatically transformed to direct imports at build time
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,19 +12,25 @@ Load large data or modules only when a feature is activated.
|
|||||||
**Example (lazy-load animation frames):**
|
**Example (lazy-load animation frames):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
|
function AnimationPlayer({
|
||||||
const [frames, setFrames] = useState<Frame[] | null>(null)
|
enabled,
|
||||||
|
setEnabled,
|
||||||
|
}: {
|
||||||
|
enabled: boolean;
|
||||||
|
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}) {
|
||||||
|
const [frames, setFrames] = useState<Frame[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enabled && !frames && typeof window !== 'undefined') {
|
if (enabled && !frames && typeof window !== "undefined") {
|
||||||
import('./animation-frames.js')
|
import("./animation-frames.js")
|
||||||
.then(mod => setFrames(mod.frames))
|
.then((mod) => setFrames(mod.frames))
|
||||||
.catch(() => setEnabled(false))
|
.catch(() => setEnabled(false));
|
||||||
}
|
}
|
||||||
}, [enabled, frames, setEnabled])
|
}, [enabled, frames, setEnabled]);
|
||||||
|
|
||||||
if (!frames) return <Skeleton />
|
if (!frames) return <Skeleton />;
|
||||||
return <Canvas frames={frames} />
|
return <Canvas frames={frames} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Analytics, logging, and error tracking don't block user interaction. Load them a
|
|||||||
**Incorrect (blocks initial bundle):**
|
**Incorrect (blocks initial bundle):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Analytics } from '@vercel/analytics/react'
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
return (
|
return (
|
||||||
@@ -22,19 +22,18 @@ export default function RootLayout({ children }) {
|
|||||||
<Analytics />
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (loads after hydration):**
|
**Correct (loads after hydration):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const Analytics = dynamic(
|
const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), {
|
||||||
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
ssr: false,
|
||||||
{ ssr: false }
|
});
|
||||||
)
|
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
return (
|
return (
|
||||||
@@ -44,6 +43,6 @@ export default function RootLayout({ children }) {
|
|||||||
<Analytics />
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,24 +12,23 @@ Use `next/dynamic` to lazy-load large components not needed on initial render.
|
|||||||
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { MonacoEditor } from './monaco-editor'
|
import { MonacoEditor } from "./monaco-editor";
|
||||||
|
|
||||||
function CodePanel({ code }: { code: string }) {
|
function CodePanel({ code }: { code: string }) {
|
||||||
return <MonacoEditor value={code} />
|
return <MonacoEditor value={code} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (Monaco loads on demand):**
|
**Correct (Monaco loads on demand):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const MonacoEditor = dynamic(
|
const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), {
|
||||||
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
ssr: false,
|
||||||
{ ssr: false }
|
});
|
||||||
)
|
|
||||||
|
|
||||||
function CodePanel({ code }: { code: string }) {
|
function CodePanel({ code }: { code: string }) {
|
||||||
return <MonacoEditor value={code} />
|
return <MonacoEditor value={code} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -14,20 +14,16 @@ Preload heavy bundles before they're needed to reduce perceived latency.
|
|||||||
```tsx
|
```tsx
|
||||||
function EditorButton({ onClick }: { onClick: () => void }) {
|
function EditorButton({ onClick }: { onClick: () => void }) {
|
||||||
const preload = () => {
|
const preload = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== "undefined") {
|
||||||
void import('./monaco-editor')
|
void import("./monaco-editor");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
|
||||||
onMouseEnter={preload}
|
|
||||||
onFocus={preload}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
Open Editor
|
Open Editor
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,14 +32,12 @@ function EditorButton({ onClick }: { onClick: () => void }) {
|
|||||||
```tsx
|
```tsx
|
||||||
function FlagsProvider({ children, flags }: Props) {
|
function FlagsProvider({ children, flags }: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.editorEnabled && typeof window !== 'undefined') {
|
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}>
|
return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
|
||||||
{children}
|
|
||||||
</FlagsContext.Provider>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ function useKeyboardShortcut(key: string, callback: () => void) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
if (e.metaKey && e.key === key) {
|
if (e.metaKey && e.key === key) {
|
||||||
callback()
|
callback();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
window.addEventListener('keydown', handler)
|
window.addEventListener("keydown", handler);
|
||||||
return () => window.removeEventListener('keydown', handler)
|
return () => window.removeEventListener("keydown", handler);
|
||||||
}, [key, callback])
|
}, [key, callback]);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,45 +30,49 @@ When using the `useKeyboardShortcut` hook multiple times, each instance will reg
|
|||||||
**Correct (N instances = 1 listener):**
|
**Correct (N instances = 1 listener):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import useSWRSubscription from 'swr/subscription'
|
import useSWRSubscription from "swr/subscription";
|
||||||
|
|
||||||
// Module-level Map to track callbacks per key
|
// 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) {
|
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||||
// Register this callback in the Map
|
// Register this callback in the Map
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!keyCallbacks.has(key)) {
|
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 () => {
|
return () => {
|
||||||
const set = keyCallbacks.get(key)
|
const set = keyCallbacks.get(key);
|
||||||
if (set) {
|
if (set) {
|
||||||
set.delete(callback)
|
set.delete(callback);
|
||||||
if (set.size === 0) {
|
if (set.size === 0) {
|
||||||
keyCallbacks.delete(key)
|
keyCallbacks.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [key, callback])
|
}, [key, callback]);
|
||||||
|
|
||||||
useSWRSubscription('global-keydown', () => {
|
useSWRSubscription("global-keydown", () => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
if (e.metaKey && keyCallbacks.has(e.key)) {
|
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)
|
window.addEventListener("keydown", handler);
|
||||||
return () => window.removeEventListener('keydown', handler)
|
return () => window.removeEventListener("keydown", handler);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function Profile() {
|
function Profile() {
|
||||||
// Multiple shortcuts will share the same listener
|
// Multiple shortcuts will share the same listener
|
||||||
useKeyboardShortcut('p', () => { /* ... */ })
|
useKeyboardShortcut("p", () => {
|
||||||
useKeyboardShortcut('k', () => { /* ... */ })
|
/* ... */
|
||||||
|
});
|
||||||
|
useKeyboardShortcut("k", () => {
|
||||||
|
/* ... */
|
||||||
|
});
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ Add version prefix to keys and store only needed fields. Prevents schema conflic
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// No version, stores everything, no error handling
|
// No version, stores everything, no error handling
|
||||||
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
|
localStorage.setItem("userConfig", JSON.stringify(fullUserObject));
|
||||||
const data = localStorage.getItem('userConfig')
|
const data = localStorage.getItem("userConfig");
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct:**
|
**Correct:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const VERSION = 'v2'
|
const VERSION = "v2";
|
||||||
|
|
||||||
function saveConfig(config: { theme: string; language: string }) {
|
function saveConfig(config: { theme: string; language: string }) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
|
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
|
||||||
} catch {
|
} catch {
|
||||||
// Throws in incognito/private browsing, quota exceeded, or disabled
|
// Throws in incognito/private browsing, quota exceeded, or disabled
|
||||||
}
|
}
|
||||||
@@ -32,21 +32,21 @@ function saveConfig(config: { theme: string; language: string }) {
|
|||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
try {
|
try {
|
||||||
const data = localStorage.getItem(`userConfig:${VERSION}`)
|
const data = localStorage.getItem(`userConfig:${VERSION}`);
|
||||||
return data ? JSON.parse(data) : null
|
return data ? JSON.parse(data) : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migration from v1 to v2
|
// Migration from v1 to v2
|
||||||
function migrate() {
|
function migrate() {
|
||||||
try {
|
try {
|
||||||
const v1 = localStorage.getItem('userConfig:v1')
|
const v1 = localStorage.getItem("userConfig:v1");
|
||||||
if (v1) {
|
if (v1) {
|
||||||
const old = JSON.parse(v1)
|
const old = JSON.parse(v1);
|
||||||
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
|
saveConfig({ theme: old.darkMode ? "dark" : "light", language: old.lang });
|
||||||
localStorage.removeItem('userConfig:v1')
|
localStorage.removeItem("userConfig:v1");
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,13 @@ function migrate() {
|
|||||||
// User object has 20+ fields, only store what UI needs
|
// User object has 20+ fields, only store what UI needs
|
||||||
function cachePrefs(user: FullUser) {
|
function cachePrefs(user: FullUser) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('prefs:v1', JSON.stringify({
|
localStorage.setItem(
|
||||||
theme: user.preferences.theme,
|
"prefs:v1",
|
||||||
notifications: user.preferences.notifications
|
JSON.stringify({
|
||||||
}))
|
theme: user.preferences.theme,
|
||||||
|
notifications: user.preferences.notifications,
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,34 +13,34 @@ Add `{ passive: true }` to touch and wheel event listeners to enable immediate s
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
|
||||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
|
||||||
|
|
||||||
document.addEventListener('touchstart', handleTouch)
|
document.addEventListener("touchstart", handleTouch);
|
||||||
document.addEventListener('wheel', handleWheel)
|
document.addEventListener("wheel", handleWheel);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('touchstart', handleTouch)
|
document.removeEventListener("touchstart", handleTouch);
|
||||||
document.removeEventListener('wheel', handleWheel)
|
document.removeEventListener("wheel", handleWheel);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct:**
|
**Correct:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
|
||||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
|
||||||
|
|
||||||
document.addEventListener('touchstart', handleTouch, { passive: true })
|
document.addEventListener("touchstart", handleTouch, { passive: true });
|
||||||
document.addEventListener('wheel', handleWheel, { passive: true })
|
document.addEventListener("wheel", handleWheel, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('touchstart', handleTouch)
|
document.removeEventListener("touchstart", handleTouch);
|
||||||
document.removeEventListener('wheel', handleWheel)
|
document.removeEventListener("wheel", handleWheel);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
|
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
|
||||||
|
|||||||
@@ -13,43 +13,43 @@ SWR enables request deduplication, caching, and revalidation across component in
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function UserList() {
|
function UserList() {
|
||||||
const [users, setUsers] = useState([])
|
const [users, setUsers] = useState([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/users')
|
fetch("/api/users")
|
||||||
.then(r => r.json())
|
.then((r) => r.json())
|
||||||
.then(setUsers)
|
.then(setUsers);
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (multiple instances share one request):**
|
**Correct (multiple instances share one request):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import useSWR from 'swr'
|
import useSWR from "swr";
|
||||||
|
|
||||||
function UserList() {
|
function UserList() {
|
||||||
const { data: users } = useSWR('/api/users', fetcher)
|
const { data: users } = useSWR("/api/users", fetcher);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**For immutable data:**
|
**For immutable data:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useImmutableSWR } from '@/lib/swr'
|
import { useImmutableSWR } from "@/lib/swr";
|
||||||
|
|
||||||
function StaticContent() {
|
function StaticContent() {
|
||||||
const { data } = useImmutableSWR('/api/config', fetcher)
|
const { data } = useImmutableSWR("/api/config", fetcher);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**For mutations:**
|
**For mutations:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useSWRMutation } from 'swr/mutation'
|
import { useSWRMutation } from "swr/mutation";
|
||||||
|
|
||||||
function UpdateButton() {
|
function UpdateButton() {
|
||||||
const { trigger } = useSWRMutation('/api/user', updateUser)
|
const { trigger } = useSWRMutation("/api/user", updateUser);
|
||||||
return <button onClick={() => trigger()}>Update</button>
|
return <button onClick={() => trigger()}>Update</button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ Avoid interleaving style writes with layout reads. When you read a layout proper
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
element.style.width = '100px'
|
element.style.width = "100px";
|
||||||
const width = element.offsetWidth // Forces reflow
|
const width = element.offsetWidth; // Forces reflow
|
||||||
element.style.height = '200px'
|
element.style.height = "200px";
|
||||||
const height = element.offsetHeight // Forces another reflow
|
const height = element.offsetHeight; // Forces another reflow
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -25,13 +25,13 @@ function updateElementStyles(element: HTMLElement) {
|
|||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
// Batch all writes together
|
// Batch all writes together
|
||||||
element.style.width = '100px'
|
element.style.width = "100px";
|
||||||
element.style.height = '200px'
|
element.style.height = "200px";
|
||||||
element.style.backgroundColor = 'blue'
|
element.style.backgroundColor = "blue";
|
||||||
element.style.border = '1px solid black'
|
element.style.border = "1px solid black";
|
||||||
|
|
||||||
// Read after all writes are done (single reflow)
|
// Read after all writes are done (single reflow)
|
||||||
const { width, height } = element.getBoundingClientRect()
|
const { width, height } = element.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -48,9 +48,9 @@ function updateElementStyles(element: HTMLElement) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function updateElementStyles(element: HTMLElement) {
|
function updateElementStyles(element: HTMLElement) {
|
||||||
element.classList.add('highlighted-box')
|
element.classList.add("highlighted-box");
|
||||||
|
|
||||||
const { width, height } = element.getBoundingClientRect()
|
const { width, height } = element.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -11,67 +11,67 @@ Use a module-level Map to cache function results when the same function is calle
|
|||||||
|
|
||||||
**Incorrect (redundant computation):**
|
**Incorrect (redundant computation):**
|
||||||
|
|
||||||
```typescript
|
```tsx
|
||||||
function ProjectList({ projects }: { projects: Project[] }) {
|
function ProjectList({ projects }: { projects: Project[] }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{projects.map(project => {
|
{projects.map((project) => {
|
||||||
// slugify() called 100+ times for same project names
|
// slugify() called 100+ times for same project names
|
||||||
const slug = slugify(project.name)
|
const slug = slugify(project.name);
|
||||||
|
|
||||||
return <ProjectCard key={project.id} slug={slug} />
|
return <ProjectCard key={project.id} slug={slug} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (cached results):**
|
**Correct (cached results):**
|
||||||
|
|
||||||
```typescript
|
```tsx
|
||||||
// Module-level cache
|
// Module-level cache
|
||||||
const slugifyCache = new Map<string, string>()
|
const slugifyCache = new Map<string, string>();
|
||||||
|
|
||||||
function cachedSlugify(text: string): string {
|
function cachedSlugify(text: string): string {
|
||||||
if (slugifyCache.has(text)) {
|
if (slugifyCache.has(text)) {
|
||||||
return slugifyCache.get(text)!
|
return slugifyCache.get(text)!;
|
||||||
}
|
}
|
||||||
const result = slugify(text)
|
const result = slugify(text);
|
||||||
slugifyCache.set(text, result)
|
slugifyCache.set(text, result);
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectList({ projects }: { projects: Project[] }) {
|
function ProjectList({ projects }: { projects: Project[] }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{projects.map(project => {
|
{projects.map((project) => {
|
||||||
// Computed only once per unique project name
|
// Computed only once per unique project name
|
||||||
const slug = cachedSlugify(project.name)
|
const slug = cachedSlugify(project.name);
|
||||||
|
|
||||||
return <ProjectCard key={project.id} slug={slug} />
|
return <ProjectCard key={project.id} slug={slug} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Simpler pattern for single-value functions:**
|
**Simpler pattern for single-value functions:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
let isLoggedInCache: boolean | null = null
|
let isLoggedInCache: boolean | null = null;
|
||||||
|
|
||||||
function isLoggedIn(): boolean {
|
function isLoggedIn(): boolean {
|
||||||
if (isLoggedInCache !== null) {
|
if (isLoggedInCache !== null) {
|
||||||
return isLoggedInCache
|
return isLoggedInCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedInCache = document.cookie.includes('auth=')
|
isLoggedInCache = document.cookie.includes("auth=");
|
||||||
return isLoggedInCache
|
return isLoggedInCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cache when auth changes
|
// Clear cache when auth changes
|
||||||
function onAuthChange() {
|
function onAuthChange() {
|
||||||
isLoggedInCache = null
|
isLoggedInCache = null;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ Cache object property lookups in hot paths.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
for (let i = 0; i < arr.length; i++) {
|
for (let i = 0; i < arr.length; i++) {
|
||||||
process(obj.config.settings.value)
|
process(obj.config.settings.value);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (1 lookup total):**
|
**Correct (1 lookup total):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const value = obj.config.settings.value
|
const value = obj.config.settings.value;
|
||||||
const len = arr.length
|
const len = arr.length;
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
process(value)
|
process(value);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ tags: javascript, localStorage, storage, caching, performance
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getTheme() {
|
function getTheme() {
|
||||||
return localStorage.getItem('theme') ?? 'light'
|
return localStorage.getItem("theme") ?? "light";
|
||||||
}
|
}
|
||||||
// Called 10 times = 10 storage reads
|
// Called 10 times = 10 storage reads
|
||||||
```
|
```
|
||||||
@@ -21,18 +21,18 @@ function getTheme() {
|
|||||||
**Correct (Map cache):**
|
**Correct (Map cache):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const storageCache = new Map<string, string | null>()
|
const storageCache = new Map<string, string | null>();
|
||||||
|
|
||||||
function getLocalStorage(key: string) {
|
function getLocalStorage(key: string) {
|
||||||
if (!storageCache.has(key)) {
|
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) {
|
function setLocalStorage(key: string, value: string) {
|
||||||
localStorage.setItem(key, value)
|
localStorage.setItem(key, value);
|
||||||
storageCache.set(key, value) // keep cache in sync
|
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:**
|
**Cookie caching:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
let cookieCache: Record<string, string> | null = null
|
let cookieCache: Record<string, string> | null = null;
|
||||||
|
|
||||||
function getCookie(name: string) {
|
function getCookie(name: string) {
|
||||||
if (!cookieCache) {
|
if (!cookieCache) {
|
||||||
cookieCache = Object.fromEntries(
|
cookieCache = Object.fromEntries(document.cookie.split("; ").map((c) => c.split("=")));
|
||||||
document.cookie.split('; ').map(c => c.split('='))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return cookieCache[name]
|
return cookieCache[name];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -58,13 +56,13 @@ function getCookie(name: string) {
|
|||||||
If storage can change externally (another tab, server-set cookies), invalidate cache:
|
If storage can change externally (another tab, server-set cookies), invalidate cache:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
window.addEventListener('storage', (e) => {
|
window.addEventListener("storage", (e) => {
|
||||||
if (e.key) storageCache.delete(e.key)
|
if (e.key) storageCache.delete(e.key);
|
||||||
})
|
});
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener("visibilitychange", () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === "visible") {
|
||||||
storageCache.clear()
|
storageCache.clear();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,21 +12,21 @@ Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine
|
|||||||
**Incorrect (3 iterations):**
|
**Incorrect (3 iterations):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const admins = users.filter(u => u.isAdmin)
|
const admins = users.filter((u) => u.isAdmin);
|
||||||
const testers = users.filter(u => u.isTester)
|
const testers = users.filter((u) => u.isTester);
|
||||||
const inactive = users.filter(u => !u.isActive)
|
const inactive = users.filter((u) => !u.isActive);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (1 iteration):**
|
**Correct (1 iteration):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const admins: User[] = []
|
const admins: User[] = [];
|
||||||
const testers: User[] = []
|
const testers: User[] = [];
|
||||||
const inactive: User[] = []
|
const inactive: User[] = [];
|
||||||
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (user.isAdmin) admins.push(user)
|
if (user.isAdmin) admins.push(user);
|
||||||
if (user.isTester) testers.push(user)
|
if (user.isTester) testers.push(user);
|
||||||
if (!user.isActive) inactive.push(user)
|
if (!user.isActive) inactive.push(user);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,22 +13,22 @@ Return early when result is determined to skip unnecessary processing.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function validateUsers(users: User[]) {
|
function validateUsers(users: User[]) {
|
||||||
let hasError = false
|
let hasError = false;
|
||||||
let errorMessage = ''
|
let errorMessage = "";
|
||||||
|
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (!user.email) {
|
if (!user.email) {
|
||||||
hasError = true
|
hasError = true;
|
||||||
errorMessage = 'Email required'
|
errorMessage = "Email required";
|
||||||
}
|
}
|
||||||
if (!user.name) {
|
if (!user.name) {
|
||||||
hasError = true
|
hasError = true;
|
||||||
errorMessage = 'Name required'
|
errorMessage = "Name required";
|
||||||
}
|
}
|
||||||
// Continues checking all users even after error found
|
// 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[]) {
|
function validateUsers(users: User[]) {
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (!user.email) {
|
if (!user.email) {
|
||||||
return { valid: false, error: 'Email required' }
|
return { valid: false, error: "Email required" };
|
||||||
}
|
}
|
||||||
if (!user.name) {
|
if (!user.name) {
|
||||||
return { valid: false, error: 'Name required' }
|
return { valid: false, error: "Name required" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true }
|
return { valid: true };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,24 +13,21 @@ Don't create RegExp inside render. Hoist to module scope or memoize with `useMem
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Highlighter({ text, query }: Props) {
|
function Highlighter({ text, query }: Props) {
|
||||||
const regex = new RegExp(`(${query})`, 'gi')
|
const regex = new RegExp(`(${query})`, "gi");
|
||||||
const parts = text.split(regex)
|
const parts = text.split(regex);
|
||||||
return <>{parts.map((part, i) => ...)}</>
|
return <>{parts.map((part, i) => part)}</>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (memoize or hoist):**
|
**Correct (memoize or hoist):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
function Highlighter({ text, query }: Props) {
|
function Highlighter({ text, query }: Props) {
|
||||||
const regex = useMemo(
|
const regex = useMemo(() => new RegExp(`(${escapeRegex(query)})`, "gi"), [query]);
|
||||||
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
|
const parts = text.split(regex);
|
||||||
[query]
|
return <>{parts.map((part, i) => part)}</>;
|
||||||
)
|
|
||||||
const parts = text.split(regex)
|
|
||||||
return <>{parts.map((part, i) => ...)}</>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -39,7 +36,7 @@ function Highlighter({ text, query }: Props) {
|
|||||||
Global regex (`/g`) has mutable `lastIndex` state:
|
Global regex (`/g`) has mutable `lastIndex` state:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const regex = /foo/g
|
const regex = /foo/g;
|
||||||
regex.test('foo') // true, lastIndex = 3
|
regex.test("foo"); // true, lastIndex = 3
|
||||||
regex.test('foo') // false, lastIndex = 0
|
regex.test("foo"); // false, lastIndex = 0
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ Multiple `.find()` calls by the same key should use a Map.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function processOrders(orders: Order[], users: User[]) {
|
function processOrders(orders: Order[], users: User[]) {
|
||||||
return orders.map(order => ({
|
return orders.map((order) => ({
|
||||||
...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
|
```typescript
|
||||||
function processOrders(orders: Order[], users: User[]) {
|
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,
|
...order,
|
||||||
user: userById.get(order.userId)
|
user: userById.get(order.userId),
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ In real-world applications, this optimization is especially valuable when the co
|
|||||||
```typescript
|
```typescript
|
||||||
function hasChanges(current: string[], original: string[]) {
|
function hasChanges(current: string[], original: string[]) {
|
||||||
// Always sorts and joins, even when lengths differ
|
// 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[]) {
|
function hasChanges(current: string[], original: string[]) {
|
||||||
// Early return if lengths differ
|
// Early return if lengths differ
|
||||||
if (current.length !== original.length) {
|
if (current.length !== original.length) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
// Only sort when lengths match
|
// Only sort 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++) {
|
||||||
if (currentSorted[i] !== originalSorted[i]) {
|
if (currentSorted[i] !== originalSorted[i]) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This new approach is more efficient because:
|
This new approach is more efficient because:
|
||||||
|
|
||||||
- It avoids the overhead of sorting and joining the arrays when lengths differ
|
- 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 consuming memory for the joined strings (especially important for large arrays)
|
||||||
- It avoids mutating the original arrays
|
- It avoids mutating the original arrays
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ Finding the smallest or largest element only requires a single pass through the
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Project {
|
interface Project {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
updatedAt: number
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestProject(projects: Project[]) {
|
function getLatestProject(projects: Project[]) {
|
||||||
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
|
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||||
return sorted[0]
|
return sorted[0];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ Sorts the entire array just to find the maximum value.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getOldestAndNewest(projects: Project[]) {
|
function getOldestAndNewest(projects: Project[]) {
|
||||||
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
|
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
|
||||||
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
|
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -41,31 +41,31 @@ Still sorts unnecessarily when only min/max are needed.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
function getLatestProject(projects: Project[]) {
|
function getLatestProject(projects: Project[]) {
|
||||||
if (projects.length === 0) return null
|
if (projects.length === 0) return null;
|
||||||
|
|
||||||
let latest = projects[0]
|
let latest = projects[0];
|
||||||
|
|
||||||
for (let i = 1; i < projects.length; i++) {
|
for (let i = 1; i < projects.length; i++) {
|
||||||
if (projects[i].updatedAt > latest.updatedAt) {
|
if (projects[i].updatedAt > latest.updatedAt) {
|
||||||
latest = projects[i]
|
latest = projects[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return latest
|
return latest;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOldestAndNewest(projects: Project[]) {
|
function getOldestAndNewest(projects: Project[]) {
|
||||||
if (projects.length === 0) return { oldest: null, newest: null }
|
if (projects.length === 0) return { oldest: null, newest: null };
|
||||||
|
|
||||||
let oldest = projects[0]
|
let oldest = projects[0];
|
||||||
let newest = projects[0]
|
let newest = projects[0];
|
||||||
|
|
||||||
for (let i = 1; i < projects.length; i++) {
|
for (let i = 1; i < projects.length; i++) {
|
||||||
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
|
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
|
||||||
if (projects[i].updatedAt > newest.updatedAt) newest = 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):**
|
**Alternative (Math.min/Math.max for small arrays):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const numbers = [5, 2, 8, 1, 9]
|
const numbers = [5, 2, 8, 1, 9];
|
||||||
const min = Math.min(...numbers)
|
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 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.
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ Convert arrays to Set/Map for repeated membership checks.
|
|||||||
**Incorrect (O(n) per check):**
|
**Incorrect (O(n) per check):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const allowedIds = ['a', 'b', 'c', ...]
|
const allowedIds = ["a", "b", "c"];
|
||||||
items.filter(item => allowedIds.includes(item.id))
|
items.filter((item) => allowedIds.includes(item.id));
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (O(1) per check):**
|
**Correct (O(1) per check):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const allowedIds = new Set(['a', 'b', 'c', ...])
|
const allowedIds = new Set(["a", "b", "c"]);
|
||||||
items.filter(item => allowedIds.has(item.id))
|
items.filter((item) => allowedIds.has(item.id));
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,27 +11,21 @@ tags: javascript, arrays, immutability, react, state, mutation
|
|||||||
|
|
||||||
**Incorrect (mutates original array):**
|
**Incorrect (mutates original array):**
|
||||||
|
|
||||||
```typescript
|
```tsx
|
||||||
function UserList({ users }: { users: User[] }) {
|
function UserList({ users }: { users: User[] }) {
|
||||||
// Mutates the users prop array!
|
// Mutates the users prop array!
|
||||||
const sorted = useMemo(
|
const sorted = useMemo(() => users.sort((a, b) => a.name.localeCompare(b.name)), [users]);
|
||||||
() => users.sort((a, b) => a.name.localeCompare(b.name)),
|
return <div>{sorted.map(renderUser)}</div>;
|
||||||
[users]
|
|
||||||
)
|
|
||||||
return <div>{sorted.map(renderUser)}</div>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (creates new array):**
|
**Correct (creates new array):**
|
||||||
|
|
||||||
```typescript
|
```tsx
|
||||||
function UserList({ users }: { users: User[] }) {
|
function UserList({ users }: { users: User[] }) {
|
||||||
// Creates new sorted array, original unchanged
|
// Creates new sorted array, original unchanged
|
||||||
const sorted = useMemo(
|
const sorted = useMemo(() => users.toSorted((a, b) => a.name.localeCompare(b.name)), [users]);
|
||||||
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
|
return <div>{sorted.map(renderUser)}</div>;
|
||||||
[users]
|
|
||||||
)
|
|
||||||
return <div>{sorted.map(renderUser)}</div>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -46,7 +40,7 @@ function UserList({ users }: { users: User[] }) {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Fallback for older browsers
|
// 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:**
|
**Other immutable array methods:**
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ Use React's `<Activity>` to preserve state/DOM for expensive components that fre
|
|||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Activity } from 'react'
|
import { Activity } from "react";
|
||||||
|
|
||||||
function Dropdown({ isOpen }: Props) {
|
function Dropdown({ isOpen }: Props) {
|
||||||
return (
|
return (
|
||||||
<Activity mode={isOpen ? 'visible' : 'hidden'}>
|
<Activity mode={isOpen ? "visible" : "hidden"}>
|
||||||
<ExpensiveMenu />
|
<ExpensiveMenu />
|
||||||
</Activity>
|
</Activity>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ Many browsers don't have hardware acceleration for CSS3 animations on SVG elemen
|
|||||||
```tsx
|
```tsx
|
||||||
function LoadingSpinner() {
|
function LoadingSpinner() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
|
||||||
className="animate-spin"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,15 +27,11 @@ function LoadingSpinner() {
|
|||||||
function LoadingSpinner() {
|
function LoadingSpinner() {
|
||||||
return (
|
return (
|
||||||
<div className="animate-spin">
|
<div className="animate-spin">
|
||||||
<svg
|
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Badge({ count }: { count: number }) {
|
function Badge({ count }: { count: number }) {
|
||||||
return (
|
return <div>{count && <span className="badge">{count}</span>}</div>;
|
||||||
<div>
|
|
||||||
{count && <span className="badge">{count}</span>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When count = 0, renders: <div>0</div>
|
// When count = 0, renders: <div>0</div>
|
||||||
@@ -28,11 +24,7 @@ function Badge({ count }: { count: number }) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Badge({ count }: { count: number }) {
|
function Badge({ count }: { count: number }) {
|
||||||
return (
|
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
|
||||||
<div>
|
|
||||||
{count > 0 ? <span className="badge">{count}</span> : null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When count = 0, renders: <div></div>
|
// When count = 0, renders: <div></div>
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ Apply `content-visibility: auto` to defer off-screen rendering.
|
|||||||
function MessageList({ messages }: { messages: Message[] }) {
|
function MessageList({ messages }: { messages: Message[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-y-auto h-screen">
|
<div className="overflow-y-auto h-screen">
|
||||||
{messages.map(msg => (
|
{messages.map((msg) => (
|
||||||
<div key={msg.id} className="message-item">
|
<div key={msg.id} className="message-item">
|
||||||
<Avatar user={msg.author} />
|
<Avatar user={msg.author} />
|
||||||
<div>{msg.content}</div>
|
<div>{msg.content}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,31 +13,21 @@ Extract static JSX outside components to avoid re-creation.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function LoadingSkeleton() {
|
function LoadingSkeleton() {
|
||||||
return <div className="animate-pulse h-20 bg-gray-200" />
|
return <div className="animate-pulse h-20 bg-gray-200" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Container() {
|
function Container() {
|
||||||
return (
|
return <div>{loading && <LoadingSkeleton />}</div>;
|
||||||
<div>
|
|
||||||
{loading && <LoadingSkeleton />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (reuses same element):**
|
**Correct (reuses same element):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const loadingSkeleton = (
|
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
|
||||||
<div className="animate-pulse h-20 bg-gray-200" />
|
|
||||||
)
|
|
||||||
|
|
||||||
function Container() {
|
function Container() {
|
||||||
return (
|
return <div>{loading && loadingSkeleton}</div>;
|
||||||
<div>
|
|
||||||
{loading && loadingSkeleton}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,9 @@ When rendering content that depends on client-side storage (localStorage, cookie
|
|||||||
```tsx
|
```tsx
|
||||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||||
// localStorage is not available on server - throws error
|
// localStorage is not available on server - throws error
|
||||||
const theme = localStorage.getItem('theme') || 'light'
|
const theme = localStorage.getItem("theme") || "light";
|
||||||
|
|
||||||
return (
|
return <div className={theme}>{children}</div>;
|
||||||
<div className={theme}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,21 +26,17 @@ Server-side rendering will fail because `localStorage` is undefined.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||||
const [theme, setTheme] = useState('light')
|
const [theme, setTheme] = useState("light");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Runs after hydration - causes visible flash
|
// Runs after hydration - causes visible flash
|
||||||
const stored = localStorage.getItem('theme')
|
const stored = localStorage.getItem("theme");
|
||||||
if (stored) {
|
if (stored) {
|
||||||
setTheme(stored)
|
setTheme(stored);
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return <div className={theme}>{children}</div>;
|
||||||
<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 }) {
|
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id="theme-wrapper">
|
<div id="theme-wrapper">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
@@ -73,7 +63,7 @@ function ThemeWrapper({ children }: { children: ReactNode }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ShareButton({ chatId }: { chatId: string }) {
|
function ShareButton({ chatId }: { chatId: string }) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
const ref = searchParams.get('ref')
|
const ref = searchParams.get("ref");
|
||||||
shareChat(chatId, { 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
|
```tsx
|
||||||
function ShareButton({ chatId }: { chatId: string }) {
|
function ShareButton({ chatId }: { chatId: string }) {
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search);
|
||||||
const ref = params.get('ref')
|
const ref = params.get("ref");
|
||||||
shareChat(chatId, { ref })
|
shareChat(chatId, { ref });
|
||||||
}
|
};
|
||||||
|
|
||||||
return <button onClick={handleShare}>Share</button>
|
return <button onClick={handleShare}>Share</button>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ Specify primitive dependencies instead of objects to minimize effect re-runs.
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(user.id)
|
console.log(user.id);
|
||||||
}, [user])
|
}, [user]);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (re-runs only when id changes):**
|
**Correct (re-runs only when id changes):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(user.id)
|
console.log(user.id);
|
||||||
}, [user.id])
|
}, [user.id]);
|
||||||
```
|
```
|
||||||
|
|
||||||
**For derived state, compute outside effect:**
|
**For derived state, compute outside effect:**
|
||||||
@@ -31,15 +31,15 @@ useEffect(() => {
|
|||||||
// Incorrect: runs on width=767, 766, 765...
|
// Incorrect: runs on width=767, 766, 765...
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (width < 768) {
|
if (width < 768) {
|
||||||
enableMobileMode()
|
enableMobileMode();
|
||||||
}
|
}
|
||||||
}, [width])
|
}, [width]);
|
||||||
|
|
||||||
// Correct: runs only on boolean transition
|
// Correct: runs only on boolean transition
|
||||||
const isMobile = width < 768
|
const isMobile = width < 768;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
enableMobileMode()
|
enableMobileMode();
|
||||||
}
|
}
|
||||||
}, [isMobile])
|
}, [isMobile]);
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ Subscribe to derived boolean state instead of continuous values to reduce re-ren
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const width = useWindowWidth() // updates continuously
|
const width = useWindowWidth(); // updates continuously
|
||||||
const isMobile = width < 768
|
const isMobile = width < 768;
|
||||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
return <nav className={isMobile ? "mobile" : "desktop"} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ function Sidebar() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
const isMobile = useMediaQuery('(max-width: 767px)')
|
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
return <nav className={isMobile ? "mobile" : "desktop"} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,19 +13,22 @@ When updating state based on the current state value, use the functional update
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function TodoList() {
|
function TodoList() {
|
||||||
const [items, setItems] = useState(initialItems)
|
const [items, setItems] = useState(initialItems);
|
||||||
|
|
||||||
// Callback must depend on items, recreated on every items change
|
// Callback must depend on items, recreated on every items change
|
||||||
const addItems = useCallback((newItems: Item[]) => {
|
const addItems = useCallback(
|
||||||
setItems([...items, ...newItems])
|
(newItems: Item[]) => {
|
||||||
}, [items]) // ❌ items dependency causes recreations
|
setItems([...items, ...newItems]);
|
||||||
|
},
|
||||||
|
[items]
|
||||||
|
); // ❌ items dependency causes recreations
|
||||||
|
|
||||||
// Risk of stale closure if dependency is forgotten
|
// Risk of stale closure if dependency is forgotten
|
||||||
const removeItem = useCallback((id: string) => {
|
const removeItem = useCallback((id: string) => {
|
||||||
setItems(items.filter(item => item.id !== id))
|
setItems(items.filter((item) => item.id !== id));
|
||||||
}, []) // ❌ Missing items dependency - will use stale items!
|
}, []); // ❌ Missing items dependency - will use stale items!
|
||||||
|
|
||||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
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
|
```tsx
|
||||||
function TodoList() {
|
function TodoList() {
|
||||||
const [items, setItems] = useState(initialItems)
|
const [items, setItems] = useState(initialItems);
|
||||||
|
|
||||||
// Stable callback, never recreated
|
// Stable callback, never recreated
|
||||||
const addItems = useCallback((newItems: Item[]) => {
|
const addItems = useCallback((newItems: Item[]) => {
|
||||||
setItems(curr => [...curr, ...newItems])
|
setItems((curr) => [...curr, ...newItems]);
|
||||||
}, []) // ✅ No dependencies needed
|
}, []); // ✅ No dependencies needed
|
||||||
|
|
||||||
// Always uses latest state, no stale closure risk
|
// Always uses latest state, no stale closure risk
|
||||||
const removeItem = useCallback((id: string) => {
|
const removeItem = useCallback((id: string) => {
|
||||||
setItems(curr => curr.filter(item => item.id !== id))
|
setItems((curr) => curr.filter((item) => item.id !== id));
|
||||||
}, []) // ✅ Safe and stable
|
}, []); // ✅ Safe and stable
|
||||||
|
|
||||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,20 +14,18 @@ Pass a function to `useState` for expensive initial values. Without the function
|
|||||||
```tsx
|
```tsx
|
||||||
function FilteredList({ items }: { items: Item[] }) {
|
function FilteredList({ items }: { items: Item[] }) {
|
||||||
// buildSearchIndex() runs on EVERY render, even after initialization
|
// buildSearchIndex() runs on EVERY render, even after initialization
|
||||||
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
|
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
// When query changes, buildSearchIndex runs again unnecessarily
|
// When query changes, buildSearchIndex runs again unnecessarily
|
||||||
return <SearchResults index={searchIndex} query={query} />
|
return <SearchResults index={searchIndex} query={query} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserProfile() {
|
function UserProfile() {
|
||||||
// JSON.parse runs on every render
|
// JSON.parse runs on every render
|
||||||
const [settings, setSettings] = useState(
|
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings") || "{}"));
|
||||||
JSON.parse(localStorage.getItem('settings') || '{}')
|
|
||||||
)
|
|
||||||
|
|
||||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,20 +34,20 @@ function UserProfile() {
|
|||||||
```tsx
|
```tsx
|
||||||
function FilteredList({ items }: { items: Item[] }) {
|
function FilteredList({ items }: { items: Item[] }) {
|
||||||
// buildSearchIndex() runs ONLY on initial render
|
// buildSearchIndex() runs ONLY on initial render
|
||||||
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
|
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
return <SearchResults index={searchIndex} query={query} />
|
return <SearchResults index={searchIndex} query={query} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserProfile() {
|
function UserProfile() {
|
||||||
// JSON.parse runs only on initial render
|
// JSON.parse runs only on initial render
|
||||||
const [settings, setSettings] = useState(() => {
|
const [settings, setSettings] = useState(() => {
|
||||||
const stored = localStorage.getItem('settings')
|
const stored = localStorage.getItem("settings");
|
||||||
return stored ? JSON.parse(stored) : {}
|
return stored ? JSON.parse(stored) : {};
|
||||||
})
|
});
|
||||||
|
|
||||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ Extract expensive work into memoized components to enable early returns before c
|
|||||||
```tsx
|
```tsx
|
||||||
function Profile({ user, loading }: Props) {
|
function Profile({ user, loading }: Props) {
|
||||||
const avatar = useMemo(() => {
|
const avatar = useMemo(() => {
|
||||||
const id = computeAvatarId(user)
|
const id = computeAvatarId(user);
|
||||||
return <Avatar id={id} />
|
return <Avatar id={id} />;
|
||||||
}, [user])
|
}, [user]);
|
||||||
|
|
||||||
if (loading) return <Skeleton />
|
if (loading) return <Skeleton />;
|
||||||
return <div>{avatar}</div>
|
return <div>{avatar}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,17 +27,17 @@ function Profile({ user, loading }: Props) {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
||||||
const id = useMemo(() => computeAvatarId(user), [user])
|
const id = useMemo(() => computeAvatarId(user), [user]);
|
||||||
return <Avatar id={id} />
|
return <Avatar id={id} />;
|
||||||
})
|
});
|
||||||
|
|
||||||
function Profile({ user, loading }: Props) {
|
function Profile({ user, loading }: Props) {
|
||||||
if (loading) return <Skeleton />
|
if (loading) return <Skeleton />;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<UserAvatar user={user} />
|
<UserAvatar user={user} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,28 +13,28 @@ Mark frequent, non-urgent state updates as transitions to maintain UI responsive
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
function ScrollTracker() {
|
function ScrollTracker() {
|
||||||
const [scrollY, setScrollY] = useState(0)
|
const [scrollY, setScrollY] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => setScrollY(window.scrollY)
|
const handler = () => setScrollY(window.scrollY);
|
||||||
window.addEventListener('scroll', handler, { passive: true })
|
window.addEventListener("scroll", handler, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', handler)
|
return () => window.removeEventListener("scroll", handler);
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (non-blocking updates):**
|
**Correct (non-blocking updates):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { startTransition } from 'react'
|
import { startTransition } from "react";
|
||||||
|
|
||||||
function ScrollTracker() {
|
function ScrollTracker() {
|
||||||
const [scrollY, setScrollY] = useState(0)
|
const [scrollY, setScrollY] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
startTransition(() => setScrollY(window.scrollY))
|
startTransition(() => setScrollY(window.scrollY));
|
||||||
}
|
};
|
||||||
window.addEventListener('scroll', handler, { passive: true })
|
window.addEventListener("scroll", handler, { passive: true });
|
||||||
return () => window.removeEventListener('scroll', handler)
|
return () => window.removeEventListener("scroll", handler);
|
||||||
}, [])
|
}, []);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,46 +12,46 @@ Use Next.js's `after()` to schedule work that should execute after a response is
|
|||||||
**Incorrect (blocks response):**
|
**Incorrect (blocks response):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { logUserAction } from '@/app/utils'
|
import { logUserAction } from "@/app/utils";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
// Perform mutation
|
// Perform mutation
|
||||||
await updateDatabase(request)
|
await updateDatabase(request);
|
||||||
|
|
||||||
// Logging blocks the response
|
// Logging blocks the response
|
||||||
const userAgent = request.headers.get('user-agent') || 'unknown'
|
const userAgent = request.headers.get("user-agent") || "unknown";
|
||||||
await logUserAction({ userAgent })
|
await logUserAction({ userAgent });
|
||||||
|
|
||||||
return new Response(JSON.stringify({ status: 'success' }), {
|
return new Response(JSON.stringify({ status: "success" }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { "Content-Type": "application/json" },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (non-blocking):**
|
**Correct (non-blocking):**
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { after } from 'next/server'
|
import { after } from "next/server";
|
||||||
import { headers, cookies } from 'next/headers'
|
import { headers, cookies } from "next/headers";
|
||||||
import { logUserAction } from '@/app/utils'
|
import { logUserAction } from "@/app/utils";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
// Perform mutation
|
// Perform mutation
|
||||||
await updateDatabase(request)
|
await updateDatabase(request);
|
||||||
|
|
||||||
// Log after response is sent
|
// Log after response is sent
|
||||||
after(async () => {
|
after(async () => {
|
||||||
const userAgent = (await headers()).get('user-agent') || 'unknown'
|
const userAgent = (await headers()).get("user-agent") || "unknown";
|
||||||
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
|
const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous";
|
||||||
|
|
||||||
logUserAction({ sessionCookie, userAgent })
|
logUserAction({ sessionCookie, userAgent });
|
||||||
})
|
});
|
||||||
|
|
||||||
return new Response(JSON.stringify({ status: 'success' }), {
|
return new Response(JSON.stringify({ status: "success" }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { "Content-Type": "application/json" },
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ tags: server, cache, lru, cross-request
|
|||||||
**Implementation:**
|
**Implementation:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from "lru-cache";
|
||||||
|
|
||||||
const cache = new LRUCache<string, any>({
|
const cache = new LRUCache<string, any>({
|
||||||
max: 1000,
|
max: 1000,
|
||||||
ttl: 5 * 60 * 1000 // 5 minutes
|
ttl: 5 * 60 * 1000, // 5 minutes
|
||||||
})
|
});
|
||||||
|
|
||||||
export async function getUser(id: string) {
|
export async function getUser(id: string) {
|
||||||
const cached = cache.get(id)
|
const cached = cache.get(id);
|
||||||
if (cached) return cached
|
if (cached) return cached;
|
||||||
|
|
||||||
const user = await db.user.findUnique({ where: { id } })
|
const user = await db.user.findUnique({ where: { id } });
|
||||||
cache.set(id, user)
|
cache.set(id, user);
|
||||||
return user
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request 1: DB query, result cached
|
// Request 1: DB query, result cached
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ Use `React.cache()` for server-side request deduplication. Authentication and da
|
|||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { cache } from 'react'
|
import { cache } from "react";
|
||||||
|
|
||||||
export const getCurrentUser = cache(async () => {
|
export const getCurrentUser = cache(async () => {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
if (!session?.user?.id) return null
|
if (!session?.user?.id) return null;
|
||||||
return await db.user.findUnique({
|
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.
|
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
|
```typescript
|
||||||
const getUser = cache(async (params: { uid: number }) => {
|
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
|
// Each call creates new object, never hits cache
|
||||||
getUser({ uid: 1 })
|
getUser({ uid: 1 });
|
||||||
getUser({ uid: 1 }) // Cache miss, runs query again
|
getUser({ uid: 1 }); // Cache miss, runs query again
|
||||||
```
|
```
|
||||||
|
|
||||||
**Correct (cache hit):**
|
**Correct (cache hit):**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const getUser = cache(async (uid: number) => {
|
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
|
// Primitive args use value equality
|
||||||
getUser(1)
|
getUser(1);
|
||||||
getUser(1) // Cache hit, returns cached result
|
getUser(1); // Cache hit, returns cached result
|
||||||
```
|
```
|
||||||
|
|
||||||
If you must pass objects, pass the same reference:
|
If you must pass objects, pass the same reference:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const params = { uid: 1 }
|
const params = { uid: 1 };
|
||||||
getUser(params) // Query runs
|
getUser(params); // Query runs
|
||||||
getUser(params) // Cache hit (same reference)
|
getUser(params); // Cache hit (same reference)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Next.js-Specific Note:**
|
**Next.js-Specific Note:**
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ React Server Components execute sequentially within a tree. Restructure with com
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const header = await fetchHeader()
|
const header = await fetchHeader();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>{header}</div>
|
<div>{header}</div>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Sidebar() {
|
async function Sidebar() {
|
||||||
const items = await fetchSidebarItems()
|
const items = await fetchSidebarItems();
|
||||||
return <nav>{items.map(renderItem)}</nav>
|
return <nav>{items.map(renderItem)}</nav>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,13 +32,13 @@ async function Sidebar() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Header() {
|
async function Header() {
|
||||||
const data = await fetchHeader()
|
const data = await fetchHeader();
|
||||||
return <div>{data}</div>
|
return <div>{data}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Sidebar() {
|
async function Sidebar() {
|
||||||
const items = await fetchSidebarItems()
|
const items = await fetchSidebarItems();
|
||||||
return <nav>{items.map(renderItem)}</nav>
|
return <nav>{items.map(renderItem)}</nav>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -47,7 +47,7 @@ export default function Page() {
|
|||||||
<Header />
|
<Header />
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,13 +55,13 @@ export default function Page() {
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Header() {
|
async function Header() {
|
||||||
const data = await fetchHeader()
|
const data = await fetchHeader();
|
||||||
return <div>{data}</div>
|
return <div>{data}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Sidebar() {
|
async function Sidebar() {
|
||||||
const items = await fetchSidebarItems()
|
const items = await fetchSidebarItems();
|
||||||
return <nav>{items.map(renderItem)}</nav>
|
return <nav>{items.map(renderItem)}</nav>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Layout({ children }: { children: ReactNode }) {
|
function Layout({ children }: { children: ReactNode }) {
|
||||||
@@ -70,7 +70,7 @@ function Layout({ children }: { children: ReactNode }) {
|
|||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -78,6 +78,6 @@ export default function Page() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ The React Server/Client boundary serializes all object properties into strings a
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const user = await fetchUser() // 50 fields
|
const user = await fetchUser(); // 50 fields
|
||||||
return <Profile user={user} />
|
return <Profile user={user} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
'use client'
|
("use client");
|
||||||
function Profile({ user }: { user: User }) {
|
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
|
```tsx
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const user = await fetchUser()
|
const user = await fetchUser();
|
||||||
return <Profile name={user.name} />
|
return <Profile name={user.name} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
'use client'
|
("use client");
|
||||||
function Profile({ name }: { name: string }) {
|
function Profile({ name }: { name: string }) {
|
||||||
return <div>{name}</div>
|
return <div>{name}</div>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ Use WebFetch to retrieve the latest rules. The fetched content contains all the
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
When a user provides a file or pattern argument:
|
When a user provides a file or pattern argument:
|
||||||
|
|
||||||
1. Fetch guidelines from the source URL above
|
1. Fetch guidelines from the source URL above
|
||||||
2. Read the specified files
|
2. Read the specified files
|
||||||
3. Apply all rules from the fetched guidelines
|
3. Apply all rules from the fetched guidelines
|
||||||
|
|||||||
220
AGENTS.md
Normal file
220
AGENTS.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
**Core Development:**
|
||||||
|
|
||||||
|
- `pnpm dev` - Start development server (runs custom server.ts with schedulers)
|
||||||
|
- `pnpm dev:next-only` - Start Next.js only with Turbopack (no schedulers)
|
||||||
|
- `pnpm build` - Build production application
|
||||||
|
- `pnpm start` - Run production server
|
||||||
|
|
||||||
|
**Code Quality:**
|
||||||
|
|
||||||
|
- `pnpm lint` - Run ESLint
|
||||||
|
- `pnpm lint:fix` - Fix ESLint issues automatically
|
||||||
|
- `pnpm format` - Format code with Prettier
|
||||||
|
- `pnpm format:check` - Check formatting without fixing
|
||||||
|
|
||||||
|
**Database:**
|
||||||
|
|
||||||
|
- `pnpm prisma:generate` - Generate Prisma client
|
||||||
|
- `pnpm prisma:migrate` - Run database migrations
|
||||||
|
- `pnpm prisma:push` - Push schema changes to database
|
||||||
|
- `pnpm prisma:push:force` - Force reset database and push schema
|
||||||
|
- `pnpm prisma:seed` - Seed database with initial data
|
||||||
|
- `pnpm prisma:studio` - Open Prisma Studio database viewer
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
|
||||||
|
- `pnpm test` - Run both Vitest and Playwright tests concurrently
|
||||||
|
- `pnpm test:vitest` - Run Vitest tests only
|
||||||
|
- `pnpm test:vitest:watch` - Run Vitest in watch mode
|
||||||
|
- `pnpm test:vitest:coverage` - Run Vitest with coverage report
|
||||||
|
- `pnpm test:coverage` - Run all tests with coverage
|
||||||
|
|
||||||
|
**Markdown:**
|
||||||
|
|
||||||
|
- `pnpm lint:md` - Lint Markdown files
|
||||||
|
- `pnpm lint:md:fix` - Fix Markdown linting issues
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
**LiveDash-Node** is a real-time analytics dashboard for monitoring user sessions with AI-powered analysis and processing pipeline.
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
|
||||||
|
- **Frontend:** Next.js 16 + React 19 + TailwindCSS 4
|
||||||
|
- **Backend:** Next.js API Routes + Custom Node.js server
|
||||||
|
- **Database:** PostgreSQL (Neon) with Prisma ORM
|
||||||
|
- **Authentication:** Neon Auth (Better Auth) - unified for all routes
|
||||||
|
- **AI Processing:** OpenAI API integration
|
||||||
|
- **Visualization:** D3.js, React Leaflet, Recharts
|
||||||
|
- **Scheduling:** Node-cron for background processing
|
||||||
|
|
||||||
|
### Key Architecture Components
|
||||||
|
|
||||||
|
**1. Multi-Stage Processing Pipeline**
|
||||||
|
The system processes user sessions through distinct stages tracked in `SessionProcessingStatus`:
|
||||||
|
|
||||||
|
- `CSV_IMPORT` - Import raw CSV data into `SessionImport`
|
||||||
|
- `TRANSCRIPT_FETCH` - Fetch transcript content from URLs
|
||||||
|
- `SESSION_CREATION` - Create normalized `Session` and `Message` records
|
||||||
|
- `AI_ANALYSIS` - AI processing for sentiment, categorization, summaries
|
||||||
|
- `QUESTION_EXTRACTION` - Extract questions from conversations
|
||||||
|
|
||||||
|
**2. Database Architecture**
|
||||||
|
|
||||||
|
- **Multi-tenant design** with `Company` as root entity
|
||||||
|
- **Dual storage pattern**: Raw CSV data in `SessionImport`, processed data in `Session`
|
||||||
|
- **1-to-1 relationship** between `SessionImport` and `Session` via `importId`
|
||||||
|
- **Message parsing** into individual `Message` records with order tracking
|
||||||
|
- **AI cost tracking** via `AIProcessingRequest` with detailed token usage
|
||||||
|
- **Flexible AI model management** through `AIModel`, `AIModelPricing`, and `CompanyAIModel`
|
||||||
|
|
||||||
|
**3. Custom Server Architecture**
|
||||||
|
|
||||||
|
- `server.ts` - Custom Next.js server with configurable scheduler initialization
|
||||||
|
- Three main schedulers: CSV import, import processing, and session processing
|
||||||
|
- Environment-based configuration via `lib/env.ts`
|
||||||
|
|
||||||
|
**4. Key Processing Libraries**
|
||||||
|
|
||||||
|
- `lib/scheduler.ts` - CSV import scheduling
|
||||||
|
- `lib/importProcessor.ts` - Raw data to Session conversion
|
||||||
|
- `lib/processingScheduler.ts` - AI analysis pipeline
|
||||||
|
- `lib/transcriptFetcher.ts` - External transcript fetching
|
||||||
|
- `lib/transcriptParser.ts` - Message parsing from transcripts
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
|
||||||
|
**Environment Configuration:**
|
||||||
|
Environment variables are managed through `lib/env.ts` with .env.local file support:
|
||||||
|
|
||||||
|
- Database: PostgreSQL via `DATABASE_URL` and `DATABASE_URL_DIRECT`
|
||||||
|
- Authentication: `NEON_AUTH_BASE_URL`, `AUTH_URL`, `JWKS_URL`
|
||||||
|
- AI Processing: `OPENAI_API_KEY`
|
||||||
|
- Schedulers: `SCHEDULER_ENABLED`, various interval configurations
|
||||||
|
|
||||||
|
**Key Files to Understand:**
|
||||||
|
|
||||||
|
- `prisma/schema.prisma` - Complete database schema with enums and relationships
|
||||||
|
- `server.ts` - Custom server entry point
|
||||||
|
- `lib/env.ts` - Environment variable management and validation
|
||||||
|
- `app/` - Next.js App Router structure
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
|
||||||
|
- Uses Vitest for unit testing
|
||||||
|
- Playwright for E2E testing
|
||||||
|
- Test files in `tests/` directory
|
||||||
|
|
||||||
|
### Important Notes
|
||||||
|
|
||||||
|
**Scheduler System:**
|
||||||
|
|
||||||
|
- Schedulers are optional and controlled by `SCHEDULER_ENABLED` environment variable
|
||||||
|
- Use `pnpm dev:next-only` to run without schedulers for pure frontend development
|
||||||
|
- Three separate schedulers handle different pipeline stages:
|
||||||
|
- CSV Import Scheduler (`lib/scheduler.ts`)
|
||||||
|
- Import Processing Scheduler (`lib/importProcessor.ts`)
|
||||||
|
- Session Processing Scheduler (`lib/processingScheduler.ts`)
|
||||||
|
|
||||||
|
**Database Migrations:**
|
||||||
|
|
||||||
|
- Always run `pnpm prisma:generate` after schema changes
|
||||||
|
- Use `pnpm prisma:migrate` for production-ready migrations
|
||||||
|
- Use `pnpm prisma:push` for development schema changes
|
||||||
|
- Database uses PostgreSQL with Prisma's driver adapter for connection pooling
|
||||||
|
|
||||||
|
**AI Processing:**
|
||||||
|
|
||||||
|
- All AI requests are tracked for cost analysis
|
||||||
|
- Support for multiple AI models per company
|
||||||
|
- Time-based pricing management for accurate cost calculation
|
||||||
|
- Processing stages can be retried on failure with retry count tracking
|
||||||
|
|
||||||
|
**Code Quality Standards:**
|
||||||
|
|
||||||
|
- Run `pnpm lint` and `pnpm format:check` before committing
|
||||||
|
- TypeScript with ES modules (type: "module" in package.json)
|
||||||
|
- React 19 with Next.js 16 App Router
|
||||||
|
- TailwindCSS 4 for styling
|
||||||
|
|
||||||
|
### Authentication Architecture
|
||||||
|
|
||||||
|
**Unified Neon Auth (Better Auth):**
|
||||||
|
|
||||||
|
All authentication uses Neon Auth with role-based access control via `UserRole` enum:
|
||||||
|
|
||||||
|
- **Platform roles** (companyId = null): `PLATFORM_SUPER_ADMIN`, `PLATFORM_ADMIN`, `PLATFORM_SUPPORT`
|
||||||
|
- **Company roles** (companyId set): `ADMIN`, `USER`
|
||||||
|
|
||||||
|
**Key Files:**
|
||||||
|
|
||||||
|
- `lib/auth/client.ts` - Client-side auth with `authClient.useSession()` hook
|
||||||
|
- `lib/auth/server.ts` - Server-side helpers:
|
||||||
|
- `neonAuth()` - Get current session/user
|
||||||
|
- `getAuthenticatedUser()` - Get any authenticated user
|
||||||
|
- `getAuthenticatedCompanyUser()` - Get company users only
|
||||||
|
- `getAuthenticatedPlatformUser()` - Get platform users only
|
||||||
|
- `isPlatformRole()`, `hasPlatformAccess()` - Role checking utilities
|
||||||
|
- `app/api/auth/[...path]/route.ts` - Neon Auth API handler
|
||||||
|
- `app/api/auth/me/route.ts` - Returns current user data from User table
|
||||||
|
|
||||||
|
**Auth Routes:**
|
||||||
|
|
||||||
|
- `/auth/sign-in`, `/auth/sign-up`, `/auth/sign-out` - Auth pages
|
||||||
|
- `/account/settings`, `/account/security` - Account management
|
||||||
|
- `/platform/login` - Platform admin login (uses same Neon Auth)
|
||||||
|
|
||||||
|
**User Data Flow:**
|
||||||
|
|
||||||
|
- Neon Auth manages authentication (sessions, JWT)
|
||||||
|
- Prisma `User` table stores `companyId` and `role` for authorization
|
||||||
|
- Users with `companyId = null` are platform users
|
||||||
|
- Users with `companyId` set are company users
|
||||||
|
- `/api/auth/me` endpoint returns full user data including role
|
||||||
|
|
||||||
|
**Key Auth Patterns:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Client Component
|
||||||
|
import { authClient } from "@/lib/auth/client";
|
||||||
|
const { data, isPending } = authClient.useSession();
|
||||||
|
|
||||||
|
// Server/API Route - General auth
|
||||||
|
import { neonAuth } from "@/lib/auth/server";
|
||||||
|
const { session, user } = await neonAuth();
|
||||||
|
|
||||||
|
// Server/API Route - Company user only
|
||||||
|
import { getAuthenticatedCompanyUser } from "@/lib/auth/server";
|
||||||
|
const { user, company } = await getAuthenticatedCompanyUser();
|
||||||
|
|
||||||
|
// Server/API Route - Platform user only
|
||||||
|
import { getAuthenticatedPlatformUser, hasPlatformAccess } from "@/lib/auth/server";
|
||||||
|
const { user } = await getAuthenticatedPlatformUser();
|
||||||
|
if (!hasPlatformAccess(user.role, "PLATFORM_ADMIN")) {
|
||||||
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**User Creation (Passwordless in DB):**
|
||||||
|
|
||||||
|
Users are created without passwords in our database. Neon Auth handles password storage:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
role: "ADMIN",
|
||||||
|
companyId: company.id,
|
||||||
|
invitedBy: currentUser.email,
|
||||||
|
invitedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// User completes signup via /auth/sign-up with same email
|
||||||
|
```
|
||||||
145
CLAUDE.md
145
CLAUDE.md
@@ -1,144 +1 @@
|
|||||||
# CLAUDE.md
|
@AGENTS.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
**Core Development:**
|
|
||||||
|
|
||||||
- `pnpm dev` - Start development server (runs custom server.ts with schedulers)
|
|
||||||
- `pnpm dev:next-only` - Start Next.js only with Turbopack (no schedulers)
|
|
||||||
- `pnpm build` - Build production application
|
|
||||||
- `pnpm start` - Run production server
|
|
||||||
|
|
||||||
**Code Quality:**
|
|
||||||
|
|
||||||
- `pnpm lint` - Run ESLint
|
|
||||||
- `pnpm lint:fix` - Fix ESLint issues automatically
|
|
||||||
- `pnpm format` - Format code with Prettier
|
|
||||||
- `pnpm format:check` - Check formatting without fixing
|
|
||||||
|
|
||||||
**Database:**
|
|
||||||
|
|
||||||
- `pnpm prisma:generate` - Generate Prisma client
|
|
||||||
- `pnpm prisma:migrate` - Run database migrations
|
|
||||||
- `pnpm prisma:push` - Push schema changes to database
|
|
||||||
- `pnpm prisma:push:force` - Force reset database and push schema
|
|
||||||
- `pnpm prisma:seed` - Seed database with initial data
|
|
||||||
- `pnpm prisma:studio` - Open Prisma Studio database viewer
|
|
||||||
|
|
||||||
**Testing:**
|
|
||||||
|
|
||||||
- `pnpm test` - Run both Vitest and Playwright tests concurrently
|
|
||||||
- `pnpm test:vitest` - Run Vitest tests only
|
|
||||||
- `pnpm test:vitest:watch` - Run Vitest in watch mode
|
|
||||||
- `pnpm test:vitest:coverage` - Run Vitest with coverage report
|
|
||||||
- `pnpm test:coverage` - Run all tests with coverage
|
|
||||||
|
|
||||||
**Markdown:**
|
|
||||||
|
|
||||||
- `pnpm lint:md` - Lint Markdown files
|
|
||||||
- `pnpm lint:md:fix` - Fix Markdown linting issues
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
**LiveDash-Node** is a real-time analytics dashboard for monitoring user sessions with AI-powered analysis and processing pipeline.
|
|
||||||
|
|
||||||
### Tech Stack
|
|
||||||
|
|
||||||
- **Frontend:** Next.js 15 + React 19 + TailwindCSS 4
|
|
||||||
- **Backend:** Next.js API Routes + Custom Node.js server
|
|
||||||
- **Database:** PostgreSQL with Prisma ORM
|
|
||||||
- **Authentication:** NextAuth.js
|
|
||||||
- **AI Processing:** OpenAI API integration
|
|
||||||
- **Visualization:** D3.js, React Leaflet, Recharts
|
|
||||||
- **Scheduling:** Node-cron for background processing
|
|
||||||
|
|
||||||
### Key Architecture Components
|
|
||||||
|
|
||||||
**1. Multi-Stage Processing Pipeline**
|
|
||||||
The system processes user sessions through distinct stages tracked in `SessionProcessingStatus`:
|
|
||||||
|
|
||||||
- `CSV_IMPORT` - Import raw CSV data into `SessionImport`
|
|
||||||
- `TRANSCRIPT_FETCH` - Fetch transcript content from URLs
|
|
||||||
- `SESSION_CREATION` - Create normalized `Session` and `Message` records
|
|
||||||
- `AI_ANALYSIS` - AI processing for sentiment, categorization, summaries
|
|
||||||
- `QUESTION_EXTRACTION` - Extract questions from conversations
|
|
||||||
|
|
||||||
**2. Database Architecture**
|
|
||||||
|
|
||||||
- **Multi-tenant design** with `Company` as root entity
|
|
||||||
- **Dual storage pattern**: Raw CSV data in `SessionImport`, processed data in `Session`
|
|
||||||
- **1-to-1 relationship** between `SessionImport` and `Session` via `importId`
|
|
||||||
- **Message parsing** into individual `Message` records with order tracking
|
|
||||||
- **AI cost tracking** via `AIProcessingRequest` with detailed token usage
|
|
||||||
- **Flexible AI model management** through `AIModel`, `AIModelPricing`, and `CompanyAIModel`
|
|
||||||
|
|
||||||
**3. Custom Server Architecture**
|
|
||||||
|
|
||||||
- `server.ts` - Custom Next.js server with configurable scheduler initialization
|
|
||||||
- Three main schedulers: CSV import, import processing, and session processing
|
|
||||||
- Environment-based configuration via `lib/env.ts`
|
|
||||||
|
|
||||||
**4. Key Processing Libraries**
|
|
||||||
|
|
||||||
- `lib/scheduler.ts` - CSV import scheduling
|
|
||||||
- `lib/importProcessor.ts` - Raw data to Session conversion
|
|
||||||
- `lib/processingScheduler.ts` - AI analysis pipeline
|
|
||||||
- `lib/transcriptFetcher.ts` - External transcript fetching
|
|
||||||
- `lib/transcriptParser.ts` - Message parsing from transcripts
|
|
||||||
|
|
||||||
### Development Environment
|
|
||||||
|
|
||||||
**Environment Configuration:**
|
|
||||||
Environment variables are managed through `lib/env.ts` with .env.local file support:
|
|
||||||
|
|
||||||
- Database: PostgreSQL via `DATABASE_URL` and `DATABASE_URL_DIRECT`
|
|
||||||
- Authentication: `NEXTAUTH_SECRET`, `NEXTAUTH_URL`
|
|
||||||
- AI Processing: `OPENAI_API_KEY`
|
|
||||||
- Schedulers: `SCHEDULER_ENABLED`, various interval configurations
|
|
||||||
|
|
||||||
**Key Files to Understand:**
|
|
||||||
|
|
||||||
- `prisma/schema.prisma` - Complete database schema with enums and relationships
|
|
||||||
- `server.ts` - Custom server entry point
|
|
||||||
- `lib/env.ts` - Environment variable management and validation
|
|
||||||
- `app/` - Next.js App Router structure
|
|
||||||
|
|
||||||
**Testing:**
|
|
||||||
|
|
||||||
- Uses Vitest for unit testing
|
|
||||||
- Playwright for E2E testing
|
|
||||||
- Test files in `tests/` directory
|
|
||||||
|
|
||||||
### Important Notes
|
|
||||||
|
|
||||||
**Scheduler System:**
|
|
||||||
|
|
||||||
- Schedulers are optional and controlled by `SCHEDULER_ENABLED` environment variable
|
|
||||||
- Use `pnpm dev:next-only` to run without schedulers for pure frontend development
|
|
||||||
- Three separate schedulers handle different pipeline stages:
|
|
||||||
- CSV Import Scheduler (`lib/scheduler.ts`)
|
|
||||||
- Import Processing Scheduler (`lib/importProcessor.ts`)
|
|
||||||
- Session Processing Scheduler (`lib/processingScheduler.ts`)
|
|
||||||
|
|
||||||
**Database Migrations:**
|
|
||||||
|
|
||||||
- Always run `pnpm prisma:generate` after schema changes
|
|
||||||
- Use `pnpm prisma:migrate` for production-ready migrations
|
|
||||||
- Use `pnpm prisma:push` for development schema changes
|
|
||||||
- Database uses PostgreSQL with Prisma's driver adapter for connection pooling
|
|
||||||
|
|
||||||
**AI Processing:**
|
|
||||||
|
|
||||||
- All AI requests are tracked for cost analysis
|
|
||||||
- Support for multiple AI models per company
|
|
||||||
- Time-based pricing management for accurate cost calculation
|
|
||||||
- Processing stages can be retried on failure with retry count tracking
|
|
||||||
|
|
||||||
**Code Quality Standards:**
|
|
||||||
|
|
||||||
- Run `pnpm lint` and `pnpm format:check` before committing
|
|
||||||
- TypeScript with ES modules (type: "module" in package.json)
|
|
||||||
- React 19 with Next.js 15 App Router
|
|
||||||
- TailwindCSS 4 for styling
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user