Skip to content

fix: use polyfilled requestIdleCallback in render-if-visible HOC#8937

Open
jamartineztelecoengineer84-dotcom wants to merge 2 commits intomakeplane:previewfrom
jamartineztelecoengineer84-dotcom:fix/safari-render-if-visible-polyfill
Open

fix: use polyfilled requestIdleCallback in render-if-visible HOC#8937
jamartineztelecoengineer84-dotcom wants to merge 2 commits intomakeplane:previewfrom
jamartineztelecoengineer84-dotcom:fix/safari-render-if-visible-polyfill

Conversation

@jamartineztelecoengineer84-dotcom
Copy link
Copy Markdown

@jamartineztelecoengineer84-dotcom jamartineztelecoengineer84-dotcom commented Apr 27, 2026

Description

When opening any project view in Safari, the entire UI crashes with:

TypeError: window.requestIdleCallback is not a function — gantt-layout-loader-Bameferh.js:1:5524

The polyfill added in #5689 only takes effect when apps/web/app/provider.tsx
finishes evaluating. However, lazy-loaded chunks (like gantt-layout-loader,
which RenderIfVisible is part of) can execute before the provider tree
finishes hydrating. Their direct calls to window.requestIdleCallback then
crash on Safari, which is the only major browser without native support
for this API (caniuse).

This PR fixes the issue by adding two defensive wrappers
(safeRequestIdleCallback, safeCancelIdleCallback) to the polyfills module
and using them at the only remaining unguarded call site:
render-if-visible-HOC.tsx.

Changes

  1. apps/web/core/lib/polyfills/index.ts — adds wrapper exports while
    preserving the existing side-effect for backward compatibility with any
    code that still depends on window.requestIdleCallback being
    monkey-patched.

  2. apps/web/core/components/core/render-if-visible-HOC.tsx — replaces
    both window.requestIdleCallback call sites with the new wrapper. Also
    cleans up a small typo at line 54 where the original guard
    typeof window !== undefined was missing the quotes around "undefined"
    (effectively a no-op check, always truthy).

Note on behavior change

At the line-54 call site, removing the && window.requestIdleCallback
short-circuit changes Safari behavior from "fall through to synchronous"
to "async via setTimeout fallback". This matches the intent of
useIdletime: true (defer the work) and is consistent with the side-effect
polyfill already applied in app/provider.tsx for the rest of the tree.

Incidental hardening

