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:
2026-01-20 21:09:29 +01:00
parent 7932fe7386
commit cd05fc8648
177 changed files with 5042 additions and 5541 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ Comprehensive performance optimization guide for React and Next.js applications,
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
@@ -23,7 +24,7 @@ Reference these guidelines when:
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| -------- | ------------------------- | ----------- | ------------ |
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
@@ -115,6 +116,7 @@ rules/_sections.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation

View File

@@ -14,9 +14,9 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
window.addEventListener(event, handler);
return () => window.removeEventListener(event, handler);
}, [event, handler]);
}
```
@@ -24,31 +24,31 @@ function useWindowEvent(event: string, handler: (e) => void) {
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
const handlerRef = useRef(handler)
const handlerRef = useRef(handler);
useEffect(() => {
handlerRef.current = handler
}, [handler])
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const listener = (e) => handlerRef.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
const listener = (e) => handlerRef.current(e);
window.addEventListener(event, listener);
return () => window.removeEventListener(event, listener);
}, [event]);
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
```tsx
import { useEffectEvent } from 'react'
import { useEffectEvent } from "react";
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler)
const onEvent = useEffectEvent(handler);
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
window.addEventListener(event, onEvent);
return () => window.removeEventListener(event, onEvent);
}, [event]);
}
```

View File

@@ -13,11 +13,11 @@ Access latest values in callbacks without adding them to dependency arrays. Prev
```typescript
function useLatest<T>(value: T) {
const ref = useRef(value)
const ref = useRef(value);
useLayoutEffect(() => {
ref.current = value
}, [value])
return ref
ref.current = value;
}, [value]);
return ref;
}
```
@@ -25,12 +25,12 @@ function useLatest<T>(value: T) {
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const [query, setQuery] = useState("");
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300)
return () => clearTimeout(timeout)
}, [query, onSearch])
const timeout = setTimeout(() => onSearch(query), 300);
return () => clearTimeout(timeout);
}, [query, onSearch]);
}
```
@@ -38,12 +38,12 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const onSearchRef = useLatest(onSearch)
const [query, setQuery] = useState("");
const onSearchRef = useLatest(onSearch);
useEffect(() => {
const timeout = setTimeout(() => onSearchRef.current(query), 300)
return () => clearTimeout(timeout)
}, [query])
const timeout = setTimeout(() => onSearchRef.current(query), 300);
return () => clearTimeout(timeout);
}, [query]);
}
```

View File

@@ -13,10 +13,10 @@ In API routes and Server Actions, start independent operations immediately, even
```typescript
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
const session = await auth();
const config = await fetchConfig();
const data = await fetchData(session.user.id);
return Response.json({ data, config });
}
```
@@ -24,14 +24,11 @@ export async function GET(request: Request) {
```typescript
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
const sessionPromise = auth();
const configPromise = fetchConfig();
const session = await sessionPromise;
const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);
return Response.json({ data, config });
}
```

View File

@@ -13,15 +13,15 @@ Move `await` operations into the branches where they're actually used to avoid b
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
const userData = await fetchUserData(userId);
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true }
return { skipped: true };
}
// Only this branch uses userData
return processUserData(userData)
return processUserData(userData);
}
```
@@ -31,12 +31,12 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true }
return { skipped: true };
}
// Fetch only when needed
const userData = await fetchUserData(userId)
return processUserData(userData)
const userData = await fetchUserData(userId);
return processUserData(userData);
}
```
@@ -45,35 +45,35 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
const permissions = await fetchPermissions(userId);
const resource = await getResource(resourceId);
if (!resource) {
return { error: 'Not found' }
return { error: "Not found" };
}
if (!permissions.canEdit) {
return { error: 'Forbidden' }
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions)
return await updateResourceData(resource, permissions);
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
const resource = await getResource(resourceId);
if (!resource) {
return { error: 'Not found' }
return { error: "Not found" };
}
const permissions = await fetchPermissions(userId)
const permissions = await fetchPermissions(userId);
if (!permissions.canEdit) {
return { error: 'Forbidden' }
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions)
return await updateResourceData(resource, permissions);
}
```

View File

@@ -12,25 +12,26 @@ For operations with partial dependencies, use `better-all` to maximize paralleli
**Incorrect (profile waits for config unnecessarily):**
```typescript
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
const profile = await fetchProfile(user.id);
```
**Correct (config and profile run in parallel):**
```typescript
import { all } from 'better-all'
import { all } from "better-all";
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async user() {
return fetchUser();
},
async config() {
return fetchConfig();
},
async profile() {
return fetchProfile((await this.$.user).id)
}
})
return fetchProfile((await this.$.user).id);
},
});
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)

View File

@@ -12,17 +12,13 @@ When async operations have no interdependencies, execute them concurrently using
**Incorrect (sequential execution, 3 round trips):**
```typescript
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
```
**Correct (parallel execution, 1 round trip):**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);
```

View File

