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

Fix flaky logout #185

Merged
merged 11 commits into from Jun 20, 2023
Merged

Fix flaky logout #185

merged 11 commits into from Jun 20, 2023

Conversation

andrecasal
Copy link
Contributor

@andrecasal andrecasal commented Jun 15, 2023

submit(e.currentTarget) pushes the form's submission task to the macrotask queue in the event loop to process later. The click event bubbles up to <DropdownMenu.Portal> which will remove itself from the DOM by pushing the removal task to the macrotask queue.

The first task in the queue, the form's submission, is sent to the call stack and processed. This results in an asynchronous network request the browser asks the OS to make and await for and becomes associated with that DOM element.

While the browser waits for the OS to handle that network request, the second task in the macrotask queue, the removal of <DropdownMenu.Portal> and its <form> child, is sent to the call stack and processed.

This creates a race condition:

  • If the network request is sent first, the logout will succeed (even if the response is not received/ignored).
  • If the removal of <form> from the DOM (and the subsequent network request cancelation associated with it) succeeds first, the network request is canceled and the logout might fail, depending on whether the request has already been sent or not.

I think 😄

Fix
Detach the form submission from its DOM element:

<DropdownMenu.Item asChild>
	<Form
		action="/logout"
		method="POST"
		className="rounded-b-3xl px-7 py-5 outline-none radix-highlighted:bg-muted-500"
		onClick={e => {
			e.preventDefault()
			submit(null, { action: '/logout', method: 'POST' })
		}}
	>
		<button type="submit">Logout</button>
	</Form>
</DropdownMenu.Item>

Result

  • No more canceled network requests seen in Playwright tests
  • No more Form submission canceled because the form is not connected console warnings

Screenshots
Network tab:
Screenshot 2023-06-15 at 10 05 40
Console:
Screenshot 2023-06-15 at 10 05 52

@andrecasal
Copy link
Contributor Author

andrecasal commented Jun 15, 2023

Just noticed the logout POST request is still aborted. This only happens on Playwright tests.

//...
await page.getByRole('link', { name: user.name ?? user.username }).click()
await page.getByRole('menuitem', { name: /logout/i }).click()
await expect(page).toHaveURL(`/`) // 👈 This doesn't help

await page.goto('/login')
await expect(page).toHaveURL(`/login`)
//...

Screenshot 2023-06-15 at 11 02 35

Edit: I'll open an issue just for test-related stuff.

@andrecasal andrecasal mentioned this pull request Jun 15, 2023
app/root.tsx Outdated
className="rounded-b-3xl px-7 py-5 outline-none radix-highlighted:bg-night-500"
onClick={e => submit(e.currentTarget)}
onClick={e => {
e.preventDefault()
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason this is not the only line changed in the PR? Why can't we keep the action and method and use submit(e.currentTarget)?

Copy link
Member

Choose a reason for hiding this comment

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

Whoops, never mind. I know why. I wonder if we could change this to e.stopPropagation to prevent the menu from closing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me try that out

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how the Radix UI internals work, but my suspicion is that, because the form takes up all of the <DropdownMenu.Item asChild>'s behavior, having an onClick on it is already too late, because it'll send the close comand to the <DropdownMenu.Portal>.

Adding onClick={e => e.stopPropagation()} to the button works 👌

@andrecasal
Copy link
Contributor Author

Should be ok now 👌

✅ Detached from DOM, so no console warning
✅ Keyboard accessible
✅ Moved padding to button

@kentcdodds
Copy link
Member

I believe I've solved it another way that I'm happier with. Can you try it out and let me know if this fixes it.

I also added a logout button to the profile page because otherwise you can't logout if JS doesn't load.

@kentcdodds kentcdodds merged commit ba56bf2 into epicweb-dev:main Jun 20, 2023
5 checks passed
@kentcdodds
Copy link
Member

Oh, I missed something: a61c713

@andrecasal
Copy link
Contributor Author

Seems pretty solid 👌

@kentcdodds
Copy link
Member

Awesome!

@kentcdodds
Copy link
Member

Thank you for your help

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