react-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Best Practices
React最佳实践
Pair with TypeScript
与TypeScript搭配使用
When working with React, always load both this skill and together. TypeScript patterns (type-first development, discriminated unions, Zod validation) apply to React code.
typescript-best-practices在使用React时,请始终同时遵循本规范和。TypeScript的设计模式(类型优先开发、可辨识联合、Zod校验)同样适用于React代码。
typescript-best-practicesCore Principle: Effects Are Escape Hatches
核心原则:Effect是逃逸舱
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?"
Effect允许你“跳出”React体系,与外部系统进行同步。大多数组件逻辑不应使用Effect。在编写Effect之前,请先问自己:“有没有不用Effect就能实现的方式?”
When to Use Effects
何时使用Effect
Effects are for synchronizing with external systems:
- Subscribing to browser APIs (WebSocket, IntersectionObserver, resize)
- Connecting to third-party libraries not written in React
- Setting up/cleaning up event listeners on window/document
- Fetching data on mount (though prefer React Query or framework data fetching)
- Controlling non-React DOM elements (video players, maps, modals)
Effect用于与外部系统同步的场景:
- 订阅浏览器API(WebSocket、IntersectionObserver、resize事件)
- 接入非React编写的第三方库
- 在window/document上设置/清理事件监听器
- 组件挂载时获取数据(不过更推荐使用React Query或框架内置的数据获取方案)
- 控制非React DOM元素(视频播放器、地图、模态框)
When NOT to Use Effects
何时不应使用Effect
Derived State (Calculate During Render)
派生状态(在渲染时计算)
tsx
// 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;tsx
// 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;Expensive Calculations (Use useMemo)
昂贵计算(使用useMemo)
tsx
// 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]
);tsx
// 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]
);Resetting State on Prop Change (Use key)
属性变化时重置状态(使用key)
tsx
// 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
// ...
}tsx
// 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
// ...
}User Event Handling (Use Event Handlers)
用户事件处理(使用事件处理器)
tsx
// 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`);
}
// ...
}tsx
// 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`);
}
// ...
}Notifying Parent of State Changes
通知父组件状态变化
tsx
// 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);
}
// ...
}tsx
// 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);
}
// ...
}Chains of Effects
Effect链式调用
tsx
// 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);
}
}
}tsx
// 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);
}
}
}Effect Dependencies
Effect依赖项
Never Suppress the Linter
永远不要禁用Lint检查
tsx
// 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]);tsx
// 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]);Use Updater Functions to Remove State Dependencies
使用更新器函数移除状态依赖
tsx
// 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 neededtsx
// 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 neededMove Objects/Functions Inside Effects
将对象/函数移入Effect内部
tsx
// 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
}tsx
// 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
}useEffectEvent for Non-Reactive Logic
使用useEffectEvent处理非响应式逻辑
tsx
// 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
}tsx
// 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
}Wrap Callback Props with useEffectEvent
用useEffectEvent包裹回调属性
tsx
// 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
}tsx
// 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
}Effect Cleanup
Effect清理
Always Clean Up Subscriptions
始终清理订阅
tsx
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
}, []);tsx
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
}, []);Data Fetching with Ignore Flag
使用忽略标记处理数据请求
tsx
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]);tsx
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]);Development Double-Fire Is Intentional
开发环境下的双重触发是有意设计的
React remounts components in development to verify cleanup works. If you see effects firing twice, don't try to prevent it with refs:
tsx
// 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
}, []);React在开发环境下会重新挂载组件,以验证清理逻辑是否正常工作。如果你看到Effect触发了两次,不要尝试用Ref来阻止:
tsx
// 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
}, []);Refs
Refs
Use Refs for Values That Don't Affect Rendering
使用Ref存储不影响渲染的值
tsx
// 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!tsx
// 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!Never Read/Write ref.current During Render
永远不要在渲染期间读取/写入ref.current
tsx
// 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]);
}tsx
// 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]);
}Ref Callbacks for Dynamic Lists
动态列表使用Ref回调
tsx
// 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);
}
}}
/>
))}tsx
// 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);
}
}}
/>
))}useImperativeHandle for Controlled Exposure
使用useImperativeHandle控制暴露范围
tsx
// 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} />;
}tsx
// 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} />;
}Custom Hooks
自定义Hooks
Hooks Share Logic, Not State
Hooks共享逻辑而非状态
tsx
// Each call gets independent state
function StatusBar() {
const isOnline = useOnlineStatus(); // Own state
}
function SaveButton() {
const isOnline = useOnlineStatus(); // Separate state instance
}tsx
// Each call gets independent state
function StatusBar() {
const isOnline = useOnlineStatus(); // Own state
}
function SaveButton() {
const isOnline = useOnlineStatus(); // Separate state instance
}Name Hooks useXxx Only If They Use Hooks
只有使用了Hooks的函数才以useXxx命名
tsx
// 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);
}tsx
// 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);
}Avoid "Lifecycle" Hooks
避免“生命周期”类Hooks
tsx
// BAD: Custom lifecycle hooks
function useMount(fn) {
useEffect(() => {
fn();
}, []); // Missing dependency, linter can't catch it
}
// GOOD: Use useEffect directly
useEffect(() => {
doSomething();
}, [doSomething]);tsx
// BAD: Custom lifecycle hooks
function useMount(fn) {
useEffect(() => {
fn();
}, []); // Missing dependency, linter can't catch it
}
// GOOD: Use useEffect directly
useEffect(() => {
doSomething();
}, [doSomething]);Keep Custom Hooks Focused
保持自定义Hook的专注性
tsx
// GOOD: Focused, concrete use cases
useChatRoom({ serverUrl, roomId });
useOnlineStatus();
useFormInput(initialValue);
// BAD: Generic, abstract hooks
useMount(fn);
useEffectOnce(fn);
useUpdateEffect(fn);tsx
// GOOD: Focused, concrete use cases
useChatRoom({ serverUrl, roomId });
useOnlineStatus();
useFormInput(initialValue);
// BAD: Generic, abstract hooks
useMount(fn);
useEffectOnce(fn);
useUpdateEffect(fn);Component Patterns
组件模式
Controlled vs Uncontrolled
受控与非受控组件
tsx
// 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)} />;
}tsx
// 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)} />;
}Prefer Composition Over Prop Drilling
优先使用组合而非属性透传
tsx
// 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>tsx
// 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>flushSync for Synchronous DOM Updates
使用flushSync实现同步DOM更新
tsx
// 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();
}tsx
// 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();
}Summary: Decision Tree
总结:决策树
- Need to respond to user interaction? Use event handler
- Need computed value from props/state? Calculate during render
- Need cached expensive calculation? Use useMemo
- Need to reset state on prop change? Use key prop
- Need to synchronize with external system? Use Effect with cleanup
- Need non-reactive code in Effect? Use useEffectEvent
- Need mutable value that doesn't trigger render? Use ref
- 需要响应用户交互? 使用事件处理器
- 需要从属性/状态计算值? 在渲染期间计算
- 需要缓存昂贵计算结果? 使用useMemo
- 需要在属性变化时重置状态? 使用key属性
- 需要与外部系统同步? 使用带清理逻辑的Effect
- Effect中需要非响应式代码? 使用useEffectEvent
- 需要不触发渲染的可变值? 使用ref