49 lines
1.6 KiB
TypeScript
49 lines
1.6 KiB
TypeScript
import { type ReactNode, Suspense, useState, useLayoutEffect } from 'react'
|
|
import { Navigate, useLocation } from 'react-router-dom'
|
|
import { useAuth } from '../contexts/AuthContext'
|
|
import { PageLoader } from './PageLoader'
|
|
|
|
// Mounts only once Suspense has resolved (chunk is ready).
|
|
// useLayoutEffect fires before the browser paints, so setState here causes a
|
|
// synchronous re-render — content becomes visible in the same paint with no
|
|
// intermediate loader flash for cached chunks.
|
|
function ContentReady({ onReady }: { onReady: () => void }) {
|
|
useLayoutEffect(() => {
|
|
onReady()
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
return null
|
|
}
|
|
|
|
type Props = { children: ReactNode }
|
|
|
|
export function ProtectedRoute({ children }: Props) {
|
|
const { user, loading } = useAuth()
|
|
const location = useLocation()
|
|
|
|
// Always start false so the loader covers any intermediate render state.
|
|
// For cached chunks, ContentReady's useLayoutEffect fires before the first
|
|
// paint and flips this synchronously — no visible flash.
|
|
const [contentReady, setContentReady] = useState(false)
|
|
|
|
if (!loading && !user) {
|
|
return <Navigate to="/" state={{ from: location }} replace />
|
|
}
|
|
|
|
const showLoader = loading || !contentReady
|
|
|
|
return (
|
|
<>
|
|
{showLoader && <PageLoader />}
|
|
{!loading && user && (
|
|
<div style={{ display: contentReady ? 'contents' : 'none' }}>
|
|
<Suspense fallback={null}>
|
|
<ContentReady onReady={() => setContentReady(true)} />
|
|
{children}
|
|
</Suspense>
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
}
|