Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always call LayoutGroup effect destroy functions. #103

Merged
merged 1 commit into from
Feb 23, 2024

Conversation

smoores-dev
Copy link
Collaborator

@smoores-dev smoores-dev commented Feb 23, 2024

This resolves an issue where LayoutGroup effects were not cleaned up when the LayoutGroup itself was unmounted. React executes layout effect destroy functions from parent to child on unmount, which is opposite how it runs during normal render. The result was that the destroyQueue was never processed on unmount.

Now, if the LayoutGroup is being unmounted, we call destroy functions immediately, without queuing.

Note: This also fixes the Prettier config for VS Code, which got messed up when we upgraded to v3!

@smoores-dev smoores-dev requested a review from a team as a code owner February 23, 2024 16:54
@tilgovi
Copy link
Contributor

tilgovi commented Feb 23, 2024

Don't the deleted lines 57-60 destroy the layout effects?

@smoores-dev
Copy link
Collaborator Author

Yes, this now happens on line 43, instead (which is executed as a layout effect cleanup function in the child component)

@tilgovi
Copy link
Contributor

tilgovi commented Feb 23, 2024

I don't have a use case in mind where grouping destroys is actually important, but the asymmetry makes me uncomfortable. It feels more surprising somehow.

I think what's maybe important is that if a descendant of the group unmounts, it should trigger a flush of the group, because the other layout effects of descendants that continue to exist might need to update. Consider a scenario where some other component in the tree has a layout group effect with no dependencies. We want to force the whole group to rerender so that re-runs after the unmount of another component, no?

@smoores-dev
Copy link
Collaborator Author

If I'm understanding correctly, we were already trying to do what you're describing with line 46. It's not sufficient, because the parent has already unmounted by the time the trigger to flush runs, and so it does not re-run its cleanup effect.

I agree that this is inconsistent, but:

  • I actually don't think it's surprising; the behavior is now that the create effect runs after the view create effect, and the destroy effect runs before it. This actually is what I had been assuming was happening before I looked into this, and I think it makes some intuitive sense
  • React itself is inconsistent in exactly this way, so I think we get a pass! Haha

@smoores-dev
Copy link
Collaborator Author

There is another approach where we keep the destroyQueue as is, and special-case the scenario where the LayoutGroup is unmounted. This would allow us to actually be more consistent than React is, and always run child effect destroy functions after the LayoutGroup's. Does that seem better?

@smoores-dev
Copy link
Collaborator Author

Just added a second commit to demonstrate the alternative approach!

@tilgovi
Copy link
Contributor

tilgovi commented Feb 23, 2024

There is another approach where we keep the destroyQueue as is, and special-case the scenario where the LayoutGroup is unmounted. This would allow us to actually be more consistent than React is, and always run child effect destroy functions after the LayoutGroup's. Does that seem better?

I was just about to suggest something like this, but let me take a bit to digest.

@@ -18,6 +18,7 @@ export interface LayoutGroupProps {
export function LayoutGroup({ children }: LayoutGroupProps) {
const createQueue = useRef(new Set<() => void>()).current;
const destroyQueue = useRef(new Set<() => void>()).current;
const isMounted = useRef(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually true, initially? 😆

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meeeaaannnn... I guess it depends on your definition of mounted haha. Given the way that this is being used, I suppose it's safe to set this to false initially and set it true in a useLayoutEffect creator.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. If somehow a child were to mount and unmount, due to synchronous updates forced by state changes done in layout effects further down the tree, we might want that child to destroy its effect? But I guess it wouldn't actually have created its effect yet. Huh. I dunno.

This is so unfortunately complicated to reason about!

It might be that I just got this wrong, initially. I maybe assumed that cleanup was child->parent, in the same way that effect creation is. But it might be parent->child, even during regular updates?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If so, I'm tempted by the idea of not queueing child destroys, but providing a simpler explanation for why.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not, I've checked. It is child->parent in regular updates, and actually behaves differently and runs parent->child in during unmounts!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true even if both the parent and the child have effects with dependencies that changed and the render was caused by a state update at the parent or above?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was testing with effects with no dependencies because it seemed easier to coordinate. But yes, if you use effects with no dependencies, and update the state at the parent, effect cleanups are run from child -> parent. If you unmount the parent, they are run from parent -> child

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's super wild.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For anyone that wants to follow along, here's a minimal reproduction on CodeSandbox.

If you open the console, you'll see that the order of the print statements changes depending on whether you click Update or Toggle mount!

Copy link
Contributor

@tilgovi tilgovi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do this!

  • Split this into two PRs, one for the regression and one for the fix to layout group effects.
  • Only set isMounted once we're actually mounted.
  • Maybe you can drop the comment around why we destroy without a queue. That'd cause me to scratch my head when simply seeing that we're not mounted is enough for me to know why can't just flush again.

@smoores-dev
Copy link
Collaborator Author

Done!

@smoores-dev
Copy link
Collaborator Author

smoores-dev commented Feb 23, 2024

Oh, wait, maybe I should have checked; were you saying we should go ahead with approach 1 (where we never queue) or 2 (where we always queue unless we're unmounting)?

OH. Drop the comment. Hm, yeah ok, I'm fine with that I suppose.

This resolves an issue where LayoutGroup effects were not cleaned up
when the LayoutGroup itself was unmounted. React executes layout effect
destroy functions from parent to child on unmount, which is opposite
how it runs during normal render. The result was that the destroyQueue
was never processed on unmount.

Now, if the LayoutGroup is being unmounted, we call destroy functions
immediately, without queuing.
@smoores-dev
Copy link
Collaborator Author

Ok now done!

@tilgovi
Copy link
Contributor

tilgovi commented Feb 23, 2024

👏🏻

@smoores-dev smoores-dev merged commit af54cf4 into main Feb 23, 2024
2 checks passed
@smoores-dev smoores-dev deleted the fix-layout-group-destroy branch February 23, 2024 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants