Loading...
Loading...
React performance optimization guidelines. Use when writing, reviewing, or refactoring React components to ensure optimal rendering and bundle patterns. Triggers on tasks involving React components, hooks, memoization, or bundle optimization.
npx skill4agent add sergiodxa/agent-skills frontend-react-best-practices// Bad: loads entire library (200-800ms)
import { Check, X } from "lucide-react";
// Good: loads only what you need
import Check from "lucide-react/dist/esm/icons/check";
import X from "lucide-react/dist/esm/icons/x";useEffect(() => {
if (enabled && typeof window !== "undefined") {
import("./heavy-module").then((mod) => setModule(mod));
}
}, [enabled]);<button
onMouseEnter={() => import("./editor")}
onFocus={() => import("./editor")}
onClick={openEditor}
>
Open Editor
</button>// Bad: stale closure risk, recreates on items change
const addItem = useCallback(
(item) => {
setItems([...items, item]);
},
[items],
);
// Good: always uses latest state, stable reference
const addItem = useCallback((item) => {
setItems((curr) => [...curr, item]);
}, []);// Bad: extra state and effect, extra render
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);
// Good: derived directly during render
const fullName = firstName + " " + lastName;// Bad: runs expensiveComputation() on every render
const [data] = useState(expensiveComputation());
// Good: runs only on initial render
const [data] = useState(() => expensiveComputation());// Bad: runs on any user field change
useEffect(() => {
console.log(user.id);
}, [user]);
// Good: runs only when id changes
useEffect(() => {
console.log(user.id);
}, [user.id]);// Bad: re-renders on every pixel change
const width = useWindowWidth();
const isMobile = width < 768;
// Good: re-renders only when boolean changes
const isMobile = useMediaQuery("(max-width: 767px)");// Good: skips computation when loading
const UserAvatar = memo(function UserAvatar({ user }) {
let id = useMemo(() => computeAvatarId(user), [user]);
return <Avatar id={id} />;
});
function Profile({ user, loading }) {
if (loading) return <Skeleton />;
return <UserAvatar user={user} />;
}// Bad: breaks memoization (new function each render)
const Button = memo(({ onClick = () => {} }) => ...)
// Good: stable default value
const NOOP = () => {}
const Button = memo(({ onClick = NOOP }) => ...)// Bad: useMemo overhead > expression cost
const isLoading = useMemo(() => a.loading || b.loading, [a.loading, b.loading]);
// Good: just compute it
const isLoading = a.loading || b.loading;// Bad: effect re-runs on theme change
useEffect(() => {
if (submitted) post("/api/register");
}, [submitted, theme]);
// Good: in handler
const handleSubmit = () => post("/api/register");// Good: non-blocking scroll tracking
const handler = () => {
startTransition(() => setScrollY(window.scrollY));
};// Good: no re-render, direct DOM update
const lastXRef = useRef(0);
const dotRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let onMove = (e) => {
lastXRef.current = e.clientX;
dotRef.current?.style.transform = `translateX(${e.clientX}px)`;
};
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
}, []);// Bad: renders "0" when count is 0
{
count && <Badge>{count}</Badge>;
}
// Good: renders nothing when count is 0
{
count > 0 ? <Badge>{count}</Badge> : null;
}// Good: reuses same element, especially for large SVGs
const skeleton = <div className="animate-pulse h-20 bg-gray-200" />;
function Container({ loading }) {
return loading ? skeleton : <Content />;
}.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}// Good: hardware accelerated
<div className="animate-spin">
<svg>...</svg>
</div>npx svgo --precision=1 --multipass icon.svg<div id="theme-wrapper">{children}</div>
<script dangerouslySetInnerHTML={{ __html: `
var theme = localStorage.getItem('theme') || 'light';
document.getElementById('theme-wrapper').className = theme;
` }} /><span suppressHydrationWarning>{new Date().toLocaleString()}</span><ClientOnly fallback={<Skeleton />}>
{() => <Map />}
</ClientOnly>useHydratedlet hydrated = useHydrated();
return hydrated ? <Widget /> : <Skeleton />;const [isPending, startTransition] = useTransition();
let handleSearch = (value) => {
startTransition(async () => {
let data = await fetchResults(value);
setResults(data);
});
};<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>document.addEventListener("wheel", handler, { passive: true });
document.addEventListener("touchstart", handler, { passive: true });const VERSION = "v2";
function saveConfig(config: Config) {
try {
localStorage.setItem(`config:${VERSION}`, JSON.stringify(config));
} catch {} // Handle incognito/quota exceeded
}// Bad: useEffect to derive state
let [filtered, setFiltered] = useState(items);
useEffect(() => {
setFiltered(items.filter((i) => i.active));
}, [items]);
// Good: derive during render
let filtered = items.filter((i) => i.active);
// Good: useMemo if expensive
let filtered = useMemo(() => items.filter((i) => i.active), [items]);// Bad: anonymous arrow function
useEffect(() => {
document.title = title;
}, [title]);
// Good: named function
useEffect(
function syncDocumentTitle() {
document.title = title;
},
[title],
);
// Good: also name cleanup functions
useEffect(function subscribeToOnlineStatus() {
window.addEventListener("online", handleOnline);
return function unsubscribeFromOnlineStatus() {
window.removeEventListener("online", handleOnline);
};
}, []);// Bad: boolean prop explosion
<Composer isThread isEditing={false} showAttachments />
// Good: explicit variants
<ThreadComposer channelId="abc" />
<EditComposer messageId="xyz" />// Good: compound components
<Composer.Provider state={state} actions={actions}>
<Composer.Frame>
<Composer.Input />
<Composer.Footer>
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
</Composer.Provider>// Good: state in provider, accessible anywhere inside
<ForwardMessageProvider>
<Dialog>
<Composer.Input />
<MessagePreview /> {/* Can read state */}
<ForwardButton /> {/* Can call submit */}
</Dialog>
</ForwardMessageProvider>// Good: self-documenting variants
function ThreadComposer({ channelId }) {
return (
<ThreadProvider channelId={channelId}>
<Composer.Frame>
<Composer.Input />
<AlsoSendToChannelField />
<Composer.Submit />
</Composer.Frame>
</ThreadProvider>
);
}// Good: children for structure
<Card>
<Card.Header>Title</Card.Header>
<Card.Body>Content</Card.Body>
</Card>
// OK: render props when passing data
<List renderItem={({ item }) => <Item {...item} />} /><Select value="abc" onChange={...}>
<Option value="abc">ABC</Option>
<Option value="xyz">XYZ</Option>
</Select>// components/button.tsx
export namespace Button {
export type Variant = "solid" | "ghost" | "outline";
export interface Props {
variant?: Variant;
children: React.ReactNode;
}
}
export function Button({ variant = "solid", children }: Button.Props) {
// ...
}
// Usage: single import
import { Button } from "~/components/button";
<Button variant="ghost">Click</Button>
function wrap(props: Button.Props) { ... }