React最佳实践 react-best-practices

提供React钩子、效果、引用和组件设计的模式。涵盖逃生口、反模式和正确的效果使用。在阅读或编写React组件(带有React导入的.tsx、.jsx文件)时必须使用。

前端开发 0 次安装 0 次浏览 更新于 2/23/2026

React最佳实践

与TypeScript搭配使用

在使用React时,总是同时加载这个技巧和typescript-best-practices。TypeScript模式(类型优先开发,区分联合,Zod验证)适用于React代码。

核心原则:效果是逃生口

效果让你可以“走出”React以同步外部系统。大多数组件逻辑不应该使用效果。 在编写效果之前,问问自己:“有没有不使用效果就能做到的方法?”

何时使用效果

效果用于与外部系统同步:

  • 订阅浏览器API(WebSocket,IntersectionObserver,resize)
  • 连接到非React编写的第三方库
  • 在window/document上设置/清理事件监听器
  • 在挂载时获取数据(尽管更倾向于React Query或框架数据获取)
  • 控制非React DOM元素(视频播放器,地图,模态框)

何时不使用效果

派生状态(在渲染期间计算)

// 坏:效果用于派生状态
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;

昂贵的计算(使用useMemo)

// 坏:效果用于缓存
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
  setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);

// 好:useMemo用于昂贵的计算
const visibleTodos = useMemo(
  () => getFilteredTodos(todos, filter),
  [todos, filter]
);

在属性变化时重置状态(使用key)

// 坏:效果用于重置状态
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(''); // 自动重置
  // ...
}

用户事件处理(使用事件处理程序)

// 坏:效果中特定于事件的逻辑
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`);
  }
  // ...
}

通知父组件状态变化

// 坏:效果用于通知父组件
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);
  }
  // ...
}

效果链

// 坏:效果链
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

// 坏:抑制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]);

使用更新器函数移除状态依赖项

// 坏:消息在依赖项中导致每次消息重新连接
useEffect(() => {
  connection.on('message', (msg) => {
    setMessages([...messages, msg]);
  });
  // ...
}, [messages]); // 每次消息都会重新连接!

// 好:更新器函数移除依赖项
useEffect(() => {
  connection.on('message', (msg) => {
    setMessages(msgs => [...msgs, msg]);
  });
  // ...
}, []); // 不需要消息依赖项

将对象/函数移动到效果内部

// 坏:每次渲染创建对象触发效果
function ChatRoom({ roomId }) {
  const options = { serverUrl, roomId }; // 每次渲染新对象
  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // 每次渲染都会重新连接!
}

// 好:在效果内创建对象
function ChatRoom({ roomId }) {
  useEffect(() => {
    const options = { serverUrl, roomId };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]); // 只有在值变化时才重新连接
}

使用useEffectEvent进行非响应式逻辑

// 坏:主题更改重新连接聊天
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]); // 主题不再导致重新连接
}

将回调属性包装在useEffectEvent中

// 坏:回调属性在依赖项中
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在开发中重新挂载组件以验证清理工作。如果你看到效果两次触发,不要尝试用refs阻止它:

// 坏:隐藏症状
const didInit = useRef(false);
useEffect(() => {
  if (didInit.current) return;
  didInit.current = true;
  // ...
}, []);

// 好:修复清理
useEffect(() => {
  const connection = createConnection();
  connection.connect();
  return () => connection.disconnect(); // 适当的清理
}, []);

Refs

使用Refs用于不影响渲染的值

// 好:用于超时ID的Ref(不影响UI)
const timeoutRef = useRef(null);

function handleClick() {
  clearTimeout(timeoutRef.current);
  timeoutRef.current = setTimeout(() => {
    // ...
  }, 1000);
}

// 坏:使用ref显示值
const countRef = useRef(0);
countRef.current++; // UI不会更新!

在渲染期间永远不要读取/写入ref.current

// 坏:在渲染期间读取ref
function MyComponent() {
  const ref = useRef(0);
  ref.current++; // 在渲染期间改变!
  return <div>{ref.current}</div>; // 在渲染期间读取!
}

// 好:在事件处理程序和效果中读取/写入refs
function MyComponent() {
  const ref = useRef(0);

  function handleClick() {
    ref.current++; // 在事件处理程序中OK
  }

  useEffect(() => {
    ref.current = someValue; // 在效果中OK
  }, [someValue]);
}

为动态列表使用Ref回调

// 坏:不能在循环中调用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);
      }
    }}
  />
))}

使用useImperativeHandle控制暴露

// 限制父组件可以访问的内容
function MyInput({ ref }) {
  const realInputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus() {
      realInputRef.current.focus();
    },
    // 父组件只能调用focus(),不能访问完整的DOM节点
  }));

  return <input ref={realInputRef} />;
}

自定义Hooks

Hooks共享逻辑,不共享状态

// 每次调用获得独立的状态
function StatusBar() {
  const isOnline = useOnlineStatus(); // 自己的状态
}

function SaveButton() {
  const isOnline = useOnlineStatus(); // 单独的状态实例
}

名称Hooks useXxx只有在它们使用Hooks时

// 坏:useXxx但没有使用hooks
function useSorted(items) {
  return items.slice().sort();
}

// 好:普通函数
function getSorted(items) {
  return items.slice().sort();
}

// 好:使用hooks,所以前缀为use
function useAuth() {
  return useContext(AuthContext);
}

避免“生命周期”Hooks

// 坏:自定义生命周期hooks
function useMount(fn) {
  useEffect(() => {
    fn();
  }, []); // 缺少依赖项,linter无法捕获
}

// 好:直接使用useEffect
useEffect(() => {
  doSomething();
}, [doSomething]);

保持自定义Hooks专注

// 好:专注,具体用例
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>

flushSync用于同步DOM更新

// 当你需要在状态更新后立即读取DOM
import { flushSync } from 'react-dom';

function handleAdd() {
  flushSync(() => {
    setTodos([...todos, newTodo]);
  });
  // DOM现在已更新,可以安全读取
  listRef.current.lastChild.scrollIntoView();
}

总结:决策树

  1. 需要响应用户交互吗? 使用事件处理程序
  2. 需要从props/state计算值吗? 在渲染期间计算
  3. 需要缓存昂贵的计算吗? 使用useMemo
  4. 需要在属性变化时重置状态吗? 使用key属性
  5. 需要与外部系统同步吗? 使用效果并清理
  6. 需要效果中的非响应式代码吗? 使用useEffectEvent
  7. 需要不触发渲染的可变值吗? 使用ref