-
Notifications
You must be signed in to change notification settings - Fork 141
setState locking throws confusing errors from event listeners #56
Comments
Good write-up! I don't think I like many of these solutions, even the asynchronous one. If a render is being caused by a render (a cascading render), that's most likely a bug. Even once asynchronous rendering lands, we should catch these cases and warn, and recommend that users wrap changed events in a It might be possible to special-case both |
So the recommended solution here is wrapping change events that call |
Roblox needs some sort of |
I think we need to update the It's been waaaay too confusing for everyone that hits it. |
It's possible this was partly fixed by #67. It doesn't quite solve the crux of the issue ( |
I wonder if we could suspend event listener invocation during rendering, at least for Roact-related events. That would be possible to do in |
I think that could be a really good idea to at least explore. It would prevent many mistakes. I'll start up a new issue for it. |
The functionality that throws if
setState
is used in lifecycle hooks is, in its present implementation, causing some issues with certain event listeners. Sometimes, property change listeners can be invoked synchronously during the render - if they callsetState
within the synchronous part of the handler, they will throw, because the component is still rendering.Change events are as synchronous as possible
Change events are executed synchronously where possible. The code will execute in the current context up until it yields; the code after the yield will be executed later. As an example, this code (in a Frame):
prints this output:
Crucially, note that the first
Immediate
is printed betweenBefore
andAfter
.Why this is an issue in Roact
The current
setState
locking code is very, very simplistic. Each stateful component stores a boolean that determines whether setting state is allowed at the moment. This flag is set tofalse
whenever the component starts re-rendering, and reset totrue
when the rendering is over.The problem comes into play when you have a change event that fires synchronously, as in the scenario detailed above, and you call
setState
inside that change handler. As an example, this code will throw immediately after being run:This is the error thrown:
What happens is the event is connected, then the position is immediately changed. This fires the event, which executes synchronously during the render process. The event listener then calls
setState
, throwing an error, as designed.Possible resolutions
There are a couple possible ways to fix this in Roact alone, though users of refs will still suffer from this problem in all of them:
spawn
all event listeners in a new thread, delaying them by a frame and forcing them to wait until the rendering is completedspawn
listeners in a new threadThis is the worst of the options overall. It will immediately resolve the problem, but it also prevents Roblox from re-using event threads. It also yields, which is messy.
Suspend listener invocation
This just turns off events completely while the component is rendering. This has obvious implications for data loss.
Defer listener invocation
This postpones event listener invocation until after rendering is completed, when
setState
can be safely called again. This has a bunch of possible pitfalls, however. The largest one on my mind is: What happens if the information in the event is stale by the time it's re-rendered? For example, if we fire an event, which will passrbx
to the listener, and then do something to make thatrbx
reference invalid, how do we resolve that?Asynchronous rendering
This would fix the issue by disconnecting
setState
from rendering - callingsetState
with asynchronous rendering would not lead to a synchronous render, it would lead to a render at some point in the future. CallingsetState
within the render process (as would happen from a synchronously-invoked change listener) would cause another render to happen at some point in the future.See also
setState
in the wrong places #17: Original issue that prompted this behavior to be introducedThe text was updated successfully, but these errors were encountered: