React hooks 函数组件防止在状态更新时重新渲染

React hooks function component prevent re-render on state update

我正在学习 React Hooks,这意味着我将不得不从 classes 转移到函数组件。以前在 classes 中,我可以有 class 独立于状态的变量,我可以在不重新渲染组件的情况下更新这些变量。现在我正在尝试将组件重新创建为带有钩子的函数组件,我遇到了 运行 我无法(据我所知)为该函数创建变量的问题,因此唯一的存储方式数据通过 useState 挂钩。然而,这意味着只要该状态更新,我的组件就会重新呈现。

我在下面的示例中对此进行了说明,我试图将 class 组件重新创建为使用挂钩的函数组件。我想在有人点击它时为 div 设置动画,但如果用户在它已经设置动画时点击它,则阻止再次调用动画。

class ClassExample extends React.Component {
  _isAnimating = false;
  _blockRef = null;
  
  onBlockRef = (ref) => {
    if (ref) {
      this._blockRef = ref;
    }
  }
  
  // Animate the block.
  onClick = () => {
    if (this._isAnimating) {
      return;
    }

    this._isAnimating = true;
    Velocity(this._blockRef, {
      translateX: 500,
      complete: () => {
        Velocity(this._blockRef, {
          translateX: 0,
          complete: () => {
            this._isAnimating = false;
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  };
  
  render() {
    console.log("Rendering ClassExample");
    return(
      <div>
        <div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
      </div>
    );
  }
}

const FunctionExample = (props) => {
  console.log("Rendering FunctionExample");
  
  const [ isAnimating, setIsAnimating ] = React.useState(false);
  const blockRef = React.useRef(null);
  
  // Animate the block.
  const onClick = React.useCallback(() => {
    if (isAnimating) {
      return;
    }

    setIsAnimating(true);
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
          complete: () => {
            setIsAnimating(false);
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  });
  
  return(
    <div>
      <div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
    </div>
  );
};

ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

如果您单击 ClassExample 栏(粉红色),您将看到它在设置动画时不会重新呈现,但是如果您单击 FunctionExample 栏(红色),它会在设置动画时重新呈现两次。这是因为我使用 setIsAnimating 导致重新渲染。我知道这可能不是很能提高性能,但如果功能组件可能的话,我想阻止它。有没有suggestions/am我做错了什么?

更新(尝试修复,尚无解决方案): 下面用户 lecstor 建议可以将 useState 的结果更改为 let 而不是 const,然后直接设置它 let [isAnimating] = React.useState(false);。不幸的是,这也不起作用,正如您在下面的代码片段中看到的那样。单击红色条将启动其动画,单击 o运行ge 方块将使组件重新渲染,如果再次单击红色条,它将打印 isAnimating 已重置为false 即使栏仍在动画。

const FunctionExample = () => {
  console.log("Rendering FunctionExample");

  // let isAnimating = false; // no good if component rerenders during animation
  
  // abuse useState var instead?
  let [isAnimating] = React.useState(false);
  
  // Var to force a re-render.
  const [ forceCount, forceUpdate ] = React.useState(0);

  const blockRef = React.useRef(null);

  // Animate the block.
  const onClick = React.useCallback(() => {
    console.log("Is animating: ", isAnimating);
    if (isAnimating) {
      return;
    }
    
    isAnimating = true;
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
          complete: () => {
            isAnimating = false;
          }
        }, {
          duration: 5000
        });
      }
    }, {
      duration: 5000
    });
  });

  return (
  <div>
    <div
      id = 'block'
      onClick = {onClick}
      ref = {blockRef}
      style = {
        {
          width: '100px',
          height: '10px',
          backgroundColor: 'red'
        }
      }
      >
      {}
    </div>
    <div onClick={() => forceUpdate(forceCount + 1)} 
      style = {
        {
          width: '100px',
          height: '100px',
          marginTop: '12px',
          backgroundColor: 'orange'
        }
      }/>
  </div>
  );
};

ReactDOM.render( < div > < FunctionExample / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

更新2(解决方案): 如果你想在函数组件中有一个变量,但不想让它在更新时重新渲染组件,你可以使用 useRef 而不是 useStateuseRef 不仅仅可以用于 dom 元素,实际上建议用于实例变量。 参见:https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables

为什么您认为不能像使用 classes 一样使用内部变量?

好的,感觉有点脏但是改变 useState 状态怎么样? 8)

不,这不符合预期 状态在重新渲染时重置

所以这与组件的实际渲染无关,也许我们的逻辑需要基于动画本身。这个特殊问题可以通过检查 class 动画时元素上的速度设置来解决。

const FunctionExample = ({ count }) => {
  console.log("Rendering FunctionExample", count);

  // let isAnimating = false; // no good if component rerenders during animation
  
  // abuse useState var instead?
  // let [isAnimating] = React.useState(false);
  
  const blockRef = React.useRef(null);

  // Animate the block.
  const onClick = React.useCallback(() => {
    // use feature of the anim itself
    if (/velocity-animating/.test(blockRef.current.className)) {
      return;
    }
    console.log("animation triggered");
    
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
        }, {
          duration: 1000
        });
      }
    }, {
      duration: 5000
    });
  });

  return (
  <div>
    <div
      id = 'block'
      onClick = {onClick}
      ref = {blockRef}
      style = {
        {
          width: '100px',
          height: '10px',
          backgroundColor: 'red'
        }
      }
      >
      {}
    </div>
  </div>
  );
};

const Counter = () => {
  const [count, setCount] = React.useState(0);
  return <div>
    <FunctionExample count={count} />
    <button onClick={() => setCount(c => c + 1)}>Count</button>
  </div>;
}

ReactDOM.render( < div > < Counter / > < /div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

使用 ref 在函数调用之间保留值而不触发渲染

class ClassExample extends React.Component {
  _isAnimating = false;
  _blockRef = null;
  
  onBlockRef = (ref) => {
    if (ref) {
      this._blockRef = ref;
    }
  }
  
  // Animate the block.
  onClick = () => {
    if (this._isAnimating) {
      return;
    }

    this._isAnimating = true;
    Velocity(this._blockRef, {
      translateX: 500,
      complete: () => {
        Velocity(this._blockRef, {
          translateX: 0,
          complete: () => {
            this._isAnimating = false;
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  };
  
  render() {
    console.log("Rendering ClassExample");
    return(
      <div>
        <div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
      </div>
    );
  }
}

const FunctionExample = (props) => {
  console.log("Rendering FunctionExample");
  
  const isAnimating = React.useRef(false)
  const blockRef = React.useRef(null);
  
  // Animate the block.
  const onClick = React.useCallback(() => {
    if (isAnimating.current) {
      return;
    }

    isAnimating.current = true
    
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
          complete: () => {
            isAnimating.current = false
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  });
  
  return(
    <div>
      <div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
    </div>
  );
};

ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>