diff --git a/.github/skills/vercel-react-best-practices/AGENTS.md b/.github/skills/vercel-react-best-practices/AGENTS.md index f9b9e99..07b11fe 100644 --- a/.github/skills/vercel-react-best-practices/AGENTS.md +++ b/.github/skills/vercel-react-best-practices/AGENTS.md @@ -94,15 +94,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); } ``` @@ -112,12 +112,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); } ``` @@ -126,35 +126,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); } ``` @@ -169,25 +169,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) @@ -202,10 +203,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 }); } ``` @@ -213,14 +214,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 }); } ``` @@ -235,19 +233,15 @@ 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()]); ``` ### 1.5 Strategic Suspense Boundaries @@ -260,8 +254,8 @@ Instead of awaiting data in async components before returning JSX, use Suspense ```tsx async function Page() { - const data = await fetchData() // Blocks entire page - + const data = await fetchData(); // Blocks entire page + return (
Sidebar
@@ -271,7 +265,7 @@ async function Page() {
Footer
- ) + ); } ``` @@ -292,12 +286,12 @@ function Page() {
Footer
- ) + ); } async function DataDisplay() { - const data = await fetchData() // Only blocks this component - return
{data.content}
+ const data = await fetchData(); // Only blocks this component + return
{data.content}
; } ``` @@ -308,8 +302,8 @@ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. ```tsx function Page() { // Start fetch immediately, but don't await - const dataPromise = fetchData() - + const dataPromise = fetchData(); + return (
Sidebar
@@ -320,17 +314,17 @@ function Page() {
Footer
- ) + ); } function DataDisplay({ dataPromise }: { dataPromise: Promise }) { - const data = use(dataPromise) // Unwraps the promise - return
{data.content}
+ const data = use(dataPromise); // Unwraps the promise + return
{data.content}
; } function DataSummary({ dataPromise }: { dataPromise: Promise }) { - const data = use(dataPromise) // Reuses the same promise - return
{data.summary}
+ const data = use(dataPromise); // Reuses the same promise + return
{data.summary}
; } ``` @@ -369,24 +363,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 ``` @@ -396,12 +390,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 ``` @@ -420,19 +414,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> }) { - const [frames, setFrames] = useState(null) +function AnimationPlayer({ + enabled, + setEnabled, +}: { + enabled: boolean; + setEnabled: React.Dispatch>; +}) { + const [frames, setFrames] = useState(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 - return + if (!frames) return ; + return ; } ``` @@ -447,7 +447,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 ( @@ -457,19 +457,18 @@ export default function RootLayout({ children }) { - ) + ); } ``` **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 ( @@ -479,7 +478,7 @@ export default function RootLayout({ children }) { - ) + ); } ``` @@ -492,25 +491,24 @@ 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 + return ; } ``` **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 + return ; } ``` @@ -525,20 +523,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 ( - - ) + ); } ``` @@ -547,14 +541,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 - {children} - + return {children}; } ``` @@ -577,20 +569,20 @@ Optimizing server-side rendering and data fetching eliminates server-side waterf **Implementation:** ```typescript -import { LRUCache } from 'lru-cache' +import { LRUCache } from "lru-cache"; const cache = new LRUCache({ 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 @@ -615,13 +607,13 @@ The React Server/Client boundary serializes all object properties into strings a ```tsx async function Page() { - const user = await fetchUser() // 50 fields - return + const user = await fetchUser(); // 50 fields + return ; } -'use client' +("use client"); function Profile({ user }: { user: User }) { - return
{user.name}
// uses 1 field + return
{user.name}
; // uses 1 field } ``` @@ -629,13 +621,13 @@ function Profile({ user }: { user: User }) { ```tsx async function Page() { - const user = await fetchUser() - return + const user = await fetchUser(); + return ; } -'use client' +("use client"); function Profile({ name }: { name: string }) { - return
{name}
+ return
{name}
; } ``` @@ -649,18 +641,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 (
{header}
- ) + ); } async function Sidebar() { - const items = await fetchSidebarItems() - return + const items = await fetchSidebarItems(); + return ; } ``` @@ -668,13 +660,13 @@ async function Sidebar() { ```tsx async function Header() { - const data = await fetchHeader() - return
{data}
+ const data = await fetchHeader(); + return
{data}
; } async function Sidebar() { - const items = await fetchSidebarItems() - return + const items = await fetchSidebarItems(); + return ; } export default function Page() { @@ -683,7 +675,7 @@ export default function Page() {
- ) + ); } ``` @@ -691,13 +683,13 @@ export default function Page() { ```tsx async function Header() { - const data = await fetchHeader() - return
{data}
+ const data = await fetchHeader(); + return
{data}
; } async function Sidebar() { - const items = await fetchSidebarItems() - return + const items = await fetchSidebarItems(); + return ; } function Layout({ children }: { children: ReactNode }) { @@ -706,7 +698,7 @@ function Layout({ children }: { children: ReactNode }) {
{children} - ) + ); } export default function Page() { @@ -714,7 +706,7 @@ export default function Page() { - ) + ); } ``` @@ -727,15 +719,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. @@ -748,20 +740,20 @@ 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 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) ``` If you must pass objects, pass the same reference: @@ -793,46 +785,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 }) - - return new Response(JSON.stringify({ status: 'success' }), { + const userAgent = request.headers.get("user-agent") || "unknown"; + await logUserAction({ userAgent }); + + return new Response(JSON.stringify({ status: "success" }), { status: 200, - headers: { 'Content-Type': 'application/json' } - }) + headers: { "Content-Type": "application/json" }, + }); } ``` **Correct: non-blocking** ```tsx -import { after } from 'next/server' -import { headers, cookies } from 'next/headers' -import { logUserAction } from '@/app/utils' +import { after } from "next/server"; +import { headers, cookies } from "next/headers"; +import { logUserAction } from "@/app/utils"; export async function POST(request: Request) { // Perform mutation - await updateDatabase(request) - + await updateDatabase(request); + // Log after response is sent after(async () => { - const userAgent = (await headers()).get('user-agent') || 'unknown' - const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous' - - logUserAction({ sessionCookie, userAgent }) - }) - - return new Response(JSON.stringify({ status: 'success' }), { + const userAgent = (await headers()).get("user-agent") || "unknown"; + const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous"; + + logUserAction({ sessionCookie, userAgent }); + }); + + return new Response(JSON.stringify({ status: "success" }), { status: 200, - headers: { 'Content-Type': 'application/json' } - }) + headers: { "Content-Type": "application/json" }, + }); } ``` @@ -879,12 +871,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]); } ``` @@ -893,45 +885,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 void>>() +const keyCallbacks = new Map 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", () => { + /* ... */ + }); // ... } ``` @@ -946,34 +942,34 @@ Add `{ passive: true }` to touch and wheel event listeners to enable immediate s ```typescript useEffect(() => { - const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) - const handleWheel = (e: WheelEvent) => console.log(e.deltaY) - - document.addEventListener('touchstart', handleTouch) - document.addEventListener('wheel', handleWheel) - + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX); + const handleWheel = (e: WheelEvent) => console.log(e.deltaY); + + document.addEventListener("touchstart", handleTouch); + document.addEventListener("wheel", handleWheel); + return () => { - document.removeEventListener('touchstart', handleTouch) - document.removeEventListener('wheel', handleWheel) - } -}, []) + document.removeEventListener("touchstart", handleTouch); + document.removeEventListener("wheel", handleWheel); + }; +}, []); ``` **Correct:** ```typescript useEffect(() => { - const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) - const handleWheel = (e: WheelEvent) => console.log(e.deltaY) - - document.addEventListener('touchstart', handleTouch, { passive: true }) - document.addEventListener('wheel', handleWheel, { passive: true }) - + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX); + const handleWheel = (e: WheelEvent) => console.log(e.deltaY); + + document.addEventListener("touchstart", handleTouch, { passive: true }); + document.addEventListener("wheel", handleWheel, { passive: true }); + return () => { - document.removeEventListener('touchstart', handleTouch) - document.removeEventListener('wheel', handleWheel) - } -}, []) + document.removeEventListener("touchstart", handleTouch); + document.removeEventListener("wheel", handleWheel); + }; +}, []); ``` **Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`. @@ -990,43 +986,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 + const { trigger } = useSWRMutation("/api/user", updateUser); + return ; } ``` @@ -1042,18 +1038,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 } @@ -1061,21 +1057,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 {} } @@ -1087,10 +1083,13 @@ function migrate() { // User object has 20+ fields, only store what UI needs function cachePrefs(user: FullUser) { try { - localStorage.setItem('prefs:v1', JSON.stringify({ - theme: user.preferences.theme, - notifications: user.preferences.notifications - })) + localStorage.setItem( + "prefs:v1", + JSON.stringify({ + theme: user.preferences.theme, + notifications: user.preferences.notifications, + }) + ); } catch {} } ``` @@ -1117,14 +1116,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 + return ; } ``` @@ -1133,12 +1132,12 @@ 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 + return ; } ``` @@ -1153,12 +1152,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 - }, [user]) + const id = computeAvatarId(user); + return ; + }, [user]); - if (loading) return - return
{avatar}
+ if (loading) return ; + return
{avatar}
; } ``` @@ -1166,17 +1165,17 @@ function Profile({ user, loading }: Props) { ```tsx const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { - const id = useMemo(() => computeAvatarId(user), [user]) - return -}) + const id = useMemo(() => computeAvatarId(user), [user]); + return ; +}); function Profile({ user, loading }: Props) { - if (loading) return + if (loading) return ; return (
- ) + ); } ``` @@ -1192,16 +1191,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:** @@ -1210,17 +1209,17 @@ 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]); ``` ### 5.4 Subscribe to Derived State @@ -1233,9 +1232,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