@@ -13,7 +13,7 @@ Instead of awaiting data in async components before returning JSX, use Suspense
```tsx
async function Page() {
const data = await fetchData() // Blocks entire page
const data = await fetchData(); // Blocks entire page
return (
<div>
@@ -24,7 +24,7 @@ async function Page() {
</div>
<div>Footer</div>
</div>
)
);
}
```
@@ -45,12 +45,12 @@ function Page() {
</div>
<div>Footer</div>
</div>
)
);
}
async function DataDisplay() {
const data = await fetchData() // Only blocks this component
return <div>{data.content}</div>
const data = await fetchData(); // Only blocks this component
return <div>{data.content}</div>;
}
```
@@ -61,7 +61,7 @@ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
```tsx
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData()
const dataPromise = fetchData();
return (
<div>
@@ -73,17 +73,17 @@ function Page() {
</Suspense>
<div>Footer</div>
</div>
)
);
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Unwraps the promise
return <div>{data.content}</div>
const data = use(dataPromise); // Unwraps the promise
return <div>{data.content}</div>;
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Reuses the same promise
return <div>{data.summary}</div>
const data = use(dataPromise); // Reuses the same promise
return <div>{data.summary}</div>;
}
```

View File

@@ -16,24 +16,24 @@ Popular icon and component libraries can have **up to 10,000 re-exports** in the
**Incorrect (imports entire library):**
```tsx
import { Check, X, Menu } from 'lucide-react'
import { Check, X, Menu } from "lucide-react";
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from '@mui/material'
import { Button, TextField } from "@mui/material";
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct (imports only what you need):**
```tsx
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
import Check from "lucide-react/dist/esm/icons/check";
import X from "lucide-react/dist/esm/icons/x";
import Menu from "lucide-react/dist/esm/icons/menu";
// Loads only 3 modules (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
// Loads only what you use
```
@@ -43,12 +43,12 @@ import TextField from '@mui/material/TextField'
// next.config.js - use optimizePackageImports
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
optimizePackageImports: ["lucide-react", "@mui/material"],
},
};
// Then you can keep the ergonomic barrel imports:
import { Check, X, Menu } from 'lucide-react'
import { Check, X, Menu } from "lucide-react";
// Automatically transformed to direct imports at build time
```

View File

@@ -12,19 +12,25 @@ Load large data or modules only when a feature is activated.
**Example (lazy-load animation frames):**
```tsx
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
const [frames, setFrames] = useState<Frame[] | null>(null)
function AnimationPlayer({
enabled,
setEnabled,
}: {
enabled: boolean;
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const [frames, setFrames] = useState<Frame[] | null>(null);
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
.catch(() => setEnabled(false))
if (enabled && !frames && typeof window !== "undefined") {
import("./animation-frames.js")
.then((mod) => setFrames(mod.frames))
.catch(() => setEnabled(false));
}
}, [enabled, frames, setEnabled])
}, [enabled, frames, setEnabled]);
if (!frames) return <Skeleton />
return <Canvas frames={frames} />
if (!frames) return <Skeleton />;
return <Canvas frames={frames} />;
}
```

View File

@@ -12,7 +12,7 @@ Analytics, logging, and error tracking don't block user interaction. Load them a
**Incorrect (blocks initial bundle):**
```tsx
import { Analytics } from '@vercel/analytics/react'
import { Analytics } from "@vercel/analytics/react";
export default function RootLayout({ children }) {
return (
@@ -22,19 +22,18 @@ export default function RootLayout({ children }) {
<Analytics />
</body>
</html>
)
);
}
```
**Correct (loads after hydration):**
```tsx
import dynamic from 'next/dynamic'
import dynamic from "next/dynamic";
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), {
ssr: false,
});
export default function RootLayout({ children }) {
return (
@@ -44,6 +43,6 @@ export default function RootLayout({ children }) {
<Analytics />
</body>
</html>
)
);
}
```

View File

@@ -12,24 +12,23 @@ Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect (Monaco bundles with main chunk ~300KB):**
```tsx
import { MonacoEditor } from './monaco-editor'
import { MonacoEditor } from "./monaco-editor";
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
return <MonacoEditor value={code} />;
}
```
**Correct (Monaco loads on demand):**
```tsx
import dynamic from 'next/dynamic'
import dynamic from "next/dynamic";
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), {
ssr: false,
});
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
return <MonacoEditor value={code} />;
}
```

View File

@@ -14,20 +14,16 @@ Preload heavy bundles before they're needed to reduce perceived latency.
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
if (typeof window !== "undefined") {
void import("./monaco-editor");
}
};
return (
<button
onMouseEnter={preload}
onFocus={preload}
onClick={onClick}
>
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
)
);
}
```
@@ -36,14 +32,12 @@ function EditorButton({ onClick }: { onClick: () => void }) {
```tsx
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== 'undefined') {
void import('./monaco-editor').then(mod => mod.init())
if (flags.editorEnabled && typeof window !== "undefined") {
void import("./monaco-editor").then((mod) => mod.init());
}
}, [flags.editorEnabled])
}, [flags.editorEnabled]);
return <FlagsContext.Provider value={flags}>
{children}
</FlagsContext.Provider>
return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
}
```

View File

@@ -16,12 +16,12 @@ function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback()
callback();
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [key, callback])
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [key, callback]);
}
```
@@ -30,45 +30,49 @@ When using the `useKeyboardShortcut` hook multiple times, each instance will reg
**Correct (N instances = 1 listener):**
```tsx
import useSWRSubscription from 'swr/subscription'
import useSWRSubscription from "swr/subscription";
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set<() => void>>()
const keyCallbacks = new Map<string, Set<() => void>>();
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set())
keyCallbacks.set(key, new Set());
}
keyCallbacks.get(key)!.add(callback)
keyCallbacks.get(key)!.add(callback);
return () => {
const set = keyCallbacks.get(key)
const set = keyCallbacks.get(key);
if (set) {
set.delete(callback)
set.delete(callback);
if (set.size === 0) {
keyCallbacks.delete(key)
keyCallbacks.delete(key);
}
}
}
}, [key, callback])
};
}, [key, callback]);
useSWRSubscription('global-keydown', () => {
useSWRSubscription("global-keydown", () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
keyCallbacks.get(e.key)!.forEach((cb) => cb());
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
});
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut('p', () => { /* ... */ })
useKeyboardShortcut('k', () => { /* ... */ })
useKeyboardShortcut("p", () => {
/* ... */
});
useKeyboardShortcut("k", () => {
/* ... */
});
// ...
}
```

View File

@@ -13,18 +13,18 @@ Add version prefix to keys and store only needed fields. Prevents schema conflic
```typescript
// No version, stores everything, no error handling
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
const data = localStorage.getItem('userConfig')
localStorage.setItem("userConfig", JSON.stringify(fullUserObject));
const data = localStorage.getItem("userConfig");
```
**Correct:**
```typescript
const VERSION = 'v2'
const VERSION = "v2";
function saveConfig(config: { theme: string; language: string }) {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
} catch {
// Throws in incognito/private browsing, quota exceeded, or disabled
}
@@ -32,21 +32,21 @@ function saveConfig(config: { theme: string; language: string }) {
function loadConfig() {
try {
const data = localStorage.getItem(`userConfig:${VERSION}`)
return data ? JSON.parse(data) : null
const data = localStorage.getItem(`userConfig:${VERSION}`);
return data ? JSON.parse(data) : null;
} catch {
return null
return null;
}
}
// Migration from v1 to v2
function migrate() {
try {
const v1 = localStorage.getItem('userConfig:v1')
const v1 = localStorage.getItem("userConfig:v1");
if (v1) {
const old = JSON.parse(v1)
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
localStorage.removeItem('userConfig:v1')
const old = JSON.parse(v1);
saveConfig({ theme: old.darkMode ? "dark" : "light", language: old.lang });
localStorage.removeItem("userConfig:v1");
}
} catch {}
}
@@ -58,10 +58,13 @@ function migrate() {
// User object has 20+ fields, only store what UI needs
function cachePrefs(user: FullUser) {
try {
localStorage.setItem('prefs:v1', JSON.stringify({
localStorage.setItem(
"prefs:v1",
JSON.stringify({
theme: user.preferences.theme,
notifications: user.preferences.notifications
}))
notifications: user.preferences.notifications,
})
);
} catch {}
}
```

View File

@@ -13,34 +13,34 @@ Add `{ passive: true }` to touch and wheel event listeners to enable immediate s
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener('touchstart', handleTouch)
document.addEventListener('wheel', handleWheel)
document.addEventListener("touchstart", handleTouch);
document.addEventListener("wheel", handleWheel);
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
document.removeEventListener("touchstart", handleTouch);
document.removeEventListener("wheel", handleWheel);
};
}, []);
```
**Correct:**
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener('touchstart', handleTouch, { passive: true })
document.addEventListener('wheel', handleWheel, { passive: true })
document.addEventListener("touchstart", handleTouch, { passive: true });
document.addEventListener("wheel", handleWheel, { passive: true });
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
document.removeEventListener("touchstart", handleTouch);
document.removeEventListener("wheel", handleWheel);
};
}, []);
```
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.

View File

@@ -13,43 +13,43 @@ SWR enables request deduplication, caching, and revalidation across component in
```tsx
function UserList() {
const [users, setUsers] = useState([])
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
fetch("/api/users")
.then((r) => r.json())
.then(setUsers);
}, []);
}
```
**Correct (multiple instances share one request):**
```tsx
import useSWR from 'swr'
import useSWR from "swr";
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
const { data: users } = useSWR("/api/users", fetcher);
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from '@/lib/swr'
import { useImmutableSWR } from "@/lib/swr";
function StaticContent() {
const { data } = useImmutableSWR('/api/config', fetcher)
const { data } = useImmutableSWR("/api/config", fetcher);
}
```
**For mutations:**
```tsx
import { useSWRMutation } from 'swr/mutation'
import { useSWRMutation } from "swr/mutation";
function UpdateButton() {
const { trigger } = useSWRMutation('/api/user', updateUser)
return <button onClick={() => trigger()}>Update</button>
const { trigger } = useSWRMutation("/api/user", updateUser);
return <button onClick={() => trigger()}>Update</button>;
}
```

View File

@@ -13,10 +13,10 @@ Avoid interleaving style writes with layout reads. When you read a layout proper
```typescript
function updateElementStyles(element: HTMLElement) {
element.style.width = '100px'
const width = element.offsetWidth // Forces reflow
element.style.height = '200px'
const height = element.offsetHeight // Forces another reflow
element.style.width = "100px";
const width = element.offsetWidth; // Forces reflow
element.style.height = "200px";
const height = element.offsetHeight; // Forces another reflow
}
```
@@ -25,13 +25,13 @@ function updateElementStyles(element: HTMLElement) {
```typescript
function updateElementStyles(element: HTMLElement) {
// Batch all writes together
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
element.style.width = "100px";
element.style.height = "200px";
element.style.backgroundColor = "blue";
element.style.border = "1px solid black";
// Read after all writes are done (single reflow)
const { width, height } = element.getBoundingClientRect()
const { width, height } = element.getBoundingClientRect();
}
```
@@ -48,9 +48,9 @@ function updateElementStyles(element: HTMLElement) {
```typescript
function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box')
element.classList.add("highlighted-box");
const { width, height } = element.getBoundingClientRect()
const { width, height } = element.getBoundingClientRect();
}
```

View File

@@ -11,67 +11,67 @@ Use a module-level Map to cache function results when the same function is calle
**Incorrect (redundant computation):**
```typescript
```tsx
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
{projects.map((project) => {
// 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>
)
);
}
```
**Correct (cached results):**
```typescript
```tsx
// Module-level cache
const slugifyCache = new Map<string, string>()
const slugifyCache = new Map<string, string>();
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) {
return slugifyCache.get(text)!
return slugifyCache.get(text)!;
}
const result = slugify(text)
slugifyCache.set(text, result)
return result
const result = slugify(text);
slugifyCache.set(text, result);
return result;
}
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
{projects.map((project) => {
// 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>
)
);
}
```
**Simpler pattern for single-value functions:**
```typescript
let isLoggedInCache: boolean | null = null
let isLoggedInCache: boolean | null = null;
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) {
return isLoggedInCache
return isLoggedInCache;
}
isLoggedInCache = document.cookie.includes('auth=')
return isLoggedInCache
isLoggedInCache = document.cookie.includes("auth=");
return isLoggedInCache;
}
// Clear cache when auth changes
function onAuthChange() {
isLoggedInCache = null
isLoggedInCache = null;
}
```

View File

@@ -13,16 +13,16 @@ Cache object property lookups in hot paths.
```typescript
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value)
process(obj.config.settings.value);
}
```
**Correct (1 lookup total):**
```typescript
const value = obj.config.settings.value
const len = arr.length
const value = obj.config.settings.value;
const len = arr.length;
for (let i = 0; i < len; i++) {
process(value)
process(value);
}
```

View File

@@ -13,7 +13,7 @@ tags: javascript, localStorage, storage, caching, performance
```typescript
function getTheme() {
return localStorage.getItem('theme') ?? 'light'
return localStorage.getItem("theme") ?? "light";
}
// Called 10 times = 10 storage reads
```
@@ -21,18 +21,18 @@ function getTheme() {
**Correct (Map cache):**
```typescript
const storageCache = new Map<string, string | null>()
const storageCache = new Map<string, string | null>();
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key))
storageCache.set(key, localStorage.getItem(key));
}
return storageCache.get(key)
return storageCache.get(key);
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value)
storageCache.set(key, value) // keep cache in sync
localStorage.setItem(key, value);
storageCache.set(key, value); // keep cache in sync
}
```
@@ -41,15 +41,13 @@ Use a Map (not a hook) so it works everywhere: utilities, event handlers, not ju
**Cookie caching:**
```typescript
let cookieCache: Record<string, string> | null = null
let cookieCache: Record<string, string> | null = null;
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split('; ').map(c => c.split('='))
)
cookieCache = Object.fromEntries(document.cookie.split("; ").map((c) => c.split("=")));
}
return cookieCache[name]
return cookieCache[name];
}
```
@@ -58,13 +56,13 @@ function getCookie(name: string) {
If storage can change externally (another tab, server-set cookies), invalidate cache:
```typescript
window.addEventListener('storage', (e) => {
if (e.key) storageCache.delete(e.key)
})
window.addEventListener("storage", (e) => {
if (e.key) storageCache.delete(e.key);
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
storageCache.clear()
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
storageCache.clear();
}
})
});
```

View File

@@ -12,21 +12,21 @@ Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine
**Incorrect (3 iterations):**
```typescript
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)
const admins = users.filter((u) => u.isAdmin);
const testers = users.filter((u) => u.isTester);
const inactive = users.filter((u) => !u.isActive);
```
**Correct (1 iteration):**
```typescript
const admins: User[] = []
const testers: User[] = []
const inactive: User[] = []
const admins: User[] = [];
const testers: User[] = [];
const inactive: User[] = [];
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
if (!user.isActive) inactive.push(user)
if (user.isAdmin) admins.push(user);
if (user.isTester) testers.push(user);
if (!user.isActive) inactive.push(user);
}
```

View File

@@ -13,22 +13,22 @@ Return early when result is determined to skip unnecessary processing.
```typescript
function validateUsers(users: User[]) {
let hasError = false
let errorMessage = ''
let hasError = false;
let errorMessage = "";
for (const user of users) {
if (!user.email) {
hasError = true
errorMessage = 'Email required'
hasError = true;
errorMessage = "Email required";
}
if (!user.name) {
hasError = true
errorMessage = 'Name required'
hasError = true;
errorMessage = "Name required";
}
// Continues checking all users even after error found
}
return hasError ? { valid: false, error: errorMessage } : { valid: true }
return hasError ? { valid: false, error: errorMessage } : { valid: true };
}
```
@@ -38,13 +38,13 @@ function validateUsers(users: User[]) {
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: 'Email required' }
return { valid: false, error: "Email required" };
}
if (!user.name) {
return { valid: false, error: 'Name required' }
return { valid: false, error: "Name required" };
}
}
return { valid: true }
return { valid: true };
}
```

