Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions frontend/src/app/components/app-ready-effect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client'

import { useEffect } from 'react'

const markReady = (element: HTMLElement) => {
element.setAttribute('data-ready', 'true')
}

const resetReady = (element: HTMLElement) => {
element.setAttribute('data-ready', 'false')
}

export default function AppReadyEffect() {
useEffect(() => {
const appRoot = document.getElementById('app')

if (!appRoot) {
return
}

let frame = requestAnimationFrame(() => {
frame = requestAnimationFrame(() => {
markReady(appRoot)
})
})

return () => {
cancelAnimationFrame(frame)
resetReady(appRoot)
}
Comment on lines +21 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix frame cancellation to handle nested requestAnimationFrame.

The current cleanup logic has a subtle race condition. When frame is reassigned on line 22, the outer requestAnimationFrame ID is lost. If the component unmounts between the first and second frame callback, only the second frame will be canceled, leaving the first callback to execute and potentially call markReady on an unmounted component.

Apply this diff to track both frame IDs separately:

-    let frame = requestAnimationFrame(() => {
-      frame = requestAnimationFrame(() => {
+    let frame1: number | undefined
+    let frame2: number | undefined
+    
+    frame1 = requestAnimationFrame(() => {
+      frame2 = requestAnimationFrame(() => {
         markReady(appRoot)
       })
     })
 
     return () => {
-      cancelAnimationFrame(frame)
+      if (frame1 !== undefined) cancelAnimationFrame(frame1)
+      if (frame2 !== undefined) cancelAnimationFrame(frame2)
       resetReady(appRoot)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let frame = requestAnimationFrame(() => {
frame = requestAnimationFrame(() => {
markReady(appRoot)
})
})
return () => {
cancelAnimationFrame(frame)
resetReady(appRoot)
}
let frame1: number | undefined
let frame2: number | undefined
frame1 = requestAnimationFrame(() => {
frame2 = requestAnimationFrame(() => {
markReady(appRoot)
})
})
return () => {
if (frame1 !== undefined) cancelAnimationFrame(frame1)
if (frame2 !== undefined) cancelAnimationFrame(frame2)
resetReady(appRoot)
}
🤖 Prompt for AI Agents
In frontend/src/app/components/app-ready-effect.tsx around lines 21 to 30, the
cleanup cancels only the last requestAnimationFrame ID because the outer frame
ID is overwritten; store the outer and inner frame IDs in separate variables
(e.g., outerFrame and innerFrame), assign outerFrame from the first
requestAnimationFrame and innerFrame from the nested one, and in the return
cleanup cancel both cancelAnimationFrame(outerFrame) and
cancelAnimationFrame(innerFrame) and still call resetReady(appRoot).

}, [])

return null
}
Comment on lines 13 to 34
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider simplifying to single requestAnimationFrame.

The double requestAnimationFrame pattern defers execution until after the next browser paint. While this ensures all DOM updates are complete, a single requestAnimationFrame is typically sufficient for marking an element as ready after render. The double-rAF pattern is more commonly used when you need to read layout properties that must reflect applied CSS changes.

If you confirm that single rAF is sufficient, apply this diff:

-    let frame = requestAnimationFrame(() => {
-      frame = requestAnimationFrame(() => {
-        markReady(appRoot)
-      })
+    let frame = requestAnimationFrame(() => {
+      markReady(appRoot)
     })

However, if you need to guarantee that all layout effects and paints have completed before marking ready (e.g., for third-party tools or analytics), the double-rAF is appropriate and can remain as-is.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function AppReadyEffect() {
useEffect(() => {
const appRoot = document.getElementById('app')
if (!appRoot) {
return
}
let frame = requestAnimationFrame(() => {
frame = requestAnimationFrame(() => {
markReady(appRoot)
})
})
return () => {
cancelAnimationFrame(frame)
resetReady(appRoot)
}
}, [])
return null
}
export default function AppReadyEffect() {
useEffect(() => {
const appRoot = document.getElementById('app')
if (!appRoot) {
return
}
let frame = requestAnimationFrame(() => {
markReady(appRoot)
})
return () => {
cancelAnimationFrame(frame)
resetReady(appRoot)
}
}, [])
return null
}
🤖 Prompt for AI Agents
In frontend/src/app/components/app-ready-effect.tsx around lines 13 to 34 the
component uses a double requestAnimationFrame to defer markReady, which can be
simplified: replace the nested requestAnimationFrame calls with a single
requestAnimationFrame that calls markReady(appRoot), store that frame id in the
same variable, and keep the existing cleanup to cancelAnimationFrame(frame) and
call resetReady(appRoot); ensure you still bail out if appRoot is missing and
that frame is defined before canceling.

6 changes: 5 additions & 1 deletion frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { Providers } from './providers'
import AppReadyEffect from './components/app-ready-effect'

const inter = Inter({ subsets: ['latin'] })

Expand All @@ -22,7 +23,10 @@ export default function RootLayout({
<html lang="en">
<body className={inter.className}>
<Providers>
{children}
<div id="app" data-ready="false">
{children}
<AppReadyEffect />
</div>
Comment on lines 25 to 29

Choose a reason for hiding this comment

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

P1 Badge Keep data-ready attribute in sync across re-renders

The wrapper sets data-ready="false" in JSX while AppReadyEffect flips it to true only once inside a useEffect([]). When Providers re-renders (for example on any client-side navigation or provider state change), React will reapply this JSX and set the attribute back to false, but the effect will not run again to mark it ready. Any CSS or scripts that rely on data-ready will see the app stuck in a non-ready state after the first navigation. Consider deriving the attribute from React state or rerunning the effect on updates so the flag remains accurate.

Useful? React with 👍 / 👎.

</Providers>
</body>
</html>
Expand Down
Loading