【翻译】React的useEffectEvent必备注意事项指南

29 阅读4分钟

原文链接:slicker.me/react/useEf…

什么是 useEffectEvent

useEffectEvent 是一个 React Hook,它允许你将非响应式逻辑从 Effects 中提取出来。它解决了常见的问题:当你需要在 Effect 内部读取 props 或 state 的最新值时,却不希望这些值改变时导致该 Effect 重新运行。

核心概念useEffectEvent 创建一个稳定的函数引用,该引用始终读取最新值,但当这些值发生变化时不会触发 Effect 的重新执行。

解决的问题

假设有一个聊天应用,你希望在房间变更时记录消息,但需要在日志中包含当前主题。传统做法是将主题添加到依赖数组中,这会导致每次主题变更时效果都会重新运行——尽管你只希望对房间变更做出响应。

❌ 传统问题:

useEffect(() => {
  logVisit(roomId, theme); // Re-runs when theme changes
}, [roomId, theme]); // Had to include theme!

✅ 使用 useEffectEvent:

const onVisit = useEffectEvent((roomId) => {
  logVisit(roomId, theme); // Reads latest theme
});

useEffect(() => {
  onVisit(roomId); // Only re-runs when roomId changes
}, [roomId]);

Do's and Don'ts

✓ 建议

  • 用于读取最新 props/state 而不将其添加到依赖项中
  • 直接在 Effects 内部调用
  • 用于需要访问 Effect 上下文的事件处理程序
  • 用于分离响应式与非响应式逻辑
  • 在 Effect 内部同步调用

✗ 禁止事项

  • 禁止在常规事件处理程序中调用
  • 禁止在渲染过程中调用
  • 禁止将其作为组件 props 传递
  • 禁止异步调用或延迟调用
  • 禁止将其作为正确备忘录机制的替代方案

详细示例

✅ 操作:提取非反应性逻辑

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on('connected', onConnected);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // Only roomId is reactive
}

此效果仅在roomId变更时重新连接,但onConnected事件始终采用最新theme

❌ 禁止:从事件处理程序中调用

function Component() {
  const onClick = useEffectEvent(() => {
    // ❌ Wrong! Don't use in event handlers
    doSomething();
  });

  return <button onClick={onClick}>Click</button>;
}

请改用常规函数或 useCallback 作为事件处理程序。

✅ 操作指南:在效果中读取最新道具

function Timer({ interval, onTick }) {
  const onTickEvent = useEffectEvent(() => {
    onTick(); // Always calls latest onTick
  });

  useEffect(() => {
    const id = setInterval(onTickEvent, interval);
    return () => clearInterval(id);
  }, [interval]); // Only interval is reactive
}

❌ 不要:异步调用

function Component() {
  const onData = useEffectEvent((data) => {
    processData(data);
  });

  useEffect(() => {
    fetchData().then(data => {
      onData(data); // ❌ Risky! Called asynchronously
    });
  }, []);
}

该函数可能在组件卸载后或值发生变化后被调用。

✅ 操作:与清理逻辑结合使用

function Analytics({ userId, page }) {
  const logPageView = useEffectEvent(() => {
    analytics.track('page_view', { userId, page });
  });

  useEffect(() => {
    logPageView();
    
    return () => {
      // Cleanup can also use Effect Events
      const logPageExit = useEffectEvent(() => {
        analytics.track('page_exit', { userId, page });
      });
      logPageExit();
    };
  }, []); // Empty deps - runs once per mount
}

❌ 禁止:将依赖项传递给其他钩子

function Component() {
  const onEvent = useEffectEvent(() => {
    doSomething();
  });

  // ❌ Don't do this
  const memoized = useMemo(() => {
    return onEvent();
  }, [onEvent]);
}

常见使用场景

1. 日志记录与分析 当您需要记录包含最新用户偏好或设置的事件时,无需重新订阅。

const logEvent = useEffectEvent((eventName) => {
  analytics.log(eventName, { theme, locale, userId });
});

useEffect(() => {
  logEvent('page_visit');
}, [pathname]); // Only react to pathname changes

2. 带最新状态的回调 在向第三方库传递回调函数时,不应导致重新订阅。

const onMessage = useEffectEvent((msg) => {
  showToast(msg, { variant: userPreference });
});

useEffect(() => {
  const unsubscribe = messageService.subscribe(onMessage);
  return unsubscribe;
}, []); // Subscribe once, callback uses latest userPreference

3. 采用最新值实现去抖动 在实现防抖动时始终使用最新的回调逻辑。

const onSearch = useEffectEvent(() => {
  performSearch(query, filters, sortBy);
});

useEffect(() => {
  const timeoutId = setTimeout(onSearch, 500);
  return () => clearTimeout(timeoutId);
}, [query]); // Debounce query, but use latest filters/sortBy

最佳实践

  • 仅在确认确实需要读取非响应式值时使用 useEffectEvent
  • 请考虑您的逻辑是否真正属于 Effect,还是可置于事件处理程序中
  • 确保 Event 函数专注于单一目的
  • 记录使用原因以便未来维护者参考
  • 请在稳定版本发布后再用于生产环境应用

迁移路径

如果你当前正在使用useCallback处理不断变化的依赖项,或通过eslint-disable注释来抑制代码检查器,useEffectEvent可能是你需要的解决方案。不过,请先考虑是否更适合重构组件逻辑。

结论

useEffectEvent 是一款强大的工具,可解决在 Effects 中读取最新值时避免不必要重新执行的难题。遵循这些注意事项,当该功能稳定后,您就能有效地使用它。请记住,它适用于特定场景——即需要在 Effects 中将响应式逻辑与非响应式逻辑分离的情况。