View File

@@ -13,24 +13,21 @@ Don't create RegExp inside render. Hoist to module scope or memoize with `useMem
```tsx
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
const regex = new RegExp(`(${query})`, "gi");
const parts = text.split(regex);
return <>{parts.map((part, i) => part)}</>;
}
```
**Correct (memoize or hoist):**
```tsx
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
const regex = useMemo(() => new RegExp(`(${escapeRegex(query)})`, "gi"), [query]);
const parts = text.split(regex);
return <>{parts.map((part, i) => part)}</>;
}
```
@@ -39,7 +36,7 @@ function Highlighter({ text, query }: Props) {
Global regex (`/g`) has mutable `lastIndex` state:
```typescript
const regex = /foo/g
regex.test('foo') // true, lastIndex = 3
regex.test('foo') // false, lastIndex = 0
const regex = /foo/g;
regex.test("foo"); // true, lastIndex = 3
regex.test("foo"); // false, lastIndex = 0
```

View File

@@ -13,10 +13,10 @@ Multiple `.find()` calls by the same key should use a Map.
```typescript
function processOrders(orders: Order[], users: User[]) {
return orders.map(order => ({
return orders.map((order) => ({
...order,
user: users.find(u => u.id === order.userId)
}))
user: users.find((u) => u.id === order.userId),
}));
}
```
@@ -24,12 +24,12 @@ function processOrders(orders: Order[], users: User[]) {
```typescript
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map(u => [u.id, u]))
const userById = new Map(users.map((u) => [u.id, u]));
return orders.map(order => ({
return orders.map((order) => ({
...order,
user: userById.get(order.userId)
}))
user: userById.get(order.userId),
}));
}
```

View File

@@ -16,7 +16,7 @@ In real-world applications, this optimization is especially valuable when the co
```typescript
function hasChanges(current: string[], original: string[]) {
// Always sorts and joins, even when lengths differ
return current.sort().join() !== original.sort().join()
return current.sort().join() !== original.sort().join();
}
```
@@ -28,21 +28,22 @@ Two O(n log n) sorts run even when `current.length` is 5 and `original.length` i
function hasChanges(current: string[], original: string[]) {
// Early return if lengths differ
if (current.length !== original.length) {
return true
return true;
}
// Only sort when lengths match
const currentSorted = current.toSorted()
const originalSorted = original.toSorted()
const currentSorted = current.toSorted();
const originalSorted = original.toSorted();
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true
return true;
}
}
return false
return false;
}
```
This new approach is more efficient because:
- It avoids the overhead of sorting and joining the arrays when lengths differ
- It avoids consuming memory for the joined strings (especially important for large arrays)
- It avoids mutating the original arrays

View File

@@ -13,14 +13,14 @@ Finding the smallest or largest element only requires a single pass through the
```typescript
interface Project {
id: string
name: string
updatedAt: number
id: string;
name: string;
updatedAt: number;
}
function getLatestProject(projects: Project[]) {
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
return sorted[0]
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
return sorted[0];
}
```
@@ -30,8 +30,8 @@ Sorts the entire array just to find the maximum value.
```typescript
function getOldestAndNewest(projects: Project[]) {
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
}
```
@@ -41,31 +41,31 @@ Still sorts unnecessarily when only min/max are needed.
```typescript
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null
if (projects.length === 0) return null;
let latest = projects[0]
let latest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i]
latest = projects[i];
}
}
return latest
return latest;
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null }
if (projects.length === 0) return { oldest: null, newest: null };
let oldest = projects[0]
let newest = projects[0]
let oldest = projects[0];
let newest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i];
}
return { oldest, newest }
return { oldest, newest };
}
```
@@ -74,9 +74,9 @@ Single pass through the array, no copying, no sorting.
**Alternative (Math.min/Math.max for small arrays):**
```typescript
const numbers = [5, 2, 8, 1, 9]
const min = Math.min(...numbers)
const max = Math.max(...numbers)
const numbers = [5, 2, 8, 1, 9];
const min = Math.min(...numbers);
const max = Math.max(...numbers);
```
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.

View File

@@ -12,13 +12,13 @@ Convert arrays to Set/Map for repeated membership checks.
**Incorrect (O(n) per check):**
```typescript
const allowedIds = ['a', 'b', 'c', ...]
items.filter(item => allowedIds.includes(item.id))
const allowedIds = ["a", "b", "c"];
items.filter((item) => allowedIds.includes(item.id));
```
**Correct (O(1) per check):**
```typescript
const allowedIds = new Set(['a', 'b', 'c', ...])
items.filter(item => allowedIds.has(item.id))
const allowedIds = new Set(["a", "b", "c"]);
items.filter((item) => allowedIds.has(item.id));
```

View File

@@ -11,27 +11,21 @@ tags: javascript, arrays, immutability, react, state, mutation
**Incorrect (mutates original array):**
```typescript
```tsx
function UserList({ users }: { users: User[] }) {
// Mutates the users prop array!
const sorted = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
const sorted = useMemo(() => users.sort((a, b) => a.name.localeCompare(b.name)), [users]);
return <div>{sorted.map(renderUser)}</div>;
}
```
**Correct (creates new array):**
```typescript
```tsx
function UserList({ users }: { users: User[] }) {
// Creates new sorted array, original unchanged
const sorted = useMemo(
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
const sorted = useMemo(() => users.toSorted((a, b) => a.name.localeCompare(b.name)), [users]);
return <div>{sorted.map(renderUser)}</div>;
}
```
@@ -46,7 +40,7 @@ function UserList({ users }: { users: User[] }) {
```typescript
// Fallback for older browsers
const sorted = [...items].sort((a, b) => a.value - b.value)
const sorted = [...items].sort((a, b) => a.value - b.value);
```
**Other immutable array methods:**

View File

@@ -12,14 +12,14 @@ Use React's `<Activity>` to preserve state/DOM for expensive components that fre
**Usage:**
```tsx
import { Activity } from 'react'
import { Activity } from "react";
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? 'visible' : 'hidden'}>
<Activity mode={isOpen ? "visible" : "hidden"}>
<ExpensiveMenu />
</Activity>
)
);
}
```

View File

@@ -14,15 +14,10 @@ Many browsers don't have hardware acceleration for CSS3 animations on SVG elemen
```tsx
function LoadingSpinner() {
return (
<svg
className="animate-spin"
width="24"
height="24"
viewBox="0 0 24 24"
>
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
)
);
}
```
@@ -32,15 +27,11 @@ function LoadingSpinner() {
function LoadingSpinner() {
return (
<div className="animate-spin">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
)
);
}
```

View File

@@ -13,11 +13,7 @@ Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count && <span className="badge">{count}</span>}
</div>
)
return <div>{count && <span className="badge">{count}</span>}</div>;
}
// When count = 0, renders: <div>0</div>
@@ -28,11 +24,7 @@ function Badge({ count }: { count: number }) {
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count > 0 ? <span className="badge">{count}</span> : null}
</div>
)
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
}
// When count = 0, renders: <div></div>

View File

@@ -24,14 +24,14 @@ Apply `content-visibility: auto` to defer off-screen rendering.
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map(msg => (
{messages.map((msg) => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
)
);
}
```

View File

@@ -13,31 +13,21 @@ Extract static JSX outside components to avoid re-creation.
```tsx
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />
return <div className="animate-pulse h-20 bg-gray-200" />;
}
function Container() {
return (
<div>
{loading && <LoadingSkeleton />}
</div>
)
return <div>{loading && <LoadingSkeleton />}</div>;
}
```
**Correct (reuses same element):**
```tsx
const loadingSkeleton = (
<div className="animate-pulse h-20 bg-gray-200" />
)
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
function Container() {
return (
<div>
{loading && loadingSkeleton}
</div>
)
return <div>{loading && loadingSkeleton}</div>;
}
```

View File

@@ -14,13 +14,9 @@ When rendering content that depends on client-side storage (localStorage, cookie
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem('theme') || 'light'
const theme = localStorage.getItem("theme") || "light";
return (
<div className={theme}>
{children}
</div>
)
return <div className={theme}>{children}</div>;
}
```
@@ -30,21 +26,17 @@ Server-side rendering will fail because `localStorage` is undefined.
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
const [theme, setTheme] = useState("light");
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem('theme')
const stored = localStorage.getItem("theme");
if (stored) {
setTheme(stored)
setTheme(stored);
}
}, [])
}, []);
return (
<div className={theme}>
{children}
</div>
)
return <div className={theme}>{children}</div>;
}
```
@@ -56,9 +48,7 @@ Component first renders with default value (`light`), then updates after hydrati
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">
{children}
</div>
<div id="theme-wrapper">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `
@@ -73,7 +63,7 @@ function ThemeWrapper({ children }: { children: ReactNode }) {
}}
/>
</>
)
);
}
```

View File

@@ -13,14 +13,14 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const searchParams = useSearchParams();
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
const ref = searchParams.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>
return <button onClick={handleShare}>Share</button>;
}
```
@@ -29,11 +29,11 @@ function ShareButton({ chatId }: { chatId: string }) {
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
const params = new URLSearchParams(window.location.search);
const ref = params.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>
return <button onClick={handleShare}>Share</button>;
}
```

View File

@@ -13,16 +13,16 @@ Specify primitive dependencies instead of objects to minimize effect re-runs.
```tsx
useEffect(() => {
console.log(user.id)
}, [user])
console.log(user.id);
}, [user]);
```
**Correct (re-runs only when id changes):**
```tsx
useEffect(() => {
console.log(user.id)
}, [user.id])
console.log(user.id);
}, [user.id]);
```
**For derived state, compute outside effect:**
@@ -31,15 +31,15 @@ useEffect(() => {
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode()
enableMobileMode();
}
}, [width])
}, [width]);
// Correct: runs only on boolean transition
const isMobile = width < 768
const isMobile = width < 768;
useEffect(() => {
if (isMobile) {
enableMobileMode()
enableMobileMode();
}
}, [isMobile])
}, [isMobile]);
```

View File

