react-best-practices by 0xbigboss/claude-code
npx skills add https://github.com/0xbigboss/claude-code --skill react-best-practices在使用 React 时,请始终同时加载此技能和 typescript-best-practices。TypeScript 模式(类型优先开发、可辨识联合、Zod 验证)同样适用于 React 代码。
Effect 让你可以"走出" React 以与外部系统同步。大多数组件逻辑不应使用 Effect。 在编写 Effect 之前,先问自己:"有没有办法不用 Effect 来实现这个?"
Effect 用于与外部系统同步:
// 错误:使用 Effect 处理派生状态
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// 正确:在渲染期间计算
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const fullName = firstName + ' ' + lastName;
// 错误:使用 Effect 进行缓存
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// 正确:使用 useMemo 处理昂贵计算
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
When working with React, always load both this skill and typescript-best-practices together. TypeScript patterns (type-first development, discriminated unions, Zod validation) apply to React code.
Effects let you "step outside" React to synchronize with external systems. Most component logic should NOT use Effects. Before writing an Effect, ask: "Is there a way to do this without an Effect?"
Effects are for synchronizing with external systems :
// BAD: Effect for derived state
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// GOOD: Calculate during render
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const fullName = firstName + ' ' + lastName;
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// 错误:使用 Effect 重置状态
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
// 正确:使用 key 重置组件状态
function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}
function Profile({ userId }) {
const [comment, setComment] = useState(''); // 自动重置
// ...
}
// 错误:在 Effect 中处理特定事件逻辑
function ProductPage({ product, addToCart }) {
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to cart`);
}
}, [product]);
// ...
}
// 正确:在事件处理器中处理逻辑
function ProductPage({ product, addToCart }) {
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to cart`);
}
// ...
}
// 错误:使用 Effect 通知父组件
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
onChange(isOn);
}, [isOn, onChange]);
// ...
}
// 正确:在事件处理器中同时更新
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
function updateToggle(nextIsOn) {
setIsOn(nextIsOn);
onChange(nextIsOn);
}
// ...
}
// 最佳:完全受控组件
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
// ...
}
// 错误:Effect 链
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1);
setGoldCardCount(0);
}
}, [goldCardCount]);
// 正确:计算派生状态,在事件处理器中更新
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
}
}
}
// 错误:禁用 linter 会隐藏错误
useEffect(() => {
const id = setInterval(() => {
setCount(count + increment);
}, 1000);
return () => clearInterval(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 正确:修复代码,而不是禁用 linter
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + increment);
}, 1000);
return () => clearInterval(id);
}, [increment]);
// 错误:messages 在依赖项中导致每条消息都重新连接
useEffect(() => {
connection.on('message', (msg) => {
setMessages([...messages, msg]);
});
// ...
}, [messages]); // 每条消息都重新连接!
// 正确:更新器函数移除了依赖
useEffect(() => {
connection.on('message', (msg) => {
setMessages(msgs => [...msgs, msg]);
});
// ...
}, []); // 不需要 messages 依赖
// 错误:每次渲染创建的对象都会触发 Effect
function ChatRoom({ roomId }) {
const options = { serverUrl, roomId }; // 每次渲染都创建新对象
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // 每次渲染都重新连接!
// 正确:在 Effect 内部创建对象
function ChatRoom({ roomId }) {
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // 仅在值改变时重新连接
}
// 错误:主题变更导致聊天重新连接
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // 主题变更时重新连接!
// 正确:使用 useEffectEvent 处理非响应式逻辑
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // 主题不再导致重新连接
}
// 错误:回调属性在依赖项中
function ChatRoom({ roomId, onReceiveMessage }) {
useEffect(() => {
connection.on('message', onReceiveMessage);
// ...
}, [roomId, onReceiveMessage]); // 父组件重新渲染时重新连接
// 正确:使用 useEffectEvent 包装回调
function ChatRoom({ roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);
useEffect(() => {
connection.on('message', onMessage);
// ...
}, [roomId]); // 稳定的依赖列表
}
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect(); // 必需
}, [roomId]);
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll); // 必需
}, []);
useEffect(() => {
let ignore = false;
async function fetchData() {
const result = await fetchTodos(userId);
if (!ignore) {
setTodos(result);
}
}
fetchData();
return () => {
ignore = true; // 防止旧请求的过时数据
};
}, [userId]);
React 在开发环境下会重新挂载组件以验证清理工作是否正常。如果你看到 Effect 触发了两次,不要尝试用 ref 来阻止它:
// 错误:掩盖症状
const didInit = useRef(false);
useEffect(() => {
if (didInit.current) return;
didInit.current = true;
// ...
}, []);
// 正确:修复清理逻辑
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect(); // 正确的清理
}, []);
// 正确:使用 ref 存储超时 ID(不影响 UI)
const timeoutRef = useRef(null);
function handleClick() {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
// ...
}, 1000);
}
// 错误:使用 ref 存储显示的值
const countRef = useRef(0);
countRef.current++; // UI 不会更新!
// 错误:在渲染期间读取 ref
function MyComponent() {
const ref = useRef(0);
ref.current++; // 在渲染期间修改!
return <div>{ref.current}</div>; // 在渲染期间读取!
// 正确:在事件处理器和 Effect 中读取/写入 refs
function MyComponent() {
const ref = useRef(0);
function handleClick() {
ref.current++; // 在事件处理器中 OK
}
useEffect(() => {
ref.current = someValue; // 在 Effect 中 OK
}, [someValue]);
}
// 错误:不能在循环中调用 useRef
{items.map((item) => {
const ref = useRef(null); // 违反规则!
return <li ref={ref} />;
})}
// 正确:使用 Map 的 ref 回调
const itemsRef = useRef(new Map());
{items.map((item) => (
<li
key={item.id}
ref={(node) => {
if (node) {
itemsRef.current.set(item.id, node);
} else {
itemsRef.current.delete(item.id);
}
}}
/>
))}
// 限制父组件可以访问的内容
function MyInput({ ref }) {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
realInputRef.current.focus();
},
// 父组件只能调用 focus(),不能访问完整的 DOM 节点
}));
return <input ref={realInputRef} />;
}
// 每次调用都获得独立的状态
function StatusBar() {
const isOnline = useOnlineStatus(); // 自己的状态
}
function SaveButton() {
const isOnline = useOnlineStatus(); // 独立的状态实例
}
// 错误:命名为 useXxx 但不使用 hooks
function useSorted(items) {
return items.slice().sort();
}
// 正确:普通函数
function getSorted(items) {
return items.slice().sort();
}
// 正确:使用 hooks,所以前缀为 use
function useAuth() {
return useContext(AuthContext);
}
// 错误:自定义生命周期 hooks
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 缺少依赖项,linter 无法捕获
}
// 正确:直接使用 useEffect
useEffect(() => {
doSomething();
}, [doSomething]);
// 正确:专注、具体的用例
useChatRoom({ serverUrl, roomId });
useOnlineStatus();
useFormInput(initialValue);
// 错误:通用、抽象的 hooks
useMount(fn);
useEffectOnce(fn);
useUpdateEffect(fn);
// 非受控:组件拥有状态
function SearchInput() {
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
// 受控:父组件拥有状态
function SearchInput({ query, onQueryChange }) {
return <input value={query} onChange={e => onQueryChange(e.target.value)} />;
}
// 错误:属性透传
<App user={user}>
<Layout user={user}>
<Header user={user}>
<Avatar user={user} />
</Header>
</Layout>
</App>
// 正确:使用 children 的组合
<App>
<Layout>
<Header avatar={<Avatar user={user} />} />
</Layout>
</App>
// 正确:使用 Context 处理真正的全局状态
<UserContext.Provider value={user}>
<App />
</UserContext.Provider>
// 当需要在状态更新后立即读取 DOM 时
import { flushSync } from 'react-dom';
function handleAdd() {
flushSync(() => {
setTodos([...todos, newTodo]);
});
// DOM 现已更新,可以安全读取
listRef.current.lastChild.scrollIntoView();
}
每周安装量
1.9K
仓库
GitHub 星标
36
首次出现
2026年1月20日
安全审计
安装于
opencode1.6K
gemini-cli1.6K
codex1.5K
github-copilot1.5K
amp1.4K
kimi-cli1.4K
// BAD: Effect for caching
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// GOOD: useMemo for expensive calculations
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
// BAD: Effect to reset state
function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
// GOOD: Use key to reset component state
function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}
function Profile({ userId }) {
const [comment, setComment] = useState(''); // Resets automatically
// ...
}
// BAD: Event-specific logic in Effect
function ProductPage({ product, addToCart }) {
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to cart`);
}
}, [product]);
// ...
}
// GOOD: Logic in event handler
function ProductPage({ product, addToCart }) {
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to cart`);
}
// ...
}
// BAD: Effect to notify parent
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
useEffect(() => {
onChange(isOn);
}, [isOn, onChange]);
// ...
}
// GOOD: Update both in event handler
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
function updateToggle(nextIsOn) {
setIsOn(nextIsOn);
onChange(nextIsOn);
}
// ...
}
// BEST: Fully controlled component
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
// ...
}
// BAD: Effect chain
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1);
setGoldCardCount(0);
}
}, [goldCardCount]);
// GOOD: Calculate derived state, update in event handler
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
}
}
}
// BAD: Suppressing linter hides bugs
useEffect(() => {
const id = setInterval(() => {
setCount(count + increment);
}, 1000);
return () => clearInterval(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// GOOD: Fix the code, not the linter
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + increment);
}, 1000);
return () => clearInterval(id);
}, [increment]);
// BAD: messages in dependencies causes reconnection on every message
useEffect(() => {
connection.on('message', (msg) => {
setMessages([...messages, msg]);
});
// ...
}, [messages]); // Reconnects on every message!
// GOOD: Updater function removes dependency
useEffect(() => {
connection.on('message', (msg) => {
setMessages(msgs => [...msgs, msg]);
});
// ...
}, []); // No messages dependency needed
// BAD: Object created each render triggers Effect
function ChatRoom({ roomId }) {
const options = { serverUrl, roomId }; // New object each render
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // Reconnects every render!
}
// GOOD: Create object inside Effect
function ChatRoom({ roomId }) {
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // Only reconnects when values change
}
// BAD: theme change reconnects chat
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // Reconnects on theme change!
}
// GOOD: useEffectEvent for non-reactive logic
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // theme no longer causes reconnection
}
// BAD: Callback prop in dependencies
function ChatRoom({ roomId, onReceiveMessage }) {
useEffect(() => {
connection.on('message', onReceiveMessage);
// ...
}, [roomId, onReceiveMessage]); // Reconnects if parent re-renders
}
// GOOD: Wrap callback in useEffectEvent
function ChatRoom({ roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);
useEffect(() => {
connection.on('message', onMessage);
// ...
}, [roomId]); // Stable dependency list
}
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect(); // REQUIRED
}, [roomId]);
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll); // REQUIRED
}, []);
useEffect(() => {
let ignore = false;
async function fetchData() {
const result = await fetchTodos(userId);
if (!ignore) {
setTodos(result);
}
}
fetchData();
return () => {
ignore = true; // Prevents stale data from old requests
};
}, [userId]);
React remounts components in development to verify cleanup works. If you see effects firing twice, don't try to prevent it with refs:
// BAD: Hiding the symptom
const didInit = useRef(false);
useEffect(() => {
if (didInit.current) return;
didInit.current = true;
// ...
}, []);
// GOOD: Fix the cleanup
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect(); // Proper cleanup
}, []);
// GOOD: Ref for timeout ID (doesn't affect UI)
const timeoutRef = useRef(null);
function handleClick() {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
// ...
}, 1000);
}
// BAD: Using ref for displayed value
const countRef = useRef(0);
countRef.current++; // UI won't update!
// BAD: Reading ref during render
function MyComponent() {
const ref = useRef(0);
ref.current++; // Mutating during render!
return <div>{ref.current}</div>; // Reading during render!
}
// GOOD: Read/write refs in event handlers and effects
function MyComponent() {
const ref = useRef(0);
function handleClick() {
ref.current++; // OK in event handler
}
useEffect(() => {
ref.current = someValue; // OK in effect
}, [someValue]);
}
// BAD: Can't call useRef in a loop
{items.map((item) => {
const ref = useRef(null); // Rule violation!
return <li ref={ref} />;
})}
// GOOD: Ref callback with Map
const itemsRef = useRef(new Map());
{items.map((item) => (
<li
key={item.id}
ref={(node) => {
if (node) {
itemsRef.current.set(item.id, node);
} else {
itemsRef.current.delete(item.id);
}
}}
/>
))}
// Limit what parent can access
function MyInput({ ref }) {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
realInputRef.current.focus();
},
// Parent can ONLY call focus(), not access full DOM node
}));
return <input ref={realInputRef} />;
}
// Each call gets independent state
function StatusBar() {
const isOnline = useOnlineStatus(); // Own state
}
function SaveButton() {
const isOnline = useOnlineStatus(); // Separate state instance
}
// BAD: useXxx but doesn't use hooks
function useSorted(items) {
return items.slice().sort();
}
// GOOD: Regular function
function getSorted(items) {
return items.slice().sort();
}
// GOOD: Uses hooks, so prefix with use
function useAuth() {
return useContext(AuthContext);
}
// BAD: Custom lifecycle hooks
function useMount(fn) {
useEffect(() => {
fn();
}, []); // Missing dependency, linter can't catch it
}
// GOOD: Use useEffect directly
useEffect(() => {
doSomething();
}, [doSomething]);
// GOOD: Focused, concrete use cases
useChatRoom({ serverUrl, roomId });
useOnlineStatus();
useFormInput(initialValue);
// BAD: Generic, abstract hooks
useMount(fn);
useEffectOnce(fn);
useUpdateEffect(fn);
// Uncontrolled: component owns state
function SearchInput() {
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
// Controlled: parent owns state
function SearchInput({ query, onQueryChange }) {
return <input value={query} onChange={e => onQueryChange(e.target.value)} />;
}
// BAD: Prop drilling
<App user={user}>
<Layout user={user}>
<Header user={user}>
<Avatar user={user} />
</Header>
</Layout>
</App>
// GOOD: Composition with children
<App>
<Layout>
<Header avatar={<Avatar user={user} />} />
</Layout>
</App>
// GOOD: Context for truly global state
<UserContext.Provider value={user}>
<App />
</UserContext.Provider>
// When you need to read DOM immediately after state update
import { flushSync } from 'react-dom';
function handleAdd() {
flushSync(() => {
setTodos([...todos, newTodo]);
});
// DOM is now updated, safe to read
listRef.current.lastChild.scrollIntoView();
}
Weekly Installs
1.9K
Repository
GitHub Stars
36
First Seen
Jan 20, 2026
Security Audits
Installed on
opencode1.6K
gemini-cli1.6K
codex1.5K
github-copilot1.5K
amp1.4K
kimi-cli1.4K
AI新闻播客制作技能:实时新闻转对话式播客脚本与音频生成
1,200 周安装