diff --git a/.agents/skills/add-provider-doc/SKILL.md b/.agents/skills/add-provider-doc/SKILL.md index 82568080ab..cab8a26b6e 100644 --- a/.agents/skills/add-provider-doc/SKILL.md +++ b/.agents/skills/add-provider-doc/SKILL.md @@ -43,11 +43,13 @@ Reference: `docs/usage/providers/fal.mdx` ```markdown ### `{PROVIDER}_API_KEY` + - Type: Required - Description: API key from {Provider Name} - Example: `{api-key-format}` ### `{PROVIDER}_MODEL_LIST` + - Type: Optional - Description: Control model list. Use `+` to add, `-` to hide - Example: `-all,+model-1,+model-2=Display Name` diff --git a/.agents/skills/desktop/SKILL.md b/.agents/skills/desktop/SKILL.md index 01f9650179..da5c0ea0ca 100644 --- a/.agents/skills/desktop/SKILL.md +++ b/.agents/skills/desktop/SKILL.md @@ -17,6 +17,7 @@ LobeChat desktop is built on Electron with main-renderer architecture: ## Adding New Desktop Features ### 1. Create Controller + Location: `apps/desktop/src/main/controllers/` ```typescript @@ -36,14 +37,21 @@ export default class NewFeatureCtr extends ControllerModule { Register in `apps/desktop/src/main/controllers/registry.ts`. ### 2. Define IPC Types + Location: `packages/electron-client-ipc/src/types.ts` ```typescript -export interface SomeParams { /* ... */ } -export interface SomeResult { success: boolean; error?: string } +export interface SomeParams { + /* ... */ +} +export interface SomeResult { + success: boolean; + error?: string; +} ``` ### 3. Create Renderer Service + Location: `src/services/electron/` ```typescript @@ -57,14 +65,17 @@ export const newFeatureService = async (params: SomeParams) => { ``` ### 4. Implement Store Action + Location: `src/store/` ### 5. Add Tests + Location: `apps/desktop/src/main/controllers/__tests__/` ## Detailed Guides See `references/` for specific topics: + - **Feature implementation**: `references/feature-implementation.md` - **Local tools workflow**: `references/local-tools.md` - **Menu configuration**: `references/menu-config.md` diff --git a/.agents/skills/desktop/references/feature-implementation.md b/.agents/skills/desktop/references/feature-implementation.md index cfc155edf8..1332950a33 100644 --- a/.agents/skills/desktop/references/feature-implementation.md +++ b/.agents/skills/desktop/references/feature-implementation.md @@ -22,7 +22,10 @@ Main Process Renderer Process ```typescript // apps/desktop/src/main/controllers/NotificationCtr.ts -import type { ShowDesktopNotificationParams, DesktopNotificationResult } from '@lobechat/electron-client-ipc'; +import type { + ShowDesktopNotificationParams, + DesktopNotificationResult, +} from '@lobechat/electron-client-ipc'; import { Notification } from 'electron'; import { ControllerModule, IpcMethod } from '@/controllers'; @@ -30,7 +33,9 @@ export default class NotificationCtr extends ControllerModule { static override readonly groupName = 'notification'; @IpcMethod() - async showDesktopNotification(params: ShowDesktopNotificationParams): Promise { + async showDesktopNotification( + params: ShowDesktopNotificationParams, + ): Promise { if (!Notification.isSupported()) { return { error: 'Notifications not supported', success: false }; } @@ -72,8 +77,7 @@ import { ensureElectronIpc } from '@/utils/electron/ipc'; const ipc = ensureElectronIpc(); export const notificationService = { - show: (params: ShowDesktopNotificationParams) => - ipc.notification.showDesktopNotification(params), + show: (params: ShowDesktopNotificationParams) => ipc.notification.showDesktopNotification(params), }; ``` diff --git a/.agents/skills/desktop/references/menu-config.md b/.agents/skills/desktop/references/menu-config.md index 769eaf0f4f..74fc5d387e 100644 --- a/.agents/skills/desktop/references/menu-config.md +++ b/.agents/skills/desktop/references/menu-config.md @@ -30,7 +30,13 @@ export const createAppMenu = (win: BrowserWindow) => { { label: 'File', submenu: [ - { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => { /* ... */ } }, + { + label: 'New', + accelerator: 'CmdOrCtrl+N', + click: () => { + /* ... */ + }, + }, { type: 'separator' }, { role: 'quit' }, ], @@ -82,9 +88,7 @@ import { i18n } from '../locales'; const template = [ { label: i18n.t('menu.file'), - submenu: [ - { label: i18n.t('menu.new'), click: createNew }, - ], + submenu: [{ label: i18n.t('menu.new'), click: createNew }], }, ]; ``` diff --git a/.agents/skills/desktop/references/window-management.md b/.agents/skills/desktop/references/window-management.md index b8d9a7f8b3..499e843b03 100644 --- a/.agents/skills/desktop/references/window-management.md +++ b/.agents/skills/desktop/references/window-management.md @@ -131,8 +131,12 @@ const window = new BrowserWindow({ ``` ```css -.titlebar { -webkit-app-region: drag; } -.titlebar-button { -webkit-app-region: no-drag; } +.titlebar { + -webkit-app-region: drag; +} +.titlebar-button { + -webkit-app-region: no-drag; +} ``` ## Best Practices diff --git a/.agents/skills/drizzle/SKILL.md b/.agents/skills/drizzle/SKILL.md index a44389cef8..68a51b9502 100644 --- a/.agents/skills/drizzle/SKILL.md +++ b/.agents/skills/drizzle/SKILL.md @@ -73,9 +73,16 @@ export type AgentItem = typeof agents.$inferSelect; export const agents = pgTable( 'agents', { - id: text('id').primaryKey().$defaultFn(() => idGenerator('agents')).notNull(), - slug: varchar('slug', { length: 100 }).$defaultFn(() => randomSlug(4)).unique(), - userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), + id: text('id') + .primaryKey() + .$defaultFn(() => idGenerator('agents')) + .notNull(), + slug: varchar('slug', { length: 100 }) + .$defaultFn(() => randomSlug(4)) + .unique(), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), clientId: text('client_id'), chatConfig: jsonb('chat_config').$type(), ...timestamps, @@ -92,9 +99,15 @@ export const agents = pgTable( export const agentsKnowledgeBases = pgTable( 'agents_knowledge_bases', { - agentId: text('agent_id').references(() => agents.id, { onDelete: 'cascade' }).notNull(), - knowledgeBaseId: text('knowledge_base_id').references(() => knowledgeBases.id, { onDelete: 'cascade' }).notNull(), - userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(), + agentId: text('agent_id') + .references(() => agents.id, { onDelete: 'cascade' }) + .notNull(), + knowledgeBaseId: text('knowledge_base_id') + .references(() => knowledgeBases.id, { onDelete: 'cascade' }) + .notNull(), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), enabled: boolean('enabled').default(true), ...timestamps, }, diff --git a/.agents/skills/hotkey/SKILL.md b/.agents/skills/hotkey/SKILL.md index 2f96149aa8..0517c2e9aa 100644 --- a/.agents/skills/hotkey/SKILL.md +++ b/.agents/skills/hotkey/SKILL.md @@ -71,7 +71,7 @@ const clearChatHotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum. - ) + ); } ``` @@ -548,13 +542,11 @@ function EditorButton({ onClick }: { onClick: () => void }) { function FlagsProvider({ children, flags }: Props) { useEffect(() => { if (flags.editorEnabled && typeof window !== 'undefined') { - void import('./monaco-editor').then(mod => mod.init()) + void import('./monaco-editor').then((mod) => mod.init()); } - }, [flags.editorEnabled]) + }, [flags.editorEnabled]); - return - {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 @@ -603,7 +595,7 @@ Use when sequential user actions hit multiple endpoints needing the same data wi **In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching. -Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) +Reference: ### 3.2 Minimize Serialization at RSC Boundaries @@ -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: @@ -782,7 +774,7 @@ In Next.js, the `fetch` API is automatically extended with request memoization. Use `React.cache()` to deduplicate these operations across your component tree. -Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache) +Reference: ### 3.5 Use after() for Non-Blocking Operations @@ -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 }) - + const userAgent = request.headers.get('user-agent') || 'unknown'; + await logUserAction({ userAgent }); + return new Response(JSON.stringify({ status: 'success' }), { status: 200, - headers: { 'Content-Type': 'application/json' } - }) + headers: { 'Content-Type': 'application/json' }, + }); } ``` **Correct: non-blocking** ```tsx -import { after } from 'next/server' -import { headers, cookies } from 'next/headers' -import { logUserAction } from '@/app/utils' +import { after } from 'next/server'; +import { headers, cookies } from 'next/headers'; +import { logUserAction } from '@/app/utils'; export async function POST(request: Request) { // Perform mutation - await updateDatabase(request) - + await updateDatabase(request); + // Log after response is sent after(async () => { - const userAgent = (await headers()).get('user-agent') || 'unknown' - const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous' - - logUserAction({ sessionCookie, userAgent }) - }) - + const userAgent = (await headers()).get('user-agent') || 'unknown'; + const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'; + + logUserAction({ sessionCookie, userAgent }); + }); + return new Response(JSON.stringify({ status: 'success' }), { status: 200, - headers: { 'Content-Type': 'application/json' } - }) + headers: { 'Content-Type': 'application/json' }, + }); } ``` @@ -856,7 +848,7 @@ The response is sent immediately while logging happens in the background. - Works in Server Actions, Route Handlers, and Server Components -Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after) +Reference: --- @@ -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', () => { 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,47 +986,47 @@ SWR enables request deduplication, caching, and revalidation across component in ```tsx function UserList() { - const [users, setUsers] = useState([]) + const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users') - .then(r => r.json()) - .then(setUsers) - }, []) + .then((r) => r.json()) + .then(setUsers); + }, []); } ``` **Correct: multiple instances share one request** ```tsx -import useSWR from 'swr' +import useSWR from 'swr'; function UserList() { - const { data: users } = useSWR('/api/users', fetcher) + const { data: users } = useSWR('/api/users', fetcher); } ``` **For immutable data:** ```tsx -import { useImmutableSWR } from '@/lib/swr' +import { useImmutableSWR } from '@/lib/swr'; function StaticContent() { - const { data } = useImmutableSWR('/api/config', fetcher) + const { data } = useImmutableSWR('/api/config', fetcher); } ``` **For mutations:** ```tsx -import { useSWRMutation } from 'swr/mutation' +import { useSWRMutation } from 'swr/mutation'; function UpdateButton() { - const { trigger } = useSWRMutation('/api/user', updateUser) - return + const { trigger } = useSWRMutation('/api/user', updateUser); + return ; } ``` -Reference: [https://swr.vercel.app](https://swr.vercel.app) +Reference: ### 4.4 Version and Minimize localStorage Data @@ -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