@@ -13,9 +13,9 @@ Subscribe to derived boolean state instead of continuous values to reduce re-ren
```tsx
function Sidebar() {
const width = useWindowWidth() // updates continuously
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'} />
const width = useWindowWidth(); // updates continuously
const isMobile = width < 768;
return <nav className={isMobile ? "mobile" : "desktop"} />;
}
```
@@ -23,7 +23,7 @@ function Sidebar() {
```tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'} />
const isMobile = useMediaQuery("(max-width: 767px)");
return <nav className={isMobile ? "mobile" : "desktop"} />;
}
```

View File

@@ -13,19 +13,22 @@ When updating state based on the current state value, use the functional update
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
const [items, setItems] = useState(initialItems);
// Callback must depend on items, recreated on every items change
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items]) // ❌ items dependency causes recreations
const addItems = useCallback(
(newItems: Item[]) => {
setItems([...items, ...newItems]);
},
[items]
); // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id))
}, []) // ❌ Missing items dependency - will use stale items!
setItems(items.filter((item) => item.id !== id));
}, []); // ❌ 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
function TodoList() {
const [items, setItems] = useState(initialItems)
const [items, setItems] = useState(initialItems);
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, []) // ✅ No dependencies needed
setItems((curr) => [...curr, ...newItems]);
}, []); // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id))
}, []) // ✅ Safe and stable
setItems((curr) => curr.filter((item) => item.id !== id));
}, []); // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
}
```

View File

@@ -14,20 +14,18 @@ Pass a function to `useState` for expensive initial values. Without the function
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
const [query, setQuery] = useState('')
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
const [query, setQuery] = useState("");
// When query changes, buildSearchIndex runs again unnecessarily
return <SearchResults index={searchIndex} query={query} />
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem('settings') || '{}')
)
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings") || "{}"));
return <SettingsForm settings={settings} onChange={setSettings} />
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```
@@ -36,20 +34,20 @@ function UserProfile() {
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
const [query, setQuery] = useState('')
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
const [query, setQuery] = useState("");
return <SearchResults index={searchIndex} query={query} />
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
const stored = localStorage.getItem("settings");
return stored ? JSON.parse(stored) : {};
});
return <SettingsForm settings={settings} onChange={setSettings} />
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```

View File

@@ -14,12 +14,12 @@ Extract expensive work into memoized components to enable early returns before c
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user)
return <Avatar id={id} />
}, [user])
const id = computeAvatarId(user);
return <Avatar id={id} />;
}, [user]);
if (loading) return <Skeleton />
return <div>{avatar}</div>
if (loading) return <Skeleton />;
return <div>{avatar}</div>;
}
```
@@ -27,17 +27,17 @@ function Profile({ user, loading }: Props) {
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return <Avatar id={id} />
})
const id = useMemo(() => computeAvatarId(user), [user]);
return <Avatar id={id} />;
});
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
if (loading) return <Skeleton />;
return (
<div>
<UserAvatar user={user} />
</div>
)
);
}
```

View File

@@ -13,28 +13,28 @@ Mark frequent, non-urgent state updates as transitions to maintain UI responsive
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => setScrollY(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
const handler = () => setScrollY(window.scrollY);
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
}
```
**Correct (non-blocking updates):**
```tsx
import { startTransition } from 'react'
import { startTransition } from "react";
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
startTransition(() => setScrollY(window.scrollY));
};
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
}
```

View File

@@ -12,46 +12,46 @@ Use Next.js's `after()` to schedule work that should execute after a response is
**Incorrect (blocks response):**
```tsx
import { logUserAction } from '@/app/utils'
import { logUserAction } from "@/app/utils";
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
await updateDatabase(request);
// Logging blocks the response
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
const userAgent = request.headers.get("user-agent") || "unknown";
await logUserAction({ userAgent });
return new Response(JSON.stringify({ status: 'success' }), {
return new Response(JSON.stringify({ status: "success" }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
headers: { "Content-Type": "application/json" },
});
}
```
**Correct (non-blocking):**
```tsx
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
import { after } from "next/server";
import { headers, cookies } from "next/headers";
import { logUserAction } from "@/app/utils";
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
await updateDatabase(request);
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
const userAgent = (await headers()).get("user-agent") || "unknown";
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,
headers: { 'Content-Type': 'application/json' }
})
headers: { "Content-Type": "application/json" },
});
}
```

View File

@@ -12,20 +12,20 @@ tags: server, cache, lru, cross-request
**Implementation:**
```typescript
import { LRUCache } from 'lru-cache'
import { LRUCache } from "lru-cache";
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000 // 5 minutes
})
ttl: 5 * 60 * 1000, // 5 minutes
});
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const cached = cache.get(id);
if (cached) return cached;
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
const user = await db.user.findUnique({ where: { id } });
cache.set(id, user);
return user;
}
// Request 1: DB query, result cached

View File

@@ -12,15 +12,15 @@ Use `React.cache()` for server-side request deduplication. Authentication and da
**Usage:**
```typescript
import { cache } from 'react'
import { cache } from "react";
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
const session = await auth();
if (!session?.user?.id) return null;
return await db.user.findUnique({
where: { id: session.user.id }
})
})
where: { id: session.user.id },
});
});
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
@@ -33,32 +33,32 @@ Within a single request, multiple calls to `getCurrentUser()` execute the query
```typescript
const getUser = cache(async (params: { uid: number }) => {
return await db.user.findUnique({ where: { id: params.uid } })
})
return await db.user.findUnique({ where: { id: params.uid } });
});
// Each call creates new object, never hits cache
getUser({ uid: 1 })
getUser({ uid: 1 }) // Cache miss, runs query again
getUser({ uid: 1 });
getUser({ uid: 1 }); // Cache miss, runs query again
```
**Correct (cache hit):**
```typescript
const getUser = cache(async (uid: number) => {
return await db.user.findUnique({ where: { id: uid } })
})
return await db.user.findUnique({ where: { id: uid } });
});
// Primitive args use value equality
getUser(1)
getUser(1) // Cache hit, returns cached result
getUser(1);
getUser(1); // Cache hit, returns cached result
```
If you must pass objects, pass the same reference:
```typescript
const params = { uid: 1 }
getUser(params) // Query runs
getUser(params) // Cache hit (same reference)
const params = { uid: 1 };
getUser(params); // Query runs
getUser(params); // Cache hit (same reference)
```
**Next.js-Specific Note:**

View File

@@ -13,18 +13,18 @@ React Server Components execute sequentially within a tree. Restructure with com
```tsx
export default async function Page() {
const header = await fetchHeader()
const header = await fetchHeader();
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
)
);
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
```
@@ -32,13 +32,13 @@ async function Sidebar() {
```tsx
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
export default function Page() {
@@ -47,7 +47,7 @@ export default function Page() {
<Header />
<Sidebar />
</div>
)
);
}
```
@@ -55,13 +55,13 @@ export default function Page() {
```tsx
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
function Layout({ children }: { children: ReactNode }) {
@@ -70,7 +70,7 @@ function Layout({ children }: { children: ReactNode }) {
<Header />
{children}
</div>
)
);
}
export default function Page() {
@@ -78,6 +78,6 @@ export default function Page() {
<Layout>
<Sidebar />
</Layout>
)
);
}
```

View File

@@ -13,13 +13,13 @@ The React Server/Client boundary serializes all object properties into strings a
```tsx
async function Page() {
const user = await fetchUser() // 50 fields
return <Profile user={user} />
const user = await fetchUser(); // 50 fields
return <Profile user={user} />;
}
'use client'
("use client");
function Profile({ user }: { user: User }) {
return <div>{user.name}</div> // uses 1 field
return <div>{user.name}</div>; // uses 1 field
}
```
@@ -27,12 +27,12 @@ function Profile({ user }: { user: User }) {
```tsx
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} />
const user = await fetchUser();
return <Profile name={user.name} />;
}
'use client'
("use client");
function Profile({ name }: { name: string }) {
return <div>{name}</div>
return <div>{name}</div>;
}
```

View File

@@ -31,6 +31,7 @@ Use WebFetch to retrieve the latest rules. The fetched content contains all the
## Usage
When a user provides a file or pattern argument:
1. Fetch guidelines from the source URL above
2. Read the specified files
3. Apply all rules from the fetched guidelines

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ Comprehensive performance optimization guide for React and Next.js applications,
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
@@ -23,7 +24,7 @@ Reference these guidelines when:
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| -------- | ------------------------- | ----------- | ------------ |
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
@@ -115,6 +116,7 @@ rules/_sections.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation

View File

@@ -14,9 +14,9 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
useEffect(() => {
window.addEventListener(event, handler)
return () => window.removeEventListener(event, handler)
}, [event, handler])
window.addEventListener(event, handler);
return () => window.removeEventListener(event, handler);
}, [event, handler]);
}
```
@@ -24,31 +24,31 @@ function useWindowEvent(event: string, handler: (e) => void) {
```tsx
function useWindowEvent(event: string, handler: (e) => void) {
const handlerRef = useRef(handler)
const handlerRef = useRef(handler);
useEffect(() => {
handlerRef.current = handler
}, [handler])
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const listener = (e) => handlerRef.current(e)
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}, [event])
const listener = (e) => handlerRef.current(e);
window.addEventListener(event, listener);
return () => window.removeEventListener(event, listener);
}, [event]);
}
```
**Alternative: use `useEffectEvent` if you're on latest React:**
```tsx
import { useEffectEvent } from 'react'
import { useEffectEvent } from "react";
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler)
const onEvent = useEffectEvent(handler);
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
window.addEventListener(event, onEvent);
return () => window.removeEventListener(event, onEvent);
}, [event]);
}
```

View File

@@ -13,11 +13,11 @@ Access latest values in callbacks without adding them to dependency arrays. Prev
```typescript
function useLatest<T>(value: T) {
const ref = useRef(value)
const ref = useRef(value);
useLayoutEffect(() => {
ref.current = value
}, [value])
return ref
ref.current = value;
}, [value]);
return ref;
}
```
@@ -25,12 +25,12 @@ function useLatest<T>(value: T) {
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const [query, setQuery] = useState("");
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300)
return () => clearTimeout(timeout)
}, [query, onSearch])
const timeout = setTimeout(() => onSearch(query), 300);
return () => clearTimeout(timeout);
}, [query, onSearch]);
}
```
@@ -38,12 +38,12 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
```tsx
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const onSearchRef = useLatest(onSearch)
const [query, setQuery] = useState("");
const onSearchRef = useLatest(onSearch);
useEffect(() => {
const timeout = setTimeout(() => onSearchRef.current(query), 300)
return () => clearTimeout(timeout)
}, [query])
const timeout = setTimeout(() => onSearchRef.current(query), 300);
return () => clearTimeout(timeout);
}, [query]);
}
```