While editing the HOC, two preexisting react-hooks warnings became
blocking under --deny-warnings. Both are legitimate fixes that align
with making RenderIfVisible more robust:

  • Capture intersectionRef.current in a local variable inside the
    useEffect so the cleanup function uses the captured node rather
    than re-reading the (possibly mutated) ref.
  • Add the missing useIdletime to the useEffect dependency array
    so the observer is reconfigured when the prop changes (it was
    silently sticking to the first render's value).

The previous // eslint-disable-next-line react-hooks/exhaustive-deps
directive is removed since the underlying warning is now resolved.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Screenshots and Media (if applicable)

Before — Safari opening any project view:

TypeError: window.requestIdleCallback is not a function
   at gantt-layout-loader-Bameferh.js:1:5524
   ↳ "Looks like something went wrong" error boundary
   ↳ React minified errors #418 (hydration mismatch)
   ↳ React minified errors #423 (hydration recovery)

After: Safari renders all views (List, Kanban, Spreadsheet, Gantt) without crashing.

Test Scenarios

  • Safari (macOS): open /{workspace}/projects/{project_id}/issues/ → kanban renders, all views switchable, no console errors related to requestIdleCallback.
  • Chrome / Firefox / Edge: no regression — window.requestIdleCallback is used natively (the wrapper short-circuits to window.requestIdleCallback(cb, options)).
  • Bundle output verified — gantt-layout-loader-*.js no longer contains unguarded window.requestIdleCallback(...) calls.
  • pnpm check:types --filter=web... — passes (21/21 tasks).
  • pnpm check:lint --filter=web — 0 errors. The fix reduces the preexisting warnings on the modified file from 4 to 2.
  • pnpm check:format --filter=web — passes.
  • pnpm build --filter=web... — passes (11/11 tasks).

No new tests added; the affected HOC has no existing test coverage in
this repo, and PR #5689 (which originally added the polyfill) followed the
same precedent. Adding test infrastructure for this HOC would be a useful
follow-up.

References

Closes #8904
Closes #8871
Related to #5689 (the original polyfill PR — this completes its intent for lazy-loaded chunks)

Summary by CodeRabbit

  • Bug Fixes
    • Improved compatibility of visibility detection across browsers to reduce missed or delayed content rendering.
    • More robust deferred measurement and task scheduling with better cleanup to prevent occasional layout glitches and stray background tasks.

The polyfill from makeplane#5689 is only applied as a side-effect when
app/provider.tsx is evaluated. Lazy-loaded chunks like
gantt-layout-loader can execute before the provider tree finishes
hydrating, causing window.requestIdleCallback to be undefined on
Safari (the only major browser without native support) and crashing
the entire UI with a TypeError.

Add safeRequestIdleCallback / safeCancelIdleCallback wrappers to
apps/web/core/lib/polyfills and use them at the call sites in
render-if-visible-HOC.tsx, making the call defensive at use rather
than relying on global side-effect ordering. The existing side-effect
is preserved for backward compatibility.

Also cleans up a typo in the original guard at line 54
(typeof window !== undefined → effectively always truthy).

Closes makeplane#8904
Closes makeplane#8871
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 27, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1fc506a-fbd3-4ab6-9b27-428946af7053

📥 Commits

Reviewing files that changed from the base of the PR and between 485586d and 35d3e46.

📒 Files selected for processing (1)
  • apps/web/core/components/core/render-if-visible-HOC.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/core/components/core/render-if-visible-HOC.tsx

📝 Walkthrough

Walkthrough

Replaces direct usage of window.requestIdleCallback/cancelIdleCallback with exported safeRequestIdleCallback/safeCancelIdleCallback fallbacks, and updates the render-if-visible HOC to use these safe wrappers and adjust intersection observer idle scheduling and cleanup.

Changes

Cohort / File(s) Summary
Polyfill Utilities
apps/web/core/lib/polyfills/index.ts
Adds safeRequestIdleCallback and safeCancelIdleCallback exports that delegate to native requestIdleCallback/cancelIdleCallback when present, otherwise use setTimeout/clearTimeout fallbacks. Removes prior empty export.
Render-If-Visible Component
apps/web/core/components/core/render-if-visible-HOC.tsx
Replaces direct window.requestIdleCallback/cancelIdleCallback calls with the safe wrappers; schedules/cancels idle callbacks in the IntersectionObserver callback; defers post-render placeholder height measurement via safeRequestIdleCallback; includes useIdletime in effect deps and ensures cleanup of cached node and pending idle callback.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 I peeked at heights and idle time,

where Safari hid a missing chime.
I wrapped the calls with gentle care,
now views wake up and run everywhere. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: using polyfilled requestIdleCallback in the render-if-visible HOC to fix Safari crashes.
Description check ✅ Passed The PR description is comprehensive, covering the problem, solution, changes made, behavior changes, testing scenarios, and references to linked issues. It follows the expected format with clear sections.
Linked Issues check ✅ Passed The PR addresses all requirements from linked issues #8904 and #8871: it prevents Safari crashes by guarding requestIdleCallback calls with wrappers, ensuring lazy-loaded chunks don't call unpolyfilled APIs, and allowing Kanban/List/Spreadsheet/Gantt views to render without TypeError in Safari.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the Safari requestIdleCallback crash: two files modified (polyfills wrapper exports and HOC usage), plus incidental hardening (ref capture, dependency array fix) that aligns with the main objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/core/components/core/render-if-visible-HOC.tsx (1)

77-83: ⚠️ Potential issue | 🟡 Minor

Same handle-leak concern on the height-measurement effect.

The deferred height measurement scheduled via safeRequestIdleCallback is never cancelled when the effect re-runs or the component unmounts. The body is guarded by intersectionRef.current so it won't NPE, but on Safari this now goes through the setTimeout fallback (per the PR's behavior note), which means a stale callback can still fire on a detached element and write to placeholderHeight.current after unmount. Capturing the returned handle and calling safeCancelIdleCallback in the cleanup would make this bulletproof.

Also, intersectionRef is a stable ref object; including it in the dep array has no effect — you can drop it for clarity.

