fix: call logoutAction directly via useTransition instead of form-ref submit

De form-ref-dance werkte niet betrouwbaar in de huidige base-ui:
- onSelect vuurde requestSubmit() op een hidden form
- Form zat eerst binnen DropdownMenuContent (form geunmount → ref null)
- Form daarna naar top-level verplaatst — vuurde nog steeds geen request af,
  vermoedelijk doordat onSelect in deze base-ui-build niet (consistent) een
  click-event genereerde dat de form-API trigger'de

Vervang door directe call: Server Actions kunnen sinds Next.js 14 als async
functie worden aangeroepen vanuit Client Components. useTransition voorkomt
dat de UI bevriest tijdens de redirect.

Naast onSelect ook onClick als veiligheid voor het geval base-ui later weer
van event-prop wisselt — beide handlers wijzen naar dezelfde idempotente
function (handleLogout via startTransition).

Pendingstate ('Uitloggen…' label, disabled item) zodat dubbele klikken niet
dubbele logoutAction-calls afvuren.

Quality gates: lint 0 errors, tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-27 23:40:42 +02:00
parent 4f9a6d2d9e
commit 5cbf543c16

View file

@ -1,6 +1,6 @@
'use client' 'use client'
import { useRef } from 'react' import { useTransition } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { Settings, Sun, Globe, LogOut } from 'lucide-react' import { Settings, Sun, Globe, LogOut } from 'lucide-react'
import { logoutAction } from '@/actions/auth' import { logoutAction } from '@/actions/auth'
@ -33,14 +33,19 @@ export function UserMenu({ userId, username, email, roles }: UserMenuProps) {
const initials = username.slice(0, 2).toUpperCase() const initials = username.slice(0, 2).toUpperCase()
const roleLabels = roles.map((r) => ROLE_LABELS[r]).filter(Boolean) const roleLabels = roles.map((r) => ROLE_LABELS[r]).filter(Boolean)
const subtitle = email?.trim() ? email.trim() : 'Lokaal account' const subtitle = email?.trim() ? email.trim() : 'Lokaal account'
const logoutFormRef = useRef<HTMLFormElement>(null) const [pendingLogout, startLogout] = useTransition()
// Server Action direct aanroepen — geen form/ref-dance. Eerdere implementatie
// gebruikte een hidden form binnen DropdownMenuContent; die unmount op
// onSelect en in deze base-ui-versie kwam de submit niet door.
function handleLogout() {
startLogout(async () => {
await logoutAction()
})
}
return ( return (
<> <DropdownMenu>
{/* Form buiten DropdownMenuContent die unmount op onSelect waardoor de ref
null wordt voordat requestSubmit() vuurt. */}
<form ref={logoutFormRef} action={logoutAction} className="hidden" />
<DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
className="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background" className="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
aria-label="Accountmenu openen" aria-label="Accountmenu openen"
@ -107,14 +112,15 @@ export function UserMenu({ userId, username, email, roles }: UserMenuProps) {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onSelect={() => logoutFormRef.current?.requestSubmit()} onClick={handleLogout}
onSelect={handleLogout}
disabled={pendingLogout}
className="cursor-pointer" className="cursor-pointer"
> >
<LogOut className="mr-2 h-4 w-4" /> <LogOut className="mr-2 h-4 w-4" />
<span>Uitloggen</span> <span>{pendingLogout ? 'Uitloggen…' : 'Uitloggen'}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</>
) )
} }