View File

@@ -13,10 +13,10 @@ In API routes and Server Actions, start independent operations immediately, even
```typescript
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
const session = await auth();
const config = await fetchConfig();
const data = await fetchData(session.user.id);
return Response.json({ data, config });
}
```
@@ -24,14 +24,11 @@ export async function GET(request: Request) {
```typescript
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
const sessionPromise = auth();
const configPromise = fetchConfig();
const session = await sessionPromise;
const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]);
return Response.json({ data, config });
}
```

View File

@@ -13,15 +13,15 @@ Move `await` operations into the branches where they're actually used to avoid b
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
const userData = await fetchUserData(userId);
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true }
return { skipped: true };
}
// Only this branch uses userData
return processUserData(userData)
return processUserData(userData);
}
```
@@ -31,12 +31,12 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true }
return { skipped: true };
}
// Fetch only when needed
const userData = await fetchUserData(userId)
return processUserData(userData)
const userData = await fetchUserData(userId);
return processUserData(userData);
}
```
@@ -45,35 +45,35 @@ async function handleRequest(userId: string, skipProcessing: boolean) {
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
const permissions = await fetchPermissions(userId);
const resource = await getResource(resourceId);
if (!resource) {
return { error: 'Not found' }
return { error: "Not found" };
}
if (!permissions.canEdit) {
return { error: 'Forbidden' }
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions)
return await updateResourceData(resource, permissions);
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
const resource = await getResource(resourceId);
if (!resource) {
return { error: 'Not found' }
return { error: "Not found" };
}
const permissions = await fetchPermissions(userId)
const permissions = await fetchPermissions(userId);
if (!permissions.canEdit) {
return { error: 'Forbidden' }
return { error: "Forbidden" };
}
return await updateResourceData(resource, permissions)
return await updateResourceData(resource, permissions);
}
```

View File

@@ -12,25 +12,26 @@ For operations with partial dependencies, use `better-all` to maximize paralleli
**Incorrect (profile waits for config unnecessarily):**
```typescript
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
const profile = await fetchProfile(user.id);
```
**Correct (config and profile run in parallel):**
```typescript
import { all } from 'better-all'
import { all } from "better-all";
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async user() {
return fetchUser();
},
async config() {
return fetchConfig();
},
async profile() {
return fetchProfile((await this.$.user).id)
}
})
return fetchProfile((await this.$.user).id);
},
});
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)

View File

@@ -12,17 +12,13 @@ When async operations have no interdependencies, execute them concurrently using
**Incorrect (sequential execution, 3 round trips):**
```typescript
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
```
**Correct (parallel execution, 1 round trip):**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);
```

View File

@@ -13,7 +13,7 @@ Instead of awaiting data in async components before returning JSX, use Suspense
```tsx
async function Page() {
const data = await fetchData() // Blocks entire page
const data = await fetchData(); // Blocks entire page
return (
<div>
@@ -24,7 +24,7 @@ async function Page() {
</div>
<div>Footer</div>
</div>
)
);
}
```
@@ -45,12 +45,12 @@ function Page() {
</div>
<div>Footer</div>
</div>
)
);
}
async function DataDisplay() {
const data = await fetchData() // Only blocks this component
return <div>{data.content}</div>
const data = await fetchData(); // Only blocks this component
return <div>{data.content}</div>;
}
```
@@ -61,7 +61,7 @@ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
```tsx
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData()
const dataPromise = fetchData();
return (
<div>
@@ -73,17 +73,17 @@ function Page() {
</Suspense>
<div>Footer</div>
</div>
)
);
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Unwraps the promise
return <div>{data.content}</div>
const data = use(dataPromise); // Unwraps the promise
return <div>{data.content}</div>;
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Reuses the same promise
return <div>{data.summary}</div>
const data = use(dataPromise); // Reuses the same promise
return <div>{data.summary}</div>;
}
```

View File

@@ -16,24 +16,24 @@ Popular icon and component libraries can have **up to 10,000 re-exports** in the
**Incorrect (imports entire library):**
```tsx
import { Check, X, Menu } from 'lucide-react'
import { Check, X, Menu } from "lucide-react";
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from '@mui/material'
import { Button, TextField } from "@mui/material";
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct (imports only what you need):**
```tsx
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
import Check from "lucide-react/dist/esm/icons/check";
import X from "lucide-react/dist/esm/icons/x";
import Menu from "lucide-react/dist/esm/icons/menu";
// Loads only 3 modules (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
// Loads only what you use
```
@@ -43,12 +43,12 @@ import TextField from '@mui/material/TextField'
// next.config.js - use optimizePackageImports
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
optimizePackageImports: ["lucide-react", "@mui/material"],
},
};
// Then you can keep the ergonomic barrel imports:
import { Check, X, Menu } from 'lucide-react'
import { Check, X, Menu } from "lucide-react";
// Automatically transformed to direct imports at build time
```

View File

@@ -12,19 +12,25 @@ Load large data or modules only when a feature is activated.
**Example (lazy-load animation frames):**
```tsx
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
const [frames, setFrames] = useState<Frame[] | null>(null)
function AnimationPlayer({
enabled,
setEnabled,
}: {
enabled: boolean;
setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const [frames, setFrames] = useState<Frame[] | null>(null);
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
.catch(() => setEnabled(false))
if (enabled && !frames && typeof window !== "undefined") {
import("./animation-frames.js")
.then((mod) => setFrames(mod.frames))
.catch(() => setEnabled(false));
}
}, [enabled, frames, setEnabled])
}, [enabled, frames, setEnabled]);
if (!frames) return <Skeleton />
return <Canvas frames={frames} />
if (!frames) return <Skeleton />;
return <Canvas frames={frames} />;
}
```

View File

@@ -12,7 +12,7 @@ Analytics, logging, and error tracking don't block user interaction. Load them a
**Incorrect (blocks initial bundle):**
```tsx
import { Analytics } from '@vercel/analytics/react'
import { Analytics } from "@vercel/analytics/react";
export default function RootLayout({ children }) {
return (
@@ -22,19 +22,18 @@ export default function RootLayout({ children }) {
<Analytics />
</body>
</html>
)
);
}
```
**Correct (loads after hydration):**
```tsx
import dynamic from 'next/dynamic'
import dynamic from "next/dynamic";
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
const Analytics = dynamic(() => import("@vercel/analytics/react").then((m) => m.Analytics), {
ssr: false,
});
export default function RootLayout({ children }) {
return (
@@ -44,6 +43,6 @@ export default function RootLayout({ children }) {
<Analytics />
</body>
</html>
)
);
}
```

View File

@@ -12,24 +12,23 @@ Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect (Monaco bundles with main chunk ~300KB):**
```tsx
import { MonacoEditor } from './monaco-editor'
import { MonacoEditor } from "./monaco-editor";
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
return <MonacoEditor value={code} />;
}
```
**Correct (Monaco loads on demand):**
```tsx
import dynamic from 'next/dynamic'
import dynamic from "next/dynamic";
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
const MonacoEditor = dynamic(() => import("./monaco-editor").then((m) => m.MonacoEditor), {
ssr: false,
});
function CodePanel({ code }: { code: string }) {
return <MonacoEditor value={code} />
return <MonacoEditor value={code} />;
}
```

View File

@@ -14,20 +14,16 @@ Preload heavy bundles before they're needed to reduce perceived latency.
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
if (typeof window !== "undefined") {
void import("./monaco-editor");
}
};
return (
<button
onMouseEnter={preload}
onFocus={preload}
onClick={onClick}
>
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
)
);
}
```
@@ -36,14 +32,12 @@ function EditorButton({ onClick }: { onClick: () => void }) {
```tsx
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== 'undefined') {
void import('./monaco-editor').then(mod => mod.init())
if (flags.editorEnabled && typeof window !== "undefined") {
void import("./monaco-editor").then((mod) => mod.init());
}
}, [flags.editorEnabled])
}, [flags.editorEnabled]);
return <FlagsContext.Provider value={flags}>
{children}
</FlagsContext.Provider>
return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
}
```

View File

@@ -16,12 +16,12 @@ function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback()
callback();
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [key, callback])
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [key, callback]);
}
```
@@ -30,45 +30,49 @@ When using the `useKeyboardShortcut` hook multiple times, each instance will reg
**Correct (N instances = 1 listener):**
```tsx
import useSWRSubscription from 'swr/subscription'
import useSWRSubscription from "swr/subscription";
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set<() => void>>()
const keyCallbacks = new Map<string, Set<() => void>>();
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set())
keyCallbacks.set(key, new Set());
}
keyCallbacks.get(key)!.add(callback)
keyCallbacks.get(key)!.add(callback);
return () => {
const set = keyCallbacks.get(key)
const set = keyCallbacks.get(key);
if (set) {
set.delete(callback)
set.delete(callback);
if (set.size === 0) {
keyCallbacks.delete(key)
keyCallbacks.delete(key);
}
}
}
}, [key, callback])
};
}, [key, callback]);
useSWRSubscription('global-keydown', () => {
useSWRSubscription("global-keydown", () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
keyCallbacks.get(e.key)!.forEach((cb) => cb());
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
})
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
});
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut('p', () => { /* ... */ })
useKeyboardShortcut('k', () => { /* ... */ })
useKeyboardShortcut("p", () => {
/* ... */
});
useKeyboardShortcut("k", () => {
/* ... */
});
// ...
}
```

View File

