-
Notifications
You must be signed in to change notification settings - Fork 141
Fix updateChildren re-rentrancy issue #301
Fix updateChildren re-rentrancy issue #301
Conversation
Hmm this is a pretty interesting one! I think we're hitting a confluence of different things that our existing guarantees aren't accounting for, namely the fact that event suspension isn't happening on I don't know if the fix as written really addresses the right part of the issue; maybe it makes the most sense to track when the reconciler is updating, and defer any new updates (presumably caused by On the other hand, we might also consider just warning folks that listening to changes in the hierarchy and responding synchronously is dangerous. I suspect that wrapping the body of the |
I'd be curious to know what @oltrep thinks of this! |
Yeah, maybe more broad event suspension would be a better fix for this. The current fix is way more fragile to future changes even if we add a test for this specific issue. I don't think we can simply warn folks that listening to changes in the hierarchy and responding synchronously is dangerous because this would mean that many places we respond to AbsoluteContentSize and AbsoluteSize changes would have to be wrapped to spawns. The example shown here is a very basic example so I used DescendantAdded but this is also possible to achieve with components that do bottom up sizing and UI-system events. |
I was referring to specifically changes in the hierarchy, not just derived properties in general. This doesn't happen with things like |
That's not true, this bug in LoadableGridView happens because it listens to a AbsoluteContentSize change. Event suspension does not prevent it because the events are not suspended in the parent component at the time due to the spawn. What happens is:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This need to be covered with a test, which may help illuminate the discussion thread on the PR as well. Thanks for taking the time to contribute!
@matthargett Yeah, I wasn't quite sure how to add a test for this because of Lemur not mocking ChildAdded in a way where it is actually ever fired and Lemur being read-only so I can't go there to try to commit a fix for that. I did write a test for this which I included in the description. To actually be able to run it as part of the Roact tests I'd probably need to fork Lemur and add a comprehensive mock for DescendantAdded. Wanted to get feedback on the approach first before going to this trouble. |
Was totally wrong about why the test didn't work, yield across metamethod/C-call boundary was being caused by using wait, not something to do with the ChildAdded. I changed it to use a coroutine instead of spawn and got rid of the wait. The test works correctly now. |
Changed to an alternative solution based on suspending parent events when setState is called outside of the components lifecyle. Let me know what you think of this! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had one thing that looked like an omission; however, some surface level mucking with the logic didn't change anything, so it may not be a gap that gets hit in practice
@@ -101,6 +101,7 @@ local function createReconciler(renderer) | |||
-- mountVirtualNode can return nil if the element is a boolean | |||
if childNode ~= nil then | |||
childNode.depth = virtualNode.depth + 1 | |||
childNode.parent = virtualNode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible that we we want to to add this in replaceVirtualNode
too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think we do. Since we also set depth there that makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we update the ChangeLog for the release, we should mention that this fix is behind a config flag.
In certain circumstances it is possible for updateChildren in createReconicler to be re-rendered for a virtualNode while it is already happening causing two Roblox instances to be created for the same element.
For an example of how this happens, see the added unit test.
What happens in the test is:
This example is kind of a contrived, but this did happen in the UIBlox GridView but with UI events such as AbsoluteContentSize Changed instead of DescentantAdded.
Solution
Suspend the events of parent nodes when updating a component outside of the standard lifecycle. Suspending the parent events means that no change we make can cause a parent node to reconcile while we are also reconciling.