🛠️ Suggested fix
   useEffect(() => {
     if (intersectionRef.current && isVisible && shouldRecordHeights) {
-      safeRequestIdleCallback(() => {
+      const handle = safeRequestIdleCallback(() => {
         if (intersectionRef.current) placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`;
       });
+      return () => safeCancelIdleCallback(handle);
     }
-  }, [isVisible, intersectionRef, shouldRecordHeights]);
+  }, [isVisible, shouldRecordHeights]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/components/core/render-if-visible-HOC.tsx` around lines 77 -
83, The useEffect that measures height (the effect referencing intersectionRef,
isVisible, shouldRecordHeights, placeholderHeight and calling
safeRequestIdleCallback) can schedule a deferred callback that is never
cancelled; capture the handle returned by safeRequestIdleCallback, return a
cleanup that calls safeCancelIdleCallback(handle) to cancel it on
re-run/unmount, and remove the stable intersectionRef from the dependency array
(leave isVisible and shouldRecordHeights) for clarity.
🧹 Nitpick comments (1)
apps/web/core/lib/polyfills/index.ts (1)

30-57: Optional: deduplicate the setTimeout fallback shared with the side-effect polyfill.

The fallback body in safeRequestIdleCallback (lines 41–48) is essentially identical to the one assigned to window.requestIdleCallback at lines 9–21, and the same applies to safeCancelIdleCallback vs. lines 23–27. Extracting a single internal helper (e.g. idleCallbackFallback / cancelIdleCallbackFallback) and reusing it in both the side-effect assignment and the safe wrappers would avoid drift if one branch is ever tweaked (e.g., timeout value, timeRemaining logic).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/core/lib/polyfills/index.ts` around lines 30 - 57, The fallback
implementations for requestIdleCallback and cancelIdleCallback are duplicated
between the side-effect polyfill (the
window.requestIdleCallback/window.cancelIdleCallback assignments) and the safe
wrappers safeRequestIdleCallback and safeCancelIdleCallback; extract the shared
logic into a single internal helper pair (e.g., idleCallbackFallback and
cancelIdleCallbackFallback) and have both the side-effect polyfill and the safe*
wrappers call those helpers so the timeout value and timeRemaining logic are
defined once and reused across requestIdleCallback and cancelIdleCallback code
paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/core/components/core/render-if-visible-HOC.tsx`:
- Around line 56-59: The effect schedules idle callbacks with
safeRequestIdleCallback but never stores or cancels the returned handle; update
the IntersectionObserver callback (inside render-if-visible-HOC) to capture the
handle returned by safeRequestIdleCallback into a local variable (e.g.,
idleHandle) and before scheduling a new idle callback call
safeCancelIdleCallback(idleHandle), and also cancel any pending idleHandle in
the effect cleanup; ensure you reference safeRequestIdleCallback,
safeCancelIdleCallback, setShouldVisible, and the observer callback when making
these changes so pending callbacks are always cleared on re-run/unmount.

---

Outside diff comments:
In `@apps/web/core/components/core/render-if-visible-HOC.tsx`:
- Around line 77-83: The useEffect that measures height (the effect referencing
intersectionRef, isVisible, shouldRecordHeights, placeholderHeight and calling
safeRequestIdleCallback) can schedule a deferred callback that is never
cancelled; capture the handle returned by safeRequestIdleCallback, return a
cleanup that calls safeCancelIdleCallback(handle) to cancel it on
re-run/unmount, and remove the stable intersectionRef from the dependency array
(leave isVisible and shouldRecordHeights) for clarity.

---

Nitpick comments:
In `@apps/web/core/lib/polyfills/index.ts`:
- Around line 30-57: The fallback implementations for requestIdleCallback and
cancelIdleCallback are duplicated between the side-effect polyfill (the
window.requestIdleCallback/window.cancelIdleCallback assignments) and the safe
wrappers safeRequestIdleCallback and safeCancelIdleCallback; extract the shared
logic into a single internal helper pair (e.g., idleCallbackFallback and
cancelIdleCallbackFallback) and have both the side-effect polyfill and the safe*
wrappers call those helpers so the timeout value and timeRemaining logic are
defined once and reused across requestIdleCallback and cancelIdleCallback code
paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 636ea034-2382-4a20-845d-812c4e76ac0a

📥 Commits

Reviewing files that changed from the base of the PR and between 32fb88a and 485586d.

📒 Files selected for processing (2)
  • apps/web/core/components/core/render-if-visible-HOC.tsx
  • apps/web/core/lib/polyfills/index.ts

Comment thread apps/web/core/components/core/render-if-visible-HOC.tsx
Address review feedback from coderabbitai: the handle returned by
safeRequestIdleCallback was discarded, allowing setShouldVisible to
fire on unmounted components or after the effect's deps changed.
Capture the handle, cancel any pending callback before scheduling a
new one, and cancel on cleanup.
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.

[bug]: WebKit-based browser Kanban view crash. [bug]: Board view occurs error, which then prevents me from accessing Issues at all (Safari only)

2 participants