@@ -13,18 +13,18 @@ Add version prefix to keys and store only needed fields. Prevents schema conflic
```typescript
// No version, stores everything, no error handling
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
const data = localStorage.getItem('userConfig')
localStorage.setItem("userConfig", JSON.stringify(fullUserObject));
const data = localStorage.getItem("userConfig");
```
**Correct:**
```typescript
const VERSION = 'v2'
const VERSION = "v2";
function saveConfig(config: { theme: string; language: string }) {
try {
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config));
} catch {
// Throws in incognito/private browsing, quota exceeded, or disabled
}
@@ -32,21 +32,21 @@ function saveConfig(config: { theme: string; language: string }) {
function loadConfig() {
try {
const data = localStorage.getItem(`userConfig:${VERSION}`)
return data ? JSON.parse(data) : null
const data = localStorage.getItem(`userConfig:${VERSION}`);
return data ? JSON.parse(data) : null;
} catch {
return null
return null;
}
}
// Migration from v1 to v2
function migrate() {
try {
const v1 = localStorage.getItem('userConfig:v1')
const v1 = localStorage.getItem("userConfig:v1");
if (v1) {
const old = JSON.parse(v1)
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
localStorage.removeItem('userConfig:v1')
const old = JSON.parse(v1);
saveConfig({ theme: old.darkMode ? "dark" : "light", language: old.lang });
localStorage.removeItem("userConfig:v1");
}
} catch {}
}
@@ -58,10 +58,13 @@ function migrate() {
// User object has 20+ fields, only store what UI needs
function cachePrefs(user: FullUser) {
try {
localStorage.setItem('prefs:v1', JSON.stringify({
localStorage.setItem(
"prefs:v1",
JSON.stringify({
theme: user.preferences.theme,
notifications: user.preferences.notifications
}))
notifications: user.preferences.notifications,
})
);
} catch {}
}
```

View File

@@ -13,34 +13,34 @@ Add `{ passive: true }` to touch and wheel event listeners to enable immediate s
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener('touchstart', handleTouch)
document.addEventListener('wheel', handleWheel)
document.addEventListener("touchstart", handleTouch);
document.addEventListener("wheel", handleWheel);
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
document.removeEventListener("touchstart", handleTouch);
document.removeEventListener("wheel", handleWheel);
};
}, []);
```
**Correct:**
```typescript
useEffect(() => {
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX);
const handleWheel = (e: WheelEvent) => console.log(e.deltaY);
document.addEventListener('touchstart', handleTouch, { passive: true })
document.addEventListener('wheel', handleWheel, { passive: true })
document.addEventListener("touchstart", handleTouch, { passive: true });
document.addEventListener("wheel", handleWheel, { passive: true });
return () => {
document.removeEventListener('touchstart', handleTouch)
document.removeEventListener('wheel', handleWheel)
}
}, [])
document.removeEventListener("touchstart", handleTouch);
document.removeEventListener("wheel", handleWheel);
};
}, []);
```
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.

View File

@@ -13,43 +13,43 @@ SWR enables request deduplication, caching, and revalidation across component in
```tsx
function UserList() {
const [users, setUsers] = useState([])
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
fetch("/api/users")
.then((r) => r.json())
.then(setUsers);
}, []);
}
```
**Correct (multiple instances share one request):**
```tsx
import useSWR from 'swr'
import useSWR from "swr";
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
const { data: users } = useSWR("/api/users", fetcher);
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from '@/lib/swr'
import { useImmutableSWR } from "@/lib/swr";
function StaticContent() {
const { data } = useImmutableSWR('/api/config', fetcher)
const { data } = useImmutableSWR("/api/config", fetcher);
}
```
**For mutations:**
```tsx
import { useSWRMutation } from 'swr/mutation'
import { useSWRMutation } from "swr/mutation";
function UpdateButton() {
const { trigger } = useSWRMutation('/api/user', updateUser)
return <button onClick={() => trigger()}>Update</button>
const { trigger } = useSWRMutation("/api/user", updateUser);
return <button onClick={() => trigger()}>Update</button>;
}
```

View File

@@ -13,10 +13,10 @@ Avoid interleaving style writes with layout reads. When you read a layout proper
```typescript
function updateElementStyles(element: HTMLElement) {
element.style.width = '100px'
const width = element.offsetWidth // Forces reflow
element.style.height = '200px'
const height = element.offsetHeight // Forces another reflow
element.style.width = "100px";
const width = element.offsetWidth; // Forces reflow
element.style.height = "200px";
const height = element.offsetHeight; // Forces another reflow
}
```
@@ -25,13 +25,13 @@ function updateElementStyles(element: HTMLElement) {
```typescript
function updateElementStyles(element: HTMLElement) {
// Batch all writes together
element.style.width = '100px'
element.style.height = '200px'
element.style.backgroundColor = 'blue'
element.style.border = '1px solid black'
element.style.width = "100px";
element.style.height = "200px";
element.style.backgroundColor = "blue";
element.style.border = "1px solid black";
// Read after all writes are done (single reflow)
const { width, height } = element.getBoundingClientRect()
const { width, height } = element.getBoundingClientRect();
}
```
@@ -48,9 +48,9 @@ function updateElementStyles(element: HTMLElement) {
```typescript
function updateElementStyles(element: HTMLElement) {
element.classList.add('highlighted-box')
element.classList.add("highlighted-box");
const { width, height } = element.getBoundingClientRect()
const { width, height } = element.getBoundingClientRect();
}
```

View File

@@ -11,67 +11,67 @@ Use a module-level Map to cache function results when the same function is calle
**Incorrect (redundant computation):**
```typescript
```tsx
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
{projects.map((project) => {
// 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>
)
);
}
```
**Correct (cached results):**
```typescript
```tsx
// Module-level cache
const slugifyCache = new Map<string, string>()
const slugifyCache = new Map<string, string>();
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) {
return slugifyCache.get(text)!
return slugifyCache.get(text)!;
}
const result = slugify(text)
slugifyCache.set(text, result)
return result
const result = slugify(text);
slugifyCache.set(text, result);
return result;
}
function ProjectList({ projects }: { projects: Project[] }) {
return (
<div>
{projects.map(project => {
{projects.map((project) => {
// 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>
)
);
}
```
**Simpler pattern for single-value functions:**
```typescript
let isLoggedInCache: boolean | null = null
let isLoggedInCache: boolean | null = null;
function isLoggedIn(): boolean {
if (isLoggedInCache !== null) {
return isLoggedInCache
return isLoggedInCache;
}
isLoggedInCache = document.cookie.includes('auth=')
return isLoggedInCache
isLoggedInCache = document.cookie.includes("auth=");
return isLoggedInCache;
}
// Clear cache when auth changes
function onAuthChange() {
isLoggedInCache = null
isLoggedInCache = null;
}
```

View File

@@ -13,16 +13,16 @@ Cache object property lookups in hot paths.
```typescript
for (let i = 0; i < arr.length; i++) {
process(obj.config.settings.value)
process(obj.config.settings.value);
}
```
**Correct (1 lookup total):**
```typescript
const value = obj.config.settings.value
const len = arr.length
const value = obj.config.settings.value;
const len = arr.length;
for (let i = 0; i < len; i++) {
process(value)
process(value);
}
```

View File

@@ -13,7 +13,7 @@ tags: javascript, localStorage, storage, caching, performance
```typescript
function getTheme() {
return localStorage.getItem('theme') ?? 'light'
return localStorage.getItem("theme") ?? "light";
}
// Called 10 times = 10 storage reads
```
@@ -21,18 +21,18 @@ function getTheme() {
**Correct (Map cache):**
```typescript
const storageCache = new Map<string, string | null>()
const storageCache = new Map<string, string | null>();
function getLocalStorage(key: string) {
if (!storageCache.has(key)) {
storageCache.set(key, localStorage.getItem(key))
storageCache.set(key, localStorage.getItem(key));
}
return storageCache.get(key)
return storageCache.get(key);
}
function setLocalStorage(key: string, value: string) {
localStorage.setItem(key, value)
storageCache.set(key, value) // keep cache in sync
localStorage.setItem(key, value);
storageCache.set(key, value); // keep cache in sync
}
```
@@ -41,15 +41,13 @@ Use a Map (not a hook) so it works everywhere: utilities, event handlers, not ju
**Cookie caching:**
```typescript
let cookieCache: Record<string, string> | null = null
let cookieCache: Record<string, string> | null = null;
function getCookie(name: string) {
if (!cookieCache) {
cookieCache = Object.fromEntries(
document.cookie.split('; ').map(c => c.split('='))
)
cookieCache = Object.fromEntries(document.cookie.split("; ").map((c) => c.split("=")));
}
return cookieCache[name]
return cookieCache[name];
}
```
@@ -58,13 +56,13 @@ function getCookie(name: string) {
If storage can change externally (another tab, server-set cookies), invalidate cache:
```typescript
window.addEventListener('storage', (e) => {
if (e.key) storageCache.delete(e.key)
})
window.addEventListener("storage", (e) => {
if (e.key) storageCache.delete(e.key);
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
storageCache.clear()
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
storageCache.clear();
}
})
});
```

View File

@@ -12,21 +12,21 @@ Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine
**Incorrect (3 iterations):**
```typescript
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
const inactive = users.filter(u => !u.isActive)
const admins = users.filter((u) => u.isAdmin);
const testers = users.filter((u) => u.isTester);
const inactive = users.filter((u) => !u.isActive);
```
**Correct (1 iteration):**
```typescript
const admins: User[] = []
const testers: User[] = []
const inactive: User[] = []
const admins: User[] = [];
const testers: User[] = [];
const inactive: User[] = [];
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
if (!user.isActive) inactive.push(user)
if (user.isAdmin) admins.push(user);
if (user.isTester) testers.push(user);
if (!user.isActive) inactive.push(user);
}
```

View File

@@ -13,22 +13,22 @@ Return early when result is determined to skip unnecessary processing.
```typescript
function validateUsers(users: User[]) {
let hasError = false
let errorMessage = ''
let hasError = false;
let errorMessage = "";
for (const user of users) {
if (!user.email) {
hasError = true
errorMessage = 'Email required'
hasError = true;
errorMessage = "Email required";
}
if (!user.name) {
hasError = true
errorMessage = 'Name required'
hasError = true;
errorMessage = "Name required";
}
// Continues checking all users even after error found
}
return hasError ? { valid: false, error: errorMessage } : { valid: true }
return hasError ? { valid: false, error: errorMessage } : { valid: true };
}
```
@@ -38,13 +38,13 @@ function validateUsers(users: User[]) {
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) {
return { valid: false, error: 'Email required' }
return { valid: false, error: "Email required" };
}
if (!user.name) {
return { valid: false, error: 'Name required' }
return { valid: false, error: "Name required" };
}
}
return { valid: true }
return { valid: true };
}
```

View File

@@ -13,24 +13,21 @@ Don't create RegExp inside render. Hoist to module scope or memoize with `useMem
```tsx
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
const regex = new RegExp(`(${query})`, "gi");
const parts = text.split(regex);
return <>{parts.map((part, i) => part)}</>;
}
```
**Correct (memoize or hoist):**
```tsx
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
const regex = useMemo(() => new RegExp(`(${escapeRegex(query)})`, "gi"), [query]);
const parts = text.split(regex);
return <>{parts.map((part, i) => part)}</>;
}
```
@@ -39,7 +36,7 @@ function Highlighter({ text, query }: Props) {
Global regex (`/g`) has mutable `lastIndex` state:
```typescript
const regex = /foo/g
regex.test('foo') // true, lastIndex = 3
regex.test('foo') // false, lastIndex = 0
const regex = /foo/g;
regex.test("foo"); // true, lastIndex = 3
regex.test("foo"); // false, lastIndex = 0
```

View File

@@ -13,10 +13,10 @@ Multiple `.find()` calls by the same key should use a Map.
```typescript
function processOrders(orders: Order[], users: User[]) {
return orders.map(order => ({
return orders.map((order) => ({
...order,
user: users.find(u => u.id === order.userId)
}))
user: users.find((u) => u.id === order.userId),
}));
}
```
@@ -24,12 +24,12 @@ function processOrders(orders: Order[], users: User[]) {
```typescript
function processOrders(orders: Order[], users: User[]) {
const userById = new Map(users.map(u => [u.id, u]))
const userById = new Map(users.map((u) => [u.id, u]));
return orders.map(order => ({
return orders.map((order) => ({
...order,
user: userById.get(order.userId)
}))
user: userById.get(order.userId),
}));
}
```

View File

@@ -16,7 +16,7 @@ In real-world applications, this optimization is especially valuable when the co
```typescript
function hasChanges(current: string[], original: string[]) {
// Always sorts and joins, even when lengths differ
return current.sort().join() !== original.sort().join()
return current.sort().join() !== original.sort().join();
}
```
@@ -28,21 +28,22 @@ Two O(n log n) sorts run even when `current.length` is 5 and `original.length` i
function hasChanges(current: string[], original: string[]) {
// Early return if lengths differ
if (current.length !== original.length) {
return true
return true;
}
// Only sort when lengths match
const currentSorted = current.toSorted()
const originalSorted = original.toSorted()
const currentSorted = current.toSorted();
const originalSorted = original.toSorted();
for (let i = 0; i < currentSorted.length; i++) {
if (currentSorted[i] !== originalSorted[i]) {
return true
return true;
}
}
return false
return false;
}
```
This new approach is more efficient because:
- It avoids the overhead of sorting and joining the arrays when lengths differ
- It avoids consuming memory for the joined strings (especially important for large arrays)
- It avoids mutating the original arrays

View File

@@ -13,14 +13,14 @@ Finding the smallest or largest element only requires a single pass through the
```typescript
interface Project {
id: string
name: string
updatedAt: number
id: string;
name: string;
updatedAt: number;
}
function getLatestProject(projects: Project[]) {
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
return sorted[0]
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
return sorted[0];
}
```
@@ -30,8 +30,8 @@ Sorts the entire array just to find the maximum value.
```typescript
function getOldestAndNewest(projects: Project[]) {
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
}
```
@@ -41,31 +41,31 @@ Still sorts unnecessarily when only min/max are needed.
```typescript
function getLatestProject(projects: Project[]) {
if (projects.length === 0) return null
if (projects.length === 0) return null;
let latest = projects[0]
let latest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt > latest.updatedAt) {
latest = projects[i]
latest = projects[i];
}
}
return latest
return latest;
}
function getOldestAndNewest(projects: Project[]) {
if (projects.length === 0) return { oldest: null, newest: null }
if (projects.length === 0) return { oldest: null, newest: null };
let oldest = projects[0]
let newest = projects[0]
let oldest = projects[0];
let newest = projects[0];
for (let i = 1; i < projects.length; i++) {
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i];
}
return { oldest, newest }
return { oldest, newest };
}
```
@@ -74,9 +74,9 @@ Single pass through the array, no copying, no sorting.
**Alternative (Math.min/Math.max for small arrays):**
```typescript
const numbers = [5, 2, 8, 1, 9]
const min = Math.min(...numbers)
const max = Math.max(...numbers)
const numbers = [5, 2, 8, 1, 9];
const min = Math.min(...numbers);
const max = Math.max(...numbers);
```
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.

View File

@@ -12,13 +12,13 @@ Convert arrays to Set/Map for repeated membership checks.
**Incorrect (O(n) per check):**
```typescript
const allowedIds = ['a', 'b', 'c', ...]
items.filter(item => allowedIds.includes(item.id))
const allowedIds = ["a", "b", "c"];
items.filter((item) => allowedIds.includes(item.id));
```
**Correct (O(1) per check):**
```typescript
const allowedIds = new Set(['a', 'b', 'c', ...])
items.filter(item => allowedIds.has(item.id))
const allowedIds = new Set(["a", "b", "c"]);
items.filter((item) => allowedIds.has(item.id));
```

View File

@@ -11,27 +11,21 @@ tags: javascript, arrays, immutability, react, state, mutation
**Incorrect (mutates original array):**
```typescript
```tsx
function UserList({ users }: { users: User[] }) {
// Mutates the users prop array!
const sorted = useMemo(
() => users.sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
const sorted = useMemo(() => users.sort((a, b) => a.name.localeCompare(b.name)), [users]);
return <div>{sorted.map(renderUser)}</div>;
}
```
**Correct (creates new array):**
```typescript
```tsx
function UserList({ users }: { users: User[] }) {
// Creates new sorted array, original unchanged
const sorted = useMemo(
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
[users]
)
return <div>{sorted.map(renderUser)}</div>
const sorted = useMemo(() => users.toSorted((a, b) => a.name.localeCompare(b.name)), [users]);
return <div>{sorted.map(renderUser)}</div>;
}
```
@@ -46,7 +40,7 @@ function UserList({ users }: { users: User[] }) {
```typescript
// Fallback for older browsers
const sorted = [...items].sort((a, b) => a.value - b.value)
const sorted = [...items].sort((a, b) => a.value - b.value);
```
**Other immutable array methods:**

View File

@@ -12,14 +12,14 @@ Use React's `<Activity>` to preserve state/DOM for expensive components that fre
**Usage:**
```tsx
import { Activity } from 'react'
import { Activity } from "react";
function Dropdown({ isOpen }: Props) {
return (
<Activity mode={isOpen ? 'visible' : 'hidden'}>
<Activity mode={isOpen ? "visible" : "hidden"}>
<ExpensiveMenu />
</Activity>
)
);
}
```

View File

@@ -14,15 +14,10 @@ Many browsers don't have hardware acceleration for CSS3 animations on SVG elemen
```tsx
function LoadingSpinner() {
return (
<svg
className="animate-spin"
width="24"
height="24"
viewBox="0 0 24 24"
>
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
)
);
}
```
@@ -32,15 +27,11 @@ function LoadingSpinner() {
function LoadingSpinner() {
return (
<div className="animate-spin">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
>
<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" />
</svg>
</div>
)
);
}
```

