fix(web): resolve the 5 actionable React-compiler lint advisories
Web lint warnings reduced from 20 → 15 by fixing the categories that flag real architectural smells rather than the canonical fetch-on-mount setState pattern. Real fixes: 1. web/src/lib/use-theme.ts — replace useEffect + setState mount-sync pattern with React.useSyncExternalStore. The hook now subscribes to browser storage events, returns a stable snapshot for SSR, and uses a manual storage-event dispatch so same-document setters refresh correctly. Eliminates the cascading-render advisory and gains free cross-tab theme sync. 2. web/src/lib/use-keyboard-shortcuts.ts — move ref assignment from render time into a useEffect. Fixes the 'Cannot access refs during render' advisory without behavior change. 3. web/src/components/NoteEditor.tsx — move onSaveRef.current = onSave from render time into a useEffect for the same reason. 4. web/src/app/(app)/reviews/page.tsx — wrap handleDecision and handleBatchDecision in useCallback so the useEffect that depends on them no longer re-subscribes the keydown listener on every render. Fixes both react-hooks/exhaustive-deps warnings and the underlying perf bug they pointed at. 5. web/src/app/(app)/prompts/page.tsx — wrap loadTemplates in useCallback declared before the useEffect that calls it. Fixes the 'Cannot access variable before it is declared' advisory. Remaining 15 warnings are React-compiler runtime hints about fetchData().then(setData) patterns inside useEffect, which is the canonical fetch-on-mount pattern shown in React's own docs. Resolving them properly requires Suspense + use() or risky startTransition wraps; both are out of scope and tracked under future tech debt. Verified: - pnpm --filter @notelett/web run typecheck: passes - pnpm --filter @notelett/web run lint: 0 errors, 15 warnings (down 5) - pnpm run verify: backend 380/380, web 96/96, mobile 97/97
This commit is contained in:
parent
78433b0e45
commit
3c4d46f3ad
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Sparkles, FileText, Image, Layers, Trash2 } from "lucide-react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { Badge, Button, Card } from "@/components/ui/Primitives";
|
||||
@ -28,11 +28,7 @@ export default function PromptsPage() {
|
||||
const [activeCategory, setActiveCategory] = useState<PromptCategory | "all">("all");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
void loadTemplates();
|
||||
}, []);
|
||||
|
||||
async function loadTemplates() {
|
||||
const loadTemplates = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setTemplates(await listPromptTemplates());
|
||||
@ -41,7 +37,11 @@ export default function PromptsPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadTemplates();
|
||||
}, [loadTemplates]);
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm("Delete this custom template?")) return;
|
||||
|
||||
@ -105,7 +105,7 @@ export default function ReviewsPage() {
|
||||
},
|
||||
] as const;
|
||||
|
||||
async function handleDecision(decision: "approved" | "rejected") {
|
||||
const handleDecision = useCallback(async (decision: "approved" | "rejected") => {
|
||||
if (!featuredProposal) {
|
||||
return;
|
||||
}
|
||||
@ -146,9 +146,9 @@ export default function ReviewsPage() {
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
}, [featuredProposal, reviewNote]);
|
||||
|
||||
async function handleBatchDecision(decision: "approved" | "rejected") {
|
||||
const handleBatchDecision = useCallback(async (decision: "approved" | "rejected") => {
|
||||
const batchItems = approvalQueue.filter((item) => selectedBatchIds.has(item.id));
|
||||
if (batchItems.length === 0) return;
|
||||
|
||||
@ -184,7 +184,7 @@ export default function ReviewsPage() {
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
}, [approvalQueue, selectedBatchIds, reviewNote]);
|
||||
|
||||
useEffect(() => {
|
||||
function selectRelativeQueueItem(offset: number) {
|
||||
|
||||
@ -57,7 +57,14 @@ export function NoteEditor({
|
||||
const [explainResult, setExplainResult] = useState<string | null>(null);
|
||||
const toneMenuRef = useRef<HTMLDivElement>(null);
|
||||
const onSaveRef = useRef(onSave);
|
||||
onSaveRef.current = onSave;
|
||||
|
||||
// Keep the latest onSave handler in a ref so the debounced auto-save
|
||||
// effect always invokes the current closure. Assigning inside an effect
|
||||
// (rather than during render) avoids the React 19 compiler warning
|
||||
// about mutating refs during render.
|
||||
useEffect(() => {
|
||||
onSaveRef.current = onSave;
|
||||
}, [onSave]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toneMenuOpen) return;
|
||||
|
||||
@ -13,7 +13,13 @@ export interface KeyboardShortcut {
|
||||
|
||||
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
|
||||
const shortcutsRef = useRef(shortcuts);
|
||||
shortcutsRef.current = shortcuts;
|
||||
|
||||
// Keep the ref pointed at the latest shortcuts array. Assigning inside
|
||||
// an effect (rather than during render) avoids the React 19 compiler
|
||||
// warning about mutating refs during render.
|
||||
useEffect(() => {
|
||||
shortcutsRef.current = shortcuts;
|
||||
}, [shortcuts]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
|
||||
@ -1,25 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useCallback, useEffect, useSyncExternalStore } from 'react';
|
||||
import { PRODUCT_ID } from '@/lib/product-config';
|
||||
|
||||
type Theme = 'dark' | 'light';
|
||||
const STORAGE_KEY = `${PRODUCT_ID}_theme`;
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setThemeState] = useState<Theme>('dark');
|
||||
function readTheme(): Theme {
|
||||
if (typeof window === 'undefined') return 'dark';
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY) as Theme | null;
|
||||
return stored ?? 'dark';
|
||||
}
|
||||
|
||||
function getServerSnapshot(): Theme {
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
// Subscribe to storage events so the theme stays in sync across tabs.
|
||||
// React.useSyncExternalStore handles SSR + hydration correctly and avoids
|
||||
// the setState-in-effect pattern that the React 19 compiler warns about.
|
||||
function subscribe(callback: () => void) {
|
||||
if (typeof window === 'undefined') return () => {};
|
||||
window.addEventListener('storage', callback);
|
||||
return () => window.removeEventListener('storage', callback);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const theme = useSyncExternalStore(subscribe, readTheme, getServerSnapshot);
|
||||
|
||||
// Reflect the resolved theme on the <html> element after hydration so
|
||||
// initial mount picks up the value from localStorage. Pure DOM mutation,
|
||||
// no setState, so the React-compiler advisory does not trigger.
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
|
||||
const initial = stored ?? 'dark';
|
||||
setThemeState(initial);
|
||||
document.documentElement.setAttribute('data-theme', initial);
|
||||
}, []);
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const setTheme = useCallback((t: Theme) => {
|
||||
setThemeState(t);
|
||||
localStorage.setItem(STORAGE_KEY, t);
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(STORAGE_KEY, t);
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
// Manually dispatch a storage event so the current document re-reads
|
||||
// the snapshot (browsers only fire storage events in OTHER tabs).
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: STORAGE_KEY, newValue: t }));
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user