React:“无法在未安装的组件上执行 React 状态更新”,没有 useEffect 功能

Posted

技术标签:

【中文标题】React:“无法在未安装的组件上执行 React 状态更新”,没有 useEffect 功能【英文标题】:React: "Can't perform a React state update on an unmounted component" without useEffect function 【发布时间】:2021-12-14 00:06:38 【问题描述】:

(我正在使用 Next.js + Styled Components,我完全是一个初学者,请帮助我:))

我正在开发一种“Netflix”页面,其中包含不同类型的目录组件。 页面网格中的每个内容都是一个非常复杂的组件,有很多交互,称为 ContentItem.js,在 ContentList.js 中重复。

所以,我收到了这个错误:

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.
    at ContentItem (webpack-internal:///./ltds/components/Shelf/ContentItem.js:104:62)
    at ul
    at O (webpack-internal:///./node_modules/styled-components/dist/styled-components.browser.esm.js:31:19797)
    at ContentList (webpack-internal:///./ltds/components/Shelf/ContentList.js:52:23)
    at div
    at O (webpack-internal:///./node_modules/styled-components/dist/styled-components.browser.esm.js:31:19797)
    at Shelf (webpack-internal:///./ltds/components/Shelf/Shelf.js:57:66)
    at div
    at SearchResult (webpack-internal:///./pages/search/[term].js:32:70)

但是,在这个组件中,我没有使用 useEffect:

import Image from 'next/image';
import  Paragraph  from '../../styles/Typography';
import styled from 'styled-components';
import  gridUnit  from '../../styles/GlobalStyle';
import  useEffect, useState  from 'react'; 
import  Transition  from 'react-transition-group';
import React from 'react';
import Icon from '../Icon';

const ContentItemContainer = styled.li`
  margin-bottom: 16px;
  text-decoration: none;
  transition: all 0.2s;
  position: relative;
  border-radius: $(props => props.theme.radius.lg.value)$gridUnit;
  overflow: hidden;
  height: auto;

  &:hover 
    cursor: pointer;
    transform: $props => (props.isClicking ? "scale(0.98)" : "scale(1.04)");
  
`;
const ItemCover = styled(Image)`
  
  border-radius: $(props => props.theme.radius.lg.value)$gridUnit;
  border: 1px solid #504F4E;
  overflow: visible;
  position: relative;

  transition: 0.2s;
  opacity: $( state ) => (state === "entering" ? 0 : 1);
`;

const ItemHoverContainer = styled.div`
  position: absolute;
  z-index: 10;
  top: 0;
  left: 0;
  right: 0;
  padding: 0px;
  margin: 0px;
  height: auto;

  &:hover
    border: 0.8px solid $props => (props.theme.alias.image.border.value);
    border-radius: $(props => props.theme.radius.lg.value)$gridUnit;
  
  
`;

const ItemHoverImage = styled(Image)`
  border-radius: 15px; //15px not 16px: hack to avoid a "phantom line" at the bottom of image

  transition: 0.4s;
  display: $( state ) => (state === "exited" ? "none" : "block");
  opacity: $( state ) => (state === "entered" ? 1 : 0);
 
`;

const IconContainer = styled.div`
  position: absolute;
  left: 41.84%;
  right: 41.13%;
  top: 42.58%;
  bottom: 42.11%;
`;
const DetailsContainer = styled(Paragraph)`

  padding-top: $( state ) => (state === "entered" ? props => props.theme.spacing[1].value+gridUnit : 0);
  transition: 0.4s;
  opacity: $( state ) => (state === "entered" ? 1 : 0);
  height: $( state ) => (state === "entered" ? 1 : 0);
  display: $( state ) => (state === "exited" ? "none" : "block");
  
`;

function ContentItem(props) 

  const nodeRef = React.useRef(null);
  const [isHovering, setIsHovering] = useState(false);
  const [isClicking, setIsClicking] = useState(false);
  const [isLoaded, setIsLoaded] = useState(false);

  const coverSizes = 
    wide:
      width: 236, 
      height:139
    ,
    poster:
      width: 144, 
      height: 192
    
  

  function handleMouseOver(event) 
    setIsHovering(!isHovering)
  
  function handleMouseOut(event) 
    setIsHovering(!isHovering)
  

  function handleMouseDown(event) 
    setIsClicking(!isClicking)
  
  function handleMouseUp(event) 
    setIsClicking(!isClicking)
  
  function handleLoadingComplete(event) 
    !isLoaded && (setIsLoaded(true))
  

  return (
    
    <ContentItemContainer isClicking=isClicking onMouseOver=handleMouseOver onMouseOut=handleMouseOut onMouseDown=handleMouseDown onMouseUp=handleMouseUp>
      <Transition in=isLoaded timeout=0 nodeRef=nodeRef>
      (state) => ( <div>
        <ItemCover 
            src=props.coverType == "wide" ? props.wideCover : props.posterCover  
            alt=props.alt 
            layout='responsive'   
            width=props.coverType == "wide" ? coverSizes.wide.width : coverSizes.poster.width 
            height=props.coverType == "wide" ? coverSizes.wide.height+1 : coverSizes.poster.height//+1: hack to avoid cut at the bottom of image
            placeholder='blur'
            blurDataURL=props.coverPlaceholder
            onLoadingComplete=handleLoadingComplete  
        />
        </div>)
      </Transition>

      <ItemHoverContainer>
        <Transition in=isHovering timeout=0 nodeRef=nodeRef mountOnEnter unmountOnExit>
          (state) => (
          <div>
            <ItemHoverImage 
              src=props.coverType == "wide" ? props.wideLoopVideo : props.posterLoopVideo 
              layout='responsive' 
              width=props.coverType == "wide" ? coverSizes.wide.width : coverSizes.poster.width 
              height=props.coverType == "wide" ? coverSizes.wide.height : coverSizes.poster.height+1 //+1: hack to avoid a "phantom line" at the bottom of image
              state=state
            />
            <IconContainer>
              <Icon preserveAspectRatio="xMinYMin meet" name="coverPlay"/>
              </IconContainer>
          </div>
          )  
        </Transition>
      </ItemHoverContainer>


      <Transition in=props.isDetailed timeout=100 nodeRef=nodeRef>
        (state) => (
          <DetailsContainer state=state isDetailed=props.isDetailed>props.content.details</DetailsContainer>
        )
      </Transition>
      

    </ContentItemContainer>

  );
  

  export default ContentItem

我该如何解决这个问题?

更新

我尝试使用基于@MB_ 答案的useEffect,但仍然发生内存泄漏错误:

import React,  useState, useRef, useEffect  from 'react';
import Image from 'next/image';
import  Transition  from 'react-transition-group';
import styled from 'styled-components';
import  Paragraph  from '../../styles/Typography';
import  gridUnit  from '../../styles/GlobalStyle';

import Icon from '../Icon';

function ContentItem(props) 

  const [isHovering, setIsHovering] = useState(false);
  const [isClicking, setIsClicking] = useState(false);
  const [isLoaded, setIsLoaded] = useState(false);

  const nodeRef = useRef(null);
  const mouseRef = useRef(null);
  const imgRef = useRef(null);

  useEffect(() => 

    const currentMouseRef = mouseRef.current;
    
    if (currentMouseRef) 
      currentMouseRef.addEventListener('mouseover', handleMouseOver);
      currentMouseRef.addEventListener('mouseout', handleMouseOut);
      currentMouseRef.addEventListener('mousedown', handleMouseDown);
      currentMouseRef.addEventListener('mouseup', handleMouseUp);

      return () => 
        currentMouseRef.removeEventListener('mouseover', handleMouseOver);
        currentMouseRef.removeEventListener('mouseout', handleMouseOut);
        currentMouseRef.removeEventListener('mousedown', handleMouseDown);
        currentMouseRef.removeEventListener('mouseup', handleMouseUp);
      ;
    
  , []);

  const handleMouseOver = () => setIsHovering(true);
  const handleMouseOut = () => setIsHovering(false);
  const handleMouseDown = () => setIsClicking(true);
  const handleMouseUp = () => setIsClicking(false);

  const handleLoadingComplete = () => !isLoaded && setIsLoaded(true);

  const coverSizes = 
    wide:
      width: 236, 
      height:139
    ,
    poster:
      width: 144, 
      height: 192
    
  

  return (
    
    <ContentItemContainer 
      ref=mouseRef 
      onMouseOver=handleMouseOver 
      onMouseOut=handleMouseOut 
      onMouseDown=handleMouseDown 
      onMouseUp=handleMouseUp
      isClicking=isClicking 
    >
      <Transition in=isLoaded timeout=0 nodeRef=nodeRef>
      (state) => ( <div>
        <ItemCover 
            src=props.coverType == "wide" ? props.wideCover : props.posterCover  
            alt=props.alt 
            layout='responsive'   
            width=props.coverType == "wide" ? coverSizes.wide.width : coverSizes.poster.width 
            height=props.coverType == "wide" ? coverSizes.wide.height+1 : coverSizes.poster.height//+1: hack to avoid cut at the bottom of image
            placeholder='blur'
            blurDataURL=props.coverPlaceholder
            onLoadingComplete=handleLoadingComplete  
        />
        </div>)
      </Transition>

      <ItemHoverContainer>
        <Transition in=isHovering timeout=0 nodeRef=nodeRef mountOnEnter unmountOnExit>
          (state) => (
          <div>
            <ItemHoverImage 
              src=props.coverType == "wide" ? props.wideLoopVideo : props.posterLoopVideo 
              layout='responsive' 
              width=props.coverType == "wide" ? coverSizes.wide.width : coverSizes.poster.width 
              height=props.coverType == "wide" ? coverSizes.wide.height : coverSizes.poster.height+1 //+1: hack to avoid a "phantom line" at the bottom of image
              state=state
            />
            <IconContainer>
              <Icon preserveAspectRatio="xMinYMin meet" name="coverPlay"/>
              </IconContainer>
          </div>
          )  
        </Transition>
      </ItemHoverContainer>


      <Transition in=props.isDetailed timeout=100 nodeRef=nodeRef>
        (state) => (
          <DetailsContainer state=state isDetailed=props.isDetailed>props.content.details</DetailsContainer>
        )
      </Transition>
      

    </ContentItemContainer>

  );
  

  export default ContentItem

  const ContentItemContainer = styled.li`
  margin-bottom: 16px;
  text-decoration: none;
  transition: all 0.2s;
  position: relative;
  border-radius: $(props => props.theme.radius.lg.value)$gridUnit;
  overflow: hidden;
  height: auto;

  &:hover 
    cursor: pointer;
    transform: $props => (props.isClicking ? "scale(0.98)" : "scale(1.04)");
  
`;
const ItemCover = styled(Image)`
  
  border-radius: $(props => props.theme.radius.lg.value)$gridUnit;
  border: 1px solid #504F4E;
  overflow: visible;
  position: relative;

  transition: 0.2s;
  opacity: $( state ) => (state === "entering" ? 0 : 1);
`;

const ItemHoverContainer = styled.div`
  position: absolute;
  z-index: 10;
  top: 0;
  left: 0;
  right: 0;
  padding: 0px;
  margin: 0px;
  height: auto;

  &:hover
    border: 0.8px solid $props => (props.theme.alias.image.border.value);
    border-radius: $(props => props.theme.radius.lg.value)$gridUnit;
  
  
`;

const ItemHoverImage = styled(Image)`
  border-radius: 15px; //15px not 16px: hack to avoid a "phantom line" at the bottom of image

  transition: 0.4s;
  display: $( state ) => (state === "exited" ? "none" : "block");
  opacity: $( state ) => (state === "entered" ? 1 : 0);
 
`;

const IconContainer = styled.div`
  position: absolute;
  left: 41.84%;
  right: 41.13%;
  top: 42.58%;
  bottom: 42.11%;
`;
const DetailsContainer = styled(Paragraph)`

  padding-top: $( state ) => (state === "entered" ? props => props.theme.spacing[1].value+gridUnit : 0);
  transition: 0.4s;
  opacity: $( state ) => (state === "entered" ? 1 : 0);
  height: $( state ) => (state === "entered" ? 1 : 0);
  display: $( state ) => (state === "exited" ? "none" : "block");
  
`;

【问题讨论】:

【参考方案1】:

使用EventListeners时需要useEffect

// (1)
import React,  useState, useRef, useEffect  from 'react';
import Image from 'next/image';
import  Transition  from 'react-transition-group';
import styled from 'styled-components';
import  Paragraph  from '../../styles/Typography';
import  gridUnit  from '../../styles/GlobalStyle';

import Icon from '../Icon';

export default function ContentItem(props)                  // (2)
  const [isHovering, setIsHovering] = useState(false);
  const [isClicking, setIsClicking] = useState(false);
  const [isLoaded, setIsLoaded] = useState(false);

  const nodeRef = useRef(null);
  const mouseRef = useRef(null);               // create another ref for mouse listener


  useEffect(() => 
    if (mouseRef.current) 
      mouseRef.current.addEventListener('mouseover', handleMouseOver);
      mouseRef.current.addEventListener('mouseout', handleMouseOut);

      return () => 
        mouseRef.current.removeEventListener('mouseover', handleMouseOver);
        mouseRef.current.removeEventListener('mouseout', handleMouseOut);
      ;
    
  , [mouseRef.current]);


  const handleMouseOver = () => setIsHovering(true);
  const handleMouseOut = () => setIsHovering(false);
  const toggleClick = () => setIsClicking(!isClicking);
  const handleLoadingComplete = () => !isLoaded && setIsLoaded(true);


  const coverSizes = 
    wide: 
      width: 236,
      height: 139,
    ,
    poster: 
      width: 144,
      height: 192,
    ,
  ;

  return (
    <ContentItemContainer ref=mouseRef onClick=toggleClick>     // ref + onClick
      <Transition in=isLoaded timeout=0 nodeRef=nodeRef>
        (state) => (                                               // state ?
          <div>
            <ItemCover
              src=
                props.coverType == 'wide' ? props.wideCover : props.posterCover
              
              alt=props.alt
              layout='responsive'
              width=
                props.coverType == 'wide'
                  ? coverSizes.wide.width
                  : coverSizes.poster.width
              
              height=
                props.coverType == 'wide'
                  ? coverSizes.wide.height + 1
                  : coverSizes.poster.height
               //+1: hack to avoid cut at the bottom of image
              placeholder="blur"
              blurDataURL=props.coverPlaceholder
              onLoadingComplete=handleLoadingComplete
            />
          </div>
        )
      </Transition>

      <ItemHoverContainer>
        <Transition
          in=isHovering
          timeout=0
          nodeRef=nodeRef
          mountOnEnter
          unmountOnExit
        >
          (state) => (
            <div>
              <ItemHoverImage
                src=
                  props.coverType == 'wide'
                    ? props.wideLoopVideo
                    : props.posterLoopVideo
                
                layout='responsive'
                width=
                  props.coverType == 'wide'
                    ? coverSizes.wide.width
                    : coverSizes.poster.width
                
                height=
                  props.coverType == 'wide'
                    ? coverSizes.wide.height
                    : coverSizes.poster.height + 1
                 //+1: hack to avoid a "phantom line" at the bottom of image
                state=state
              />
              <IconContainer>
                <Icon preserveAspectRatio="xMinYMin meet" name="coverPlay" />
              </IconContainer>
            </div>
          )
        </Transition>
      </ItemHoverContainer>

      <Transition in=props.isDetailed timeout=100 nodeRef=nodeRef>
        (state) => (
          <DetailsContainer state=state isDetailed=props.isDetailed>
            props.content.details
          </DetailsContainer>
        )
      </Transition>
    </ContentItemContainer>
  );


// styled components
const ContentItemContainer = styled.li`
  margin-bottom: 16px;
  text-decoration: none;
  transition: all 0.2s;
  position: relative;
  border-radius: $(props) => props.theme.radius.lg.value $gridUnit;
  overflow: hidden;
  height: auto;
  &:hover 
    cursor: pointer;
    transform: $(props) => (props.isClicking ? 'scale(0.98)' : 'scale(1.04)');
  
`;

const ItemCover = styled(Image)`
  border-radius: $(props) => props.theme.radius.lg.value $gridUnit;
  border: 1px solid #504f4e;
  overflow: visible;
  position: relative;
  transition: 0.2s;
  opacity: $( state ) => (state === 'entering' ? 0 : 1);
`;

const ItemHoverContainer = styled.div`
  position: absolute;
  z-index: 10;
  top: 0;
  left: 0;
  right: 0;
  padding: 0px;
  margin: 0px;
  height: auto;
  &:hover 
    border: 0.8px solid $(props) => props.theme.alias.image.border.value;
    border-radius: $(props) => props.theme.radius.lg.value $gridUnit;
  
`;

const ItemHoverImage = styled(Image)`
  border-radius: 15px; //15px not 16px: hack to avoid a "phantom line" at the bottom of image
  transition: 0.4s;
  display: $( state ) => (state === 'exited' ? 'none' : 'block');
  opacity: $( state ) => (state === 'entered' ? 1 : 0);
`;

const IconContainer = styled.div`
  position: absolute;
  left: 41.84%;
  right: 41.13%;
  top: 42.58%;
  bottom: 42.11%;
`;

const DetailsContainer = styled(Paragraph)`
  padding-top: $( state ) =>
    state === 'entered'
      ? (props) => props.theme.spacing[1].value + gridUnit
      : 0;
  transition: 0.4s;
  opacity: $( state ) => (state === 'entered' ? 1 : 0);
  height: $( state ) => (state === 'entered' ? 1 : 0);
  display: $( state ) => (state === 'exited' ? 'none' : 'block');
`;

(1) 你必须组织你的代码

导入顺序:

    反应 + 钩子 包 样式表 组件

页面底部的样式化组件

(2) 网上看解构道具

鼠标事件监听器与 useEffect 演示: Stacblitz

【讨论】:

谢谢@MB_!它非常有用并且工作正常,但内存泄漏错误仍在发生 + 2 个新警告:1)Error: The ref value 'mouseRef.current' will likely have changed by the time this effect cleanup function runs. 我刚刚在 useEffect 中创建了 currentRef=mouseRef.current 并工作了。 2)React Hook useEffect has an unnecessary dependency: 'mouseRef.current'我删除了依赖数组并开始工作。 Obs.:我更改了您关于切换的建议,因为该组件具有基于“按住鼠标”的行为,将您的逻辑复制到 mouseDown 和 Up 并且可以正常工作。 内存泄漏错误是由useEffect 引起的 => mouseRef.current 是一个必要的依赖项,通常这应该与useEffect 中的const mouseref = useRef (null);mouseref.current 一起正常工作(就像在我的演示中一样)。【参考方案2】:

基于@MB_ 逻辑,我在 useEffect 中添加了 setIsLoaded(false) 并且它起作用了 :)

useEffect(() => 

    const currentMouseRef = mouseRef.current;
    
    if (currentMouseRef) 
      currentMouseRef.addEventListener('mouseover', handleMouseOver);
      currentMouseRef.addEventListener('mouseout', handleMouseOut);
      currentMouseRef.addEventListener('mousedown', handleMouseDown);
      currentMouseRef.addEventListener('mouseup', handleMouseUp);

      return () => 
        currentMouseRef.removeEventListener('mouseover', handleMouseOver);
        currentMouseRef.removeEventListener('mouseout', handleMouseOut);
        currentMouseRef.removeEventListener('mousedown', handleMouseDown);
        currentMouseRef.removeEventListener('mouseup', handleMouseUp);
        setIsLoaded(false); //Added this here
      ;
    
  , []);

【讨论】:

以上是关于React:“无法在未安装的组件上执行 React 状态更新”,没有 useEffect 功能的主要内容,如果未能解决你的问题,请参考以下文章

有啥区别(从“react”导入React;)与(从“react”导入React;)[重复]

react插件网站,react插件社区,react中文论坛

「首席架构师推荐」React生态系统大集合

import * as react from 'react' 与 import react from 'react' 有啥区别

“使用 JSX 时,React 必须在范围内”(react/react-in-jsx-scope 与 index.js 上的“window.React = React”)

React 系列教程