View File

@@ -13,11 +13,7 @@ Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count && <span className="badge">{count}</span>}
</div>
)
return <div>{count && <span className="badge">{count}</span>}</div>;
}
// When count = 0, renders: <div>0</div>
@@ -28,11 +24,7 @@ function Badge({ count }: { count: number }) {
```tsx
function Badge({ count }: { count: number }) {
return (
<div>
{count > 0 ? <span className="badge">{count}</span> : null}
</div>
)
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
}
// When count = 0, renders: <div></div>

View File

@@ -24,14 +24,14 @@ Apply `content-visibility: auto` to defer off-screen rendering.
function MessageList({ messages }: { messages: Message[] }) {
return (
<div className="overflow-y-auto h-screen">
{messages.map(msg => (
{messages.map((msg) => (
<div key={msg.id} className="message-item">
<Avatar user={msg.author} />
<div>{msg.content}</div>
</div>
))}
</div>
)
);
}
```

View File

@@ -13,31 +13,21 @@ Extract static JSX outside components to avoid re-creation.
```tsx
function LoadingSkeleton() {
return <div className="animate-pulse h-20 bg-gray-200" />
return <div className="animate-pulse h-20 bg-gray-200" />;
}
function Container() {
return (
<div>
{loading && <LoadingSkeleton />}
</div>
)
return <div>{loading && <LoadingSkeleton />}</div>;
}
```
**Correct (reuses same element):**
```tsx
const loadingSkeleton = (
<div className="animate-pulse h-20 bg-gray-200" />
)
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
function Container() {
return (
<div>
{loading && loadingSkeleton}
</div>
)
return <div>{loading && loadingSkeleton}</div>;
}
```

View File

@@ -14,13 +14,9 @@ When rendering content that depends on client-side storage (localStorage, cookie
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
// localStorage is not available on server - throws error
const theme = localStorage.getItem('theme') || 'light'
const theme = localStorage.getItem("theme") || "light";
return (
<div className={theme}>
{children}
</div>
)
return <div className={theme}>{children}</div>;
}
```
@@ -30,21 +26,17 @@ Server-side rendering will fail because `localStorage` is undefined.
```tsx
function ThemeWrapper({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light')
const [theme, setTheme] = useState("light");
useEffect(() => {
// Runs after hydration - causes visible flash
const stored = localStorage.getItem('theme')
const stored = localStorage.getItem("theme");
if (stored) {
setTheme(stored)
setTheme(stored);
}
}, [])
}, []);
return (
<div className={theme}>
{children}
</div>
)
return <div className={theme}>{children}</div>;
}
```
@@ -56,9 +48,7 @@ Component first renders with default value (`light`), then updates after hydrati
function ThemeWrapper({ children }: { children: ReactNode }) {
return (
<>
<div id="theme-wrapper">
{children}
</div>
<div id="theme-wrapper">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `
@@ -73,7 +63,7 @@ function ThemeWrapper({ children }: { children: ReactNode }) {
}}
/>
</>
)
);
}
```

View File

@@ -13,14 +13,14 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const searchParams = useSearchParams();
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
const ref = searchParams.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>
return <button onClick={handleShare}>Share</button>;
}
```
@@ -29,11 +29,11 @@ function ShareButton({ chatId }: { chatId: string }) {
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
const params = new URLSearchParams(window.location.search);
const ref = params.get("ref");
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>
return <button onClick={handleShare}>Share</button>;
}
```

View File

@@ -13,16 +13,16 @@ Specify primitive dependencies instead of objects to minimize effect re-runs.
```tsx
useEffect(() => {
console.log(user.id)
}, [user])
console.log(user.id);
}, [user]);
```
**Correct (re-runs only when id changes):**
```tsx
useEffect(() => {
console.log(user.id)
}, [user.id])
console.log(user.id);
}, [user.id]);
```
**For derived state, compute outside effect:**
@@ -31,15 +31,15 @@ useEffect(() => {
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode()
enableMobileMode();
}
}, [width])
}, [width]);
// Correct: runs only on boolean transition
const isMobile = width < 768
const isMobile = width < 768;
useEffect(() => {
if (isMobile) {
enableMobileMode()
enableMobileMode();
}
}, [isMobile])
}, [isMobile]);
```

