草庐IT

javascript - 错误的 React Hook 行为与事件监听器

coder 2024-05-08 原文

我正在尝试使用 React Hooks,但遇到了一个问题。 当我尝试使用事件监听器处理的按钮来控制台记录它时,它显示了错误的状态。

代码沙箱: https://codesandbox.io/s/lrxw1wr97m

  1. 点击“添加卡”按钮 2 次
  2. 在第一张卡片中,点击 Button1 并在控制台中看到有 2 张卡片处于状态(正确行为)
  3. 在第一张卡片中,点击 Button2(由事件监听器处理)并在控制台中看到只有一张卡片处于状态(错误行为)

为什么会显示错误的状态?
在第一张卡片中,Button2应该显示 2控制台中的卡片。有什么想法吗?

const { useState, useContext, useRef, useEffect } = React;

const CardsContext = React.createContext();

const CardsProvider = props => {
  const [cards, setCards] = useState([]);

  const addCard = () => {
    const id = cards.length;
    setCards([...cards, { id: id, json: {} }]);
  };

  const handleCardClick = id => console.log(cards);
  const handleButtonClick = id => console.log(cards);

  return (
    <CardsContext.Provider
      value={{ cards, addCard, handleCardClick, handleButtonClick }}
    >
      {props.children}
    </CardsContext.Provider>
  );
};

function App() {
  const { cards, addCard, handleCardClick, handleButtonClick } = useContext(
    CardsContext
  );

  return (
    <div className="App">
      <button onClick={addCard}>Add card</button>
      {cards.map((card, index) => (
        <Card
          key={card.id}
          id={card.id}
          handleCardClick={() => handleCardClick(card.id)}
          handleButtonClick={() => handleButtonClick(card.id)}
        />
      ))}
    </div>
  );
}

function Card(props) {
  const ref = useRef();

  useEffect(() => {
    ref.current.addEventListener("click", props.handleCardClick);
    return () => {
      ref.current.removeEventListener("click", props.handleCardClick);
    };
  }, []);

  return (
    <div className="card">
      Card {props.id}
      <div>
        <button onClick={props.handleButtonClick}>Button1</button>
        <button ref={node => (ref.current = node)}>Button2</button>
      </div>
    </div>
  );
}

