使用对话框中的状态按钮或 Material UI 中的警报来反应内存泄漏警告

Posted

技术标签:

【中文标题】使用对话框中的状态按钮或 Material UI 中的警报来反应内存泄漏警告【英文标题】:React memory leaks warning with using a stateful button in a dialog or alert in Material UI 【发布时间】:2020-03-26 01:02:48 【问题描述】:

我正在将 Material UI 用于组件库,并注意到当我单击 Dialog 或 Alert 中的按钮(两个组件都管理打开/关闭状态)时,我收到了内存泄漏警告。我不确定如何解决这里的问题。按钮组件在单击时使用状态创建一个活动类,该类使用setTimeout onClick 使按钮单击在 UI 中更明显/更持久。

这是按钮组件:

function Button(
  classes,
  className,
  onClick,
  ...props
) 
  let [active, setActive] = useState(false);

  let handleClick = e => 
    e.persist();
    setActive(true);
    setTimeout(() => 
      setActive(false);
    , 250);
  if (typeof onClick === "function") onClick(e);
  ;

  return (
    <MuiButton
      variant=finalVariant(variant)
      className=`$active ? "Mui-active" : "" $className`
      classes=buttonClasses
      onClick=handleClick
      ...props
    />
  );


let containedStyle = color => (
  "&:active": 
    backgroundColor: color.dark
  ,
  "&.Mui-active": 
    backgroundColor: color.dark
  
);

这是我在单击警报或对话框组件中的按钮时收到的内存泄漏警告:

index.js:1437 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

我已尝试按照警告的建议使用useEffect 来清除活动状态,但没有运气。这是一个演示,当我使用使用 MUI 构建的自定义按钮时会发生什么

【问题讨论】:

stackblitz.com/fork/react 能不能把你的代码加在这里,让我看清楚 这里是警告codesandbox.io/s/…的演示 已完成 ;) 【参考方案1】:

发生这种情况是因为您的handleClick 函数使用了setTimeout

  let handleClick = e => 
    e.persist();
    setActive(true);
    setTimeout(() => 
      setActive(false);
    , 250);
    if (typeof onClick === "function") onClick(e);
  ;

更新状态。

当调用onClick 时,父组件正在卸载组件,但仍有订阅(您的超时)保持活动状态。

如果这是一次性事件,这并不是什么大不了的事,就像在这种情况下一样。这是一个警告,而不是错误。此警告的主要目的是让您知道在卸载某些内容后您是否保留订阅或引用很长时间。

有一些变通方法可以通过在组件未挂载时设置标志来消除警告,如果设置了标志,则不更新状态,但这并不能真正解决存在对卸载后保留的组件的引用。

解决此问题的更好方法是使用React.useRef() 保留对超时的引用,然后在useEffect() 中清除它,如下所示:

function Button(
  classes,
  className,
  onClick,
  ...props
) 
  let [active, setActive] = useState(false);
+ const timeout = React.useRef(undefined);

+ React.useEffect(() => 
+   return () => 
+     if (timeout.current !== undefined) 
+       clearTimeout(timeout.current);
+     
+   
+ , []);

  let handleClick = e => 
    e.persist();
    setActive(true);
-   setTimeout(() => 
+   timeout.current = setTimeout(() => 
      setActive(false);
    , 250);
    if (typeof onClick === "function") onClick(e);
  ; 

  return (
    <MuiButton
      variant=finalVariant(variant)
      className=`$active ? "Mui-active" : "" $className`
      classes=buttonClasses
      onClick=handleClick
      ...props
    />
  );

这可以像这样封装在一个钩子中:

  function useSafeTimeout() 
    const timeouts = React.useRef([])
    React.useEffect(() => 
      return () => 
        timeouts.forEach(timeout => 
          clearTimeout(timeout)
        )
      
    , [])

    return React.useCallback((fn, ms, ...args) => 
      const cancel = setTimeout(fn, ms, ...args)
      timeouts.current.push(cancel)
    , [])
  

并以这种方式使用:

function Button(
  classes,
  className,
  onClick,
  ...props
) 
  let [active, setActive] = useState(false);
+ const setTimeout = useSafeTimeout();

  let handleClick = e => 
    e.persist();
    setActive(true);
    setTimeout(() => 
      setActive(false);
    , 250);
    if (typeof onClick === "function") onClick(e);
  ; 

  return (
    <MuiButton
      variant=finalVariant(variant)
      className=`$active ? "Mui-active" : "" $className`
      classes=buttonClasses
      onClick=handleClick
      ...props
    />
  );

【讨论】:

嘿,丹!感谢您的回答 :) 上面添加到代码演示中的代码 sn-p 确实清除了内存泄漏警告(哇!),但看起来它没有根据需要在超时后清除活动状态/类。我在这里更新了:codesandbox.io/s/traffic-light-using-hooks-zpfrc **ive 让活动类为按钮设置了红色背景色,以便清楚地了解在单击按钮的整个流程中发生的事情 -> 查看状态变化。如果我遗漏了一些明显的东西,我们深表歉意! 这里是没有使用 useEffect + useRef 对比的版本:codesandbox.io/s/traffic-light-using-hooks-i8bid 哦,我的错,我犯了一个错误。修复!每次重新渲染组件时都会删除超时,而不仅仅是卸载组件时。 感谢您回复我/修改! :D!我还尝试添加更改并且超出了最大调用堆栈大小:`var cancel = setTimeout.apply(undefined, [fn, ms].concat(args));`。那就是实现useSafeTimeout 函数并将这个const setTimeout 设置为在handleClick 中调用该函数.. 有什么想法吗?我将继续四处寻找,但这是当前状态,此处有修订codesandbox.io/s/traffic-light-using-hooks-i8bid【参考方案2】:

这是我的解决方案:

function Button(
  classes,
  className,
  onClick,
  ...props
) 
  let [active, setActive] = useState(false);
  let timeoutIds = useRef([]);

    let registerTimeout = (f, ms) => 
    let timeoutId = setTimeout(f, ms);
    timeoutIds.current.push(timeoutId);
  ;

  let handleClick = e => 
    e.persist();
    setActive(true);
    if (typeof onClick === "function") onClick(e);
  ;

  let cleanup = () => 
    timeoutIds.current.forEach(clearTimeout);
  ;

  useEffect(() => 
    if (active === true) 
      registerTimeout(() => setActive(false), 250);
    
    return cleanup;
  , [active]);

  return (
    <MuiButton
      variant=finalVariant(variant)
      className=`$active ? "Mui-active" : "" $className`
      classes=buttonClasses
      onClick=handleClick
      ...props
    />
  );

【讨论】:

以上是关于使用对话框中的状态按钮或 Material UI 中的警报来反应内存泄漏警告的主要内容,如果未能解决你的问题,请参考以下文章

在对话框中按下按钮时,专注于 TextField 不起作用 - Material UI

Material-UI中的单选按钮和复选框不响应单击或点击

使用新状态数据刷新 Material-UI DataGrid 组件

Material-UI 的 Dialog 如何允许在对话框后面进行交互?

Material-ui 样式对话框/模态背景

Material UI Dialog 转为 Hook 闪烁