View File

@@ -13,9 +13,9 @@ Subscribe to derived boolean state instead of continuous values to reduce re-ren
```tsx
function Sidebar() {
const width = useWindowWidth() // updates continuously
const isMobile = width < 768
return <nav className={isMobile ? 'mobile' : 'desktop'} />
const width = useWindowWidth(); // updates continuously
const isMobile = width < 768;
return <nav className={isMobile ? "mobile" : "desktop"} />;
}
```
@@ -23,7 +23,7 @@ function Sidebar() {
```tsx
function Sidebar() {
const isMobile = useMediaQuery('(max-width: 767px)')
return <nav className={isMobile ? 'mobile' : 'desktop'} />
const isMobile = useMediaQuery("(max-width: 767px)");
return <nav className={isMobile ? "mobile" : "desktop"} />;
}
```

View File

@@ -13,19 +13,22 @@ When updating state based on the current state value, use the functional update
```tsx
function TodoList() {
const [items, setItems] = useState(initialItems)
const [items, setItems] = useState(initialItems);
// Callback must depend on items, recreated on every items change
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items]) // ❌ items dependency causes recreations
const addItems = useCallback(
(newItems: Item[]) => {
setItems([...items, ...newItems]);
},
[items]
); // ❌ items dependency causes recreations
// Risk of stale closure if dependency is forgotten
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id))
}, []) // ❌ Missing items dependency - will use stale items!
setItems(items.filter((item) => item.id !== id));
}, []); // ❌ 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
function TodoList() {
const [items, setItems] = useState(initialItems)
const [items, setItems] = useState(initialItems);
// Stable callback, never recreated
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, []) // ✅ No dependencies needed
setItems((curr) => [...curr, ...newItems]);
}, []); // ✅ No dependencies needed
// Always uses latest state, no stale closure risk
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id))
}, []) // ✅ Safe and stable
setItems((curr) => curr.filter((item) => item.id !== id));
}, []); // ✅ Safe and stable
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
}
```

View File

@@ -14,20 +14,18 @@ Pass a function to `useState` for expensive initial values. Without the function
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs on EVERY render, even after initialization
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
const [query, setQuery] = useState('')
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
const [query, setQuery] = useState("");
// When query changes, buildSearchIndex runs again unnecessarily
return <SearchResults index={searchIndex} query={query} />
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs on every render
const [settings, setSettings] = useState(
JSON.parse(localStorage.getItem('settings') || '{}')
)
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings") || "{}"));
return <SettingsForm settings={settings} onChange={setSettings} />
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```
@@ -36,20 +34,20 @@ function UserProfile() {
```tsx
function FilteredList({ items }: { items: Item[] }) {
// buildSearchIndex() runs ONLY on initial render
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
const [query, setQuery] = useState('')
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
const [query, setQuery] = useState("");
return <SearchResults index={searchIndex} query={query} />
return <SearchResults index={searchIndex} query={query} />;
}
function UserProfile() {
// JSON.parse runs only on initial render
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
const stored = localStorage.getItem("settings");
return stored ? JSON.parse(stored) : {};
});
return <SettingsForm settings={settings} onChange={setSettings} />
return <SettingsForm settings={settings} onChange={setSettings} />;
}
```

View File

@@ -14,12 +14,12 @@ Extract expensive work into memoized components to enable early returns before c
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user)
return <Avatar id={id} />
}, [user])
const id = computeAvatarId(user);
return <Avatar id={id} />;
}, [user]);
if (loading) return <Skeleton />
return <div>{avatar}</div>
if (loading) return <Skeleton />;
return <div>{avatar}</div>;
}
```
@@ -27,17 +27,17 @@ function Profile({ user, loading }: Props) {
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return <Avatar id={id} />
})
const id = useMemo(() => computeAvatarId(user), [user]);
return <Avatar id={id} />;
});
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />
if (loading) return <Skeleton />;
return (
<div>
<UserAvatar user={user} />
</div>
)
);
}
```

View File

@@ -13,28 +13,28 @@ Mark frequent, non-urgent state updates as transitions to maintain UI responsive
```tsx
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => setScrollY(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
const handler = () => setScrollY(window.scrollY);
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
}
```
**Correct (non-blocking updates):**
```tsx
import { startTransition } from 'react'
import { startTransition } from "react";
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
startTransition(() => setScrollY(window.scrollY));
};
window.addEventListener("scroll", handler, { passive: true });
return () => window.removeEventListener("scroll", handler);
}, []);
}
```

View File

@@ -12,46 +12,46 @@ Use Next.js's `after()` to schedule work that should execute after a response is
**Incorrect (blocks response):**
```tsx
import { logUserAction } from '@/app/utils'
import { logUserAction } from "@/app/utils";
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
await updateDatabase(request);
// Logging blocks the response
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
const userAgent = request.headers.get("user-agent") || "unknown";
await logUserAction({ userAgent });
return new Response(JSON.stringify({ status: 'success' }), {
return new Response(JSON.stringify({ status: "success" }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
headers: { "Content-Type": "application/json" },
});
}
```
**Correct (non-blocking):**
```tsx
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
import { after } from "next/server";
import { headers, cookies } from "next/headers";
import { logUserAction } from "@/app/utils";
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
await updateDatabase(request);
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
const userAgent = (await headers()).get("user-agent") || "unknown";
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,
headers: { 'Content-Type': 'application/json' }
})
headers: { "Content-Type": "application/json" },
});
}
```

View File

@@ -12,20 +12,20 @@ tags: server, cache, lru, cross-request
**Implementation:**
```typescript
import { LRUCache } from 'lru-cache'
import { LRUCache } from "lru-cache";
const cache = new LRUCache<string, any>({
max: 1000,
ttl: 5 * 60 * 1000 // 5 minutes
})
ttl: 5 * 60 * 1000, // 5 minutes
});
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const cached = cache.get(id);
if (cached) return cached;
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
const user = await db.user.findUnique({ where: { id } });
cache.set(id, user);
return user;
}
// Request 1: DB query, result cached

View File

@@ -12,15 +12,15 @@ Use `React.cache()` for server-side request deduplication. Authentication and da
**Usage:**
```typescript
import { cache } from 'react'
import { cache } from "react";
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
const session = await auth();
if (!session?.user?.id) return null;
return await db.user.findUnique({
where: { id: session.user.id }
})
})
where: { id: session.user.id },
});
});
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
@@ -33,32 +33,32 @@ Within a single request, multiple calls to `getCurrentUser()` execute the query
```typescript
const getUser = cache(async (params: { uid: number }) => {
return await db.user.findUnique({ where: { id: params.uid } })
})
return await db.user.findUnique({ where: { id: params.uid } });
});
// Each call creates new object, never hits cache
getUser({ uid: 1 })
getUser({ uid: 1 }) // Cache miss, runs query again
getUser({ uid: 1 });
getUser({ uid: 1 }); // Cache miss, runs query again
```
**Correct (cache hit):**
```typescript
const getUser = cache(async (uid: number) => {
return await db.user.findUnique({ where: { id: uid } })
})
return await db.user.findUnique({ where: { id: uid } });
});
// Primitive args use value equality
getUser(1)
getUser(1) // Cache hit, returns cached result
getUser(1);
getUser(1); // Cache hit, returns cached result
```
If you must pass objects, pass the same reference:
```typescript
const params = { uid: 1 }
getUser(params) // Query runs
getUser(params) // Cache hit (same reference)
const params = { uid: 1 };
getUser(params); // Query runs
getUser(params); // Cache hit (same reference)
```
**Next.js-Specific Note:**

View File

@@ -13,18 +13,18 @@ React Server Components execute sequentially within a tree. Restructure with com
```tsx
export default async function Page() {
const header = await fetchHeader()
const header = await fetchHeader();
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
)
);
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
```
@@ -32,13 +32,13 @@ async function Sidebar() {
```tsx
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
export default function Page() {
@@ -47,7 +47,7 @@ export default function Page() {
<Header />
<Sidebar />
</div>
)
);
}
```
@@ -55,13 +55,13 @@ export default function Page() {
```tsx
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
const data = await fetchHeader();
return <div>{data}</div>;
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
const items = await fetchSidebarItems();
return <nav>{items.map(renderItem)}</nav>;
}
function Layout({ children }: { children: ReactNode }) {
@@ -70,7 +70,7 @@ function Layout({ children }: { children: ReactNode }) {
<Header />
{children}
</div>
)
);
}
export default function Page() {
@@ -78,6 +78,6 @@ export default function Page() {
<Layout>
<Sidebar />
</Layout>
)
);
}
```

View File

@@ -13,13 +13,13 @@ The React Server/Client boundary serializes all object properties into strings a
```tsx
async function Page() {
const user = await fetchUser() // 50 fields
return <Profile user={user} />
const user = await fetchUser(); // 50 fields
return <Profile user={user} />;
}
'use client'
("use client");
function Profile({ user }: { user: User }) {
return <div>{user.name}</div> // uses 1 field
return <div>{user.name}</div>; // uses 1 field
}
```
@@ -27,12 +27,12 @@ function Profile({ user }: { user: User }) {
```tsx
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} />
const user = await fetchUser();
return <Profile name={user.name} />;
}
'use client'
("use client");
function Profile({ name }: { name: string }) {
return <div>{name}</div>
return <div>{name}</div>;
}
```

View File

@@ -31,6 +31,7 @@ Use WebFetch to retrieve the latest rules. The fetched content contains all the
## Usage
When a user provides a file or pattern argument:
1. Fetch guidelines from the source URL above
2. Read the specified files
3. Apply all rules from the fetched guidelines

220
AGENTS.md Normal file
View 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
View File

@@ -1,144 +1 @@
# 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 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
@AGENTS.md

Some files were not shown because too many files have changed in this diff Show More