我正在尝试使用 React Hooks,但遇到了一个问题。 当我尝试使用事件监听器处理的按钮来控制台记录它时,它显示了错误的状态。
代码沙箱: https://codesandbox.io/s/lrxw1wr97m
为什么会显示错误的状态?
在第一张卡片中,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 .
事件处理程序在 CardsProvider 和 Card 组件中的处理方式不同。
CardsProvider 功能组件中使用的handleCardClick 和handleButtonClick 定义在其范围内。每次运行都有新的函数,它们指的是定义它们时获得的 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]);
除非事件监听器注册在document、window或其他当前组件范围之外的事件目标上,否则React自己的DOM事件处理必须是尽可能使用,这消除了对 useEffect 的需要:
<button onClick={eventListener} />
在最后一种情况下,事件监听器可以使用 useMemo 或 useCallback 额外内存,以防止在作为 prop 传递时不必要的重新渲染:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
useState Hook 实现但在最终 React 16.8 实现中不可用的可变状态。 useState 目前仅支持不可变状态。*关于javascript - 错误的 React Hook 行为与事件监听器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53845595/
大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje
我遵循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
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c
我正在尝试编写一个将文件上传到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
我克隆了一个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
在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
我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie
我有两个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
这个问题在这里已经有了答案: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]]