ReactDOM.render(
  <CardsProvider>
    <App />
  </CardsProvider>,
  document.getElementById("root")
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id='root'></div>

我正在使用 React 16.7.0-alpha.0 和 Chrome 70.0.3538.110

顺便说一句,如果我使用类重写 CardsProvider,问题就消失了。 CodeSandbox 使用类:https://codesandbox.io/s/w2nn3mq9vl

最佳答案

这是使用 useState Hook 的功能组件的常见问题。同样的问题适用于使用 useState 状态的任何回调函数,例如setTimeout or setInterval timer functions .

事件处理程序在 CardsProviderCard 组件中的处理方式不同。

CardsProvider 功能组件中使用的

handleCardClickhandleButtonClick 定义在其范围内。每次运行都有新的函数,它们指的是定义它们时获得的 cards 状态。每次呈现 CardsProvider 组件时都会重新注册事件处理程序。

Card 功能组件中使用的

handleCardClick 作为 prop 接收,并在组件安装时使用 useEffect 注册一次。它在整个组件生命周期中都是相同的函数,指的是在第一次定义 handleCardClick 函数时新鲜的陈旧状态。 handleButtonClick 作为 prop 接收并在每次 Card 渲染时重新注册,每次都是一个新函数并引用新状态。

可变状态

解决此问题的常用方法是使用 useRef 而不是 useState。 ref 基本上是一个配方,它提供了一个可以通过引用传递的可变对象:

const ref = useRef(0);

function eventListener() {
  ref.current++;
}

在这种情况下,应该像 useState 预期的那样在状态更新时重新呈现组件,refs 不适用。

可以单独保持状态更新和可变状态,但是 forceUpdate 在类和函数组件中都被认为是一种反模式(列出仅供引用):

const useForceUpdate = () => {
  const [, setState] = useState();
  return () => setState({});
}

const ref = useRef(0);
const forceUpdate = useForceUpdate();

function eventListener() {
  ref.current++;
  forceUpdate();
}

状态更新函数

一种解决方案是使用状态更新函数,它从封闭范围接收新鲜状态而不是陈旧状态:

function eventListener() {
  // doesn't matter how often the listener is registered
  setState(freshState => freshState + 1);
}

在这种情况下,同步副作用需要一个状态,例如 console.log,解决方法是返回相同的状态以防止更新。

function eventListener() {
  setState(freshState => {
    console.log(freshState);
    return freshState;
  });
}

useEffect(() => {
  // register eventListener once

  return () => {
    // unregister eventListener once
  };
}, []);

这不适用于异步副作用,特别是 async 函数。

手动事件监听器重新注册

另一种解决方案是每次都重新注册事件监听器,因此回调总是从封闭范围获取新鲜状态:

function eventListener() {
  console.log(state);
}

useEffect(() => {
  // register eventListener on each state update

  return () => {
    // unregister eventListener
  };
}, [state]);

内置事件处理

除非事件监听器注册在documentwindow或其他当前组件范围之外的事件目标上,否则React自己的DOM事件处理必须是尽可能使用,这消除了对 useEffect 的需要:

<button onClick={eventListener} />

在最后一种情况下,事件监听器可以使用 useMemouseCallback 额外内存,以防止在作为 prop 传递时不必要的重新渲染:

const eventListener = useCallback(() => {
  console.log(state);
}, [state]);
  • 此答案的上一版本建议使用适用于 React 16.7.0-alpha 版本中初始 useState Hook 实现但在最终 React 16.8 实现中不可用的可变状态。 useState 目前仅支持不可变状态。*

关于javascript - 错误的 React Hook 行为与事件监听器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53845595/

有关javascript - 错误的 React Hook 行为与事件监听器的更多相关文章

  1. ruby-on-rails - Rails 常用字符串(用于通知和错误信息等) - 2

    大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje

  2. ruby-on-rails - 迷你测试错误 : "NameError: uninitialized constant" - 2

    我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test

  3. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  4. ruby-on-rails - 如何在 Rails View 上显示错误消息? - 2

    我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c

  5. 使用 ACL 调用 upload_file 时出现 Ruby S3 "Access Denied"错误 - 2

    我正在尝试编写一个将文件上传到AWS并公开该文件的Ruby脚本。我做了以下事情:s3=Aws::S3::Resource.new(credentials:Aws::Credentials.new(KEY,SECRET),region:'us-west-2')obj=s3.bucket('stg-db').object('key')obj.upload_file(filename)这似乎工作正常,除了该文件不是公开可用的,而且我无法获得它的公共(public)URL。但是当我登录到S3时,我可以正常查看我的文件。为了使其公开可用,我将最后一行更改为obj.upload_file(file

  6. ruby-on-rails - 错误 : Error installing pg: ERROR: Failed to build gem native extension - 2

    我克隆了一个rails仓库,我现在正尝试捆绑安装背景:OSXElCapitanruby2.2.3p173(2015-08-18修订版51636)[x86_64-darwin15]rails-v在您的Gemfile中列出的或native可用的任何gem源中找不到gem'pg(>=0)ruby​​'。运行bundleinstall以安装缺少的gem。bundleinstallFetchinggemmetadatafromhttps://rubygems.org/............Fetchingversionmetadatafromhttps://rubygems.org/...Fe

  7. ruby - #之间? Cooper 的 *Beginning Ruby* 中的错误或异常 - 2

    在Cooper的书BeginningRuby中,第166页有一个我无法重现的示例。classSongincludeComparableattr_accessor:lengthdef(other)@lengthother.lengthenddefinitialize(song_name,length)@song_name=song_name@length=lengthendenda=Song.new('Rockaroundtheclock',143)b=Song.new('BohemianRhapsody',544)c=Song.new('MinuteWaltz',60)a.betwee

  8. ruby-on-rails - 每次我尝试部署时,我都会得到 - (gcloud.preview.app.deploy) 错误响应 : [4] DEADLINE_EXCEEDED - 2

    我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie

  9. ruby-on-rails - Rails 5 Active Record 记录无效错误 - 2

    我有两个Rails模型,即Invoice和Invoice_details。一个Invoice_details属于Invoice,一个Invoice有多个Invoice_details。我无法使用accepts_nested_attributes_forinInvoice通过Invoice模型保存Invoice_details。我收到以下错误:(0.2ms)BEGIN(0.2ms)ROLLBACKCompleted422UnprocessableEntityin25ms(ActiveRecord:4.0ms)ActiveRecord::RecordInvalid(Validationfa

  10. arrays - 这是 Ruby 中 Array.fill 方法的错误吗? - 2

    这个问题在这里已经有了答案:Arraysmisbehaving(1个回答)关闭6年前。是否应该这样,即我误解了,还是错误?a=Array.new(3,Array.new(3))a[1].fill('g')=>[["g","g","g"],["g","g","g"],["g","g","g"]]它不应该导致:=>[[nil,nil,nil],["g","g","g"],[nil,nil,nil]]

随机推荐