frontend-react-best-practices by sergiodxa/agent-skills
npx skills add https://github.com/sergiodxa/agent-skills --skill frontend-react-best-practicesReact 组件的性能优化与组合模式。包含 6 大类共 33 条规则,专注于减少重渲染、优化打包体积、组件组合以及避免常见的 React 陷阱。
在以下情况时参考这些准则:
直接从源文件导入,避免使用索引文件。
// 错误:加载整个库 (200-800ms)
import { Check, X } from "lucide-react";
// 正确:仅加载所需内容
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}
>
打开编辑器
</button>
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
使用函数式 setState 以获得稳定的回调。
// 错误:存在闭包过时风险,items 变化时会重新创建
const addItem = useCallback(
(item) => {
setItems([...items, item]);
},
[items],
);
// 正确:始终使用最新状态,引用稳定
const addItem = useCallback((item) => {
setItems((curr) => [...curr, item]);
}, []);
在渲染期间派生状态,而非在副作用中。
// 错误:额外的状态和副作用,导致额外渲染
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);
// 正确:直接在渲染期间派生
const fullName = firstName + " " + lastName;
向 useState 传递函数以处理昂贵的初始值计算。
// 错误:每次渲染都会运行 expensiveComputation()
const [data] = useState(expensiveComputation());
// 正确:仅在初始渲染时运行
const [data] = useState(() => expensiveComputation());
在副作用中使用原始值作为依赖项。
// 错误:user 的任何字段变化都会触发运行
useEffect(() => {
console.log(user.id);
}, [user]);
// 正确:仅在 id 变化时运行
useEffect(() => {
console.log(user.id);
}, [user.id]);
订阅派生的布尔值,而非原始值。
// 错误:每个像素变化都会导致重渲染
const width = useWindowWidth();
const isMobile = width < 768;
// 正确:仅在布尔值变化时重渲染
const isMobile = useMediaQuery("(max-width: 767px)");
将昂贵的计算提取到记忆化组件中。
// 正确:加载时跳过计算
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} />;
}
将非原始类型的默认属性提升为常量。
// 错误:破坏记忆化(每次渲染都会创建新函数)
const Button = memo(({ onClick = () => {} }) => ...)
// 正确:稳定的默认值
const NOOP = () => {}
const Button = memo(({ onClick = NOOP }) => ...)
不要将简单的原始值表达式包裹在 useMemo 中。
// 错误:useMemo 的开销大于表达式计算成本
const isLoading = useMemo(() => a.loading || b.loading, [a.loading, b.loading]);
// 正确:直接计算
const isLoading = a.loading || b.loading;
将交互逻辑放在事件处理器中,而非副作用中。
// 错误:主题变化时副作用会重新运行
useEffect(() => {
if (submitted) post("/api/register");
}, [submitted, theme]);
// 正确:放在处理器中
const handleSubmit = () => post("/api/register");
对非紧急更新使用 startTransition。
// 正确:非阻塞的滚动追踪
const handler = () => {
startTransition(() => setScrollY(window.scrollY));
};
使用 ref 来存储瞬态频繁变化的值。
// 正确:无重渲染,直接更新 DOM
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);
}, []);
对于包含数字的条件渲染,使用三元运算符而非 &&。
// 错误:当 count 为 0 时会渲染 "0"
{
count && <Badge>{count}</Badge>;
}
// 正确:当 count 为 0 时什么都不渲染
{
count > 0 ? <Badge>{count}</Badge> : null;
}
将静态 JSX 提取到组件外部。
// 正确:复用相同的元素,尤其对于大型 SVG
const skeleton = <div className="animate-pulse h-20 bg-gray-200" />;
function Container({ loading }) {
return loading ? skeleton : <Content />;
}
对长列表使用 content-visibility。
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
动画化包装 div,而非 SVG 元素(以启用 GPU 加速)。
// 正确:硬件加速
<div className="animate-spin">
<svg>...</svg>
</div>
使用 SVGO 降低 SVG 坐标精度。
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 和回退内容来渲染仅浏览器组件。
<ClientOnly fallback={<Skeleton />}>
{() => <Map />}
</ClientOnly>
使用 useHydrated 处理 SSR/CSR 差异。
let hydrated = useHydrated();
return hydrated ? <Widget /> : <Skeleton />;
优先使用 useTransition 而非手动加载状态。
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 });
对 localStorage 数据进行版本控制和最小化。
const VERSION = "v2";
function saveConfig(config: Config) {
try {
localStorage.setItem(`config:${VERSION}`, JSON.stringify(config));
} catch {} // 处理隐身模式/配额超出
}
仅在绝对必要时使用 useEffect。优先使用派生状态或事件处理器。
// 错误:使用 useEffect 派生状态
let [filtered, setFiltered] = useState(items);
useEffect(() => {
setFiltered(items.filter((i) => i.active));
}, [items]);
// 正确:在渲染期间派生
let filtered = items.filter((i) => i.active);
// 正确:如果计算昂贵则使用 useMemo
let filtered = useMemo(() => items.filter((i) => i.active), [items]);
在 useEffect 中使用具名函数声明,以便于调试和自文档化。
// 错误:匿名箭头函数
useEffect(() => {
document.title = title;
}, [title]);
// 正确:具名函数
useEffect(
function syncDocumentTitle() {
document.title = title;
},
[title],
);
// 正确:同时命名清理函数
useEffect(function subscribeToOnlineStatus() {
window.addEventListener("online", handleOnline);
return function unsubscribeFromOnlineStatus() {
window.removeEventListener("online", handleOnline);
};
}, []);
不要添加布尔属性来自定义行为。应使用组合替代。
// 错误:布尔属性爆炸
<Composer isThread isEditing={false} showAttachments />
// 正确:明确的变体
<ThreadComposer channelId="abc" />
<EditComposer messageId="xyz" />
将复杂组件构建为具有共享上下文的复合组件。
// 正确:复合组件
<Composer.Provider state={state} actions={actions}>
<Composer.Frame>
<Composer.Input />
<Composer.Footer>
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
</Composer.Provider>
将状态提升到 Provider 组件中,以便跨组件访问。
// 正确:状态在 Provider 中,内部任何地方都可访问
<ForwardMessageProvider>
<Dialog>
<Composer.Input />
<MessagePreview /> {/* 可以读取状态 */}
<ForwardButton /> {/* 可以调用提交 */}
</Dialog>
</ForwardMessageProvider>
创建明确的变体组件,而非属性组合。
// 正确:自文档化的变体
function ThreadComposer({ channelId }) {
return (
<ThreadProvider channelId={channelId}>
<Composer.Frame>
<Composer.Input />
<AlsoSendToChannelField />
<Composer.Submit />
</Composer.Frame>
</ThreadProvider>
);
}
优先使用 children 进行组合。仅在需要回传数据时使用 render props。
// 正确:使用 children 构建结构
<Card>
<Card.Header>标题</Card.Header>
<Card.Body>内容</Card.Body>
</Card>
// 可以:回传数据时使用 render props
<List renderItem={({ item }) => <Item {...item} />} />
避免僵化的配置属性;优先使用可组合的 children API。
<Select value="abc" onChange={...}>
<Option value="abc">ABC</Option>
<Option value="xyz">XYZ</Option>
</Select>
使用 TypeScript 命名空间来组合组件及其类型,以便通过单一导入访问。
// 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) {
// ...
}
// 用法:单一导入
import { Button } from "~/components/button";
<Button variant="ghost">点击</Button>
function wrap(props: Button.Props) { ... }
重要提示: 命名空间应仅包含类型,绝不应包含运行时代码。
每周安装量
497
代码仓库
GitHub 星标数
80
首次出现
2026 年 1 月 28 日
安全审计
安装于
opencode452
codex446
gemini-cli435
github-copilot434
cursor418
kimi-cli397
Performance optimization and composition patterns for React components. Contains 33 rules across 6 categories focused on reducing re-renders, optimizing bundles, component composition, and avoiding common React pitfalls.
Reference these guidelines when:
Import directly from source, avoid barrel files.
// 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";
Load modules only when feature is activated.
useEffect(() => {
if (enabled && typeof window !== "undefined") {
import("./heavy-module").then((mod) => setModule(mod));
}
}, [enabled]);
Preload on hover/focus for perceived speed.
<button
onMouseEnter={() => import("./editor")}
onFocus={() => import("./editor")}
onClick={openEditor}
>
Open Editor
</button>
Use functional setState for stable callbacks.
// 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]);
}, []);
Derive state during render, not in effects.
// 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;
Pass function to useState for expensive initial values.
// Bad: runs expensiveComputation() on every render
const [data] = useState(expensiveComputation());
// Good: runs only on initial render
const [data] = useState(() => expensiveComputation());
Use primitive dependencies in effects.
// 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]);
Subscribe to derived booleans, not raw values.
// 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)");
Extract expensive work into memoized components.
// 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} />;
}
Hoist default non-primitive props to constants.
// Bad: breaks memoization (new function each render)
const Button = memo(({ onClick = () => {} }) => ...)
// Good: stable default value
const NOOP = () => {}
const Button = memo(({ onClick = NOOP }) => ...)
Don't wrap simple primitive expressions in useMemo.
// 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;
Put interaction logic in event handlers, not effects.
// Bad: effect re-runs on theme change
useEffect(() => {
if (submitted) post("/api/register");
}, [submitted, theme]);
// Good: in handler
const handleSubmit = () => post("/api/register");
Use startTransition for non-urgent updates.
// Good: non-blocking scroll tracking
const handler = () => {
startTransition(() => setScrollY(window.scrollY));
};
Use refs for transient frequent values.
// 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);
}, []);
Use ternary, not && for conditionals with numbers.
// Bad: renders "0" when count is 0
{
count && <Badge>{count}</Badge>;
}
// Good: renders nothing when count is 0
{
count > 0 ? <Badge>{count}</Badge> : null;
}
Extract static JSX outside components.
// 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 />;
}
Use content-visibility for long lists.
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
Animate wrapper div, not SVG element (for GPU acceleration).
// Good: hardware accelerated
<div className="animate-spin">
<svg>...</svg>
</div>
Reduce SVG coordinate precision with SVGO.
npx svgo --precision=1 --multipass icon.svg
Use inline script for client-only data to prevent flicker.
<div id="theme-wrapper">{children}</div>
<script dangerouslySetInnerHTML={{ __html: `
var theme = localStorage.getItem('theme') || 'light';
document.getElementById('theme-wrapper').className = theme;
` }} />
Suppress expected hydration mismatches.
<span suppressHydrationWarning>{new Date().toLocaleString()}</span>
Render browser-only components with ClientOnly and a fallback.
<ClientOnly fallback={<Skeleton />}>
{() => <Map />}
</ClientOnly>
Use useHydrated for SSR/CSR divergence.
let hydrated = useHydrated();
return hydrated ? <Widget /> : <Skeleton />;
Prefer useTransition over manual loading states.
const [isPending, startTransition] = useTransition();
let handleSearch = (value) => {
startTransition(async () => {
let data = await fetchResults(value);
setResults(data);
});
};
Place error boundaries at feature boundaries.
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
Use passive listeners for scroll/touch.
document.addEventListener("wheel", handler, { passive: true });
document.addEventListener("touchstart", handler, { passive: true });
Version and minimize localStorage data.
const VERSION = "v2";
function saveConfig(config: Config) {
try {
localStorage.setItem(`config:${VERSION}`, JSON.stringify(config));
} catch {} // Handle incognito/quota exceeded
}
Use useEffect only when absolutely necessary. Prefer derived state or event handlers.
// 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]);
Use named function declarations in useEffect for better debugging and self-documentation.
// 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);
};
}, []);
Don't add boolean props to customize behavior. Use composition instead.
// Bad: boolean prop explosion
<Composer isThread isEditing={false} showAttachments />
// Good: explicit variants
<ThreadComposer channelId="abc" />
<EditComposer messageId="xyz" />
Structure complex components as compound components with shared context.
// Good: compound components
<Composer.Provider state={state} actions={actions}>
<Composer.Frame>
<Composer.Input />
<Composer.Footer>
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
</Composer.Provider>
Lift state into provider components for cross-component access.
// Good: state in provider, accessible anywhere inside
<ForwardMessageProvider>
<Dialog>
<Composer.Input />
<MessagePreview /> {/* Can read state */}
<ForwardButton /> {/* Can call submit */}
</Dialog>
</ForwardMessageProvider>
Create explicit variant components instead of prop combinations.
// Good: self-documenting variants
function ThreadComposer({ channelId }) {
return (
<ThreadProvider channelId={channelId}>
<Composer.Frame>
<Composer.Input />
<AlsoSendToChannelField />
<Composer.Submit />
</Composer.Frame>
</ThreadProvider>
);
}
Prefer children for composition. Use render props only when passing data back.
// 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} />} />
Avoid rigid configuration props; prefer composable children APIs.
<Select value="abc" onChange={...}>
<Option value="abc">ABC</Option>
<Option value="xyz">XYZ</Option>
</Select>
Use TypeScript namespaces to combine component and its types for single-import access.
// 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) { ... }
Important: Namespaces should only contain types, never runtime code.
Weekly Installs
497
Repository
GitHub Stars
80
First Seen
Jan 28, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode452
codex446
gemini-cli435
github-copilot434
cursor418
kimi-cli397
Vue.js测试最佳实践:Vue 3组件、组合式函数、Pinia与异步测试完整指南
3,700 周安装
竞争对手研究指南:SEO、内容、反向链接与定价分析工具
231 周安装
Azure 工作负载自动升级评估工具 - 支持 Functions、App Service 计划与 SKU 迁移
231 周安装
Kaizen持续改进方法论:软件开发中的渐进式优化与防错设计实践指南
231 周安装
软件UI/UX设计指南:以用户为中心的设计原则、WCAG可访问性与平台规范
231 周安装
Apify 网络爬虫和自动化平台 - 无需编码抓取亚马逊、谷歌、领英等网站数据
231 周安装
llama.cpp 中文指南:纯 C/C++ LLM 推理,CPU/非 NVIDIA 硬件优化部署
231 周安装