Skip to content

Commit eb0edcd

Browse files
bubachoDevrocks Digital
authored andcommitted
refactor: improve idle task scheduling safety in render-if-visible
1 parent d048ed2 commit eb0edcd

3 files changed

Lines changed: 47 additions & 15 deletions

File tree

apps/web/core/components/core/render-if-visible-HOC.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import type { ReactNode, MutableRefObject } from "react";
88
import React, { useState, useRef, useEffect } from "react";
99
import { cn } from "@plane/utils";
10+
import { runIdleTask } from "@/lib/idle-task";
1011

1112
type Props = {
1213
defaultHeight?: string;
@@ -19,18 +20,10 @@ type Props = {
1920
placeholderChildren?: ReactNode;
2021
defaultValue?: boolean;
2122
shouldRecordHeights?: boolean;
22-
useIdletime?: boolean;
23+
useIdleTime?: boolean;
2324
forceRender?: boolean;
2425
};
2526

26-
const runIdleTask = (callback: () => void) => {
27-
if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
28-
window.requestIdleCallback(callback, { timeout: 300 });
29-
return;
30-
}
31-
globalThis.setTimeout(callback, 0);
32-
};
33-
3427
function RenderIfVisible(props: Props) {
3528
const {
3629
defaultHeight = "300px",
@@ -44,12 +37,14 @@ function RenderIfVisible(props: Props) {
4437
//placeholder children
4538
placeholderChildren = null, //placeholder children
4639
defaultValue = false,
47-
useIdletime = false,
40+
useIdleTime = false,
4841
forceRender = false,
4942
} = props;
5043
const [shouldVisible, setShouldVisible] = useState<boolean>(defaultValue);
5144
const placeholderHeight = useRef<string>(defaultHeight);
5245
const intersectionRef = useRef<HTMLElement | null>(null);
46+
const visibilityIdleTaskRef = useRef<ReturnType<typeof runIdleTask> | null>(null);
47+
const heightIdleTaskRef = useRef<ReturnType<typeof runIdleTask> | null>(null);
5348

5449
const isVisible = shouldVisible || forceRender;
5550

@@ -60,8 +55,11 @@ function RenderIfVisible(props: Props) {
6055
const observer = new IntersectionObserver(
6156
(entries) => {
6257
//DO no remove comments for future
63-
if (typeof window !== "undefined" && useIdletime) {
64-
runIdleTask(() => setShouldVisible(entries[entries.length - 1].isIntersecting));
58+
if (typeof window !== "undefined" && useIdleTime) {
59+
visibilityIdleTaskRef.current?.cancel();
60+
visibilityIdleTaskRef.current = runIdleTask(() =>
61+
setShouldVisible(entries[entries.length - 1].isIntersecting)
62+
);
6563
} else {
6664
setShouldVisible(entries[entries.length - 1].isIntersecting);
6765
}
@@ -73,18 +71,25 @@ function RenderIfVisible(props: Props) {
7371
);
7472
observer.observe(target);
7573
return () => {
74+
visibilityIdleTaskRef.current?.cancel();
75+
visibilityIdleTaskRef.current = null;
7676
observer.unobserve(target);
7777
};
7878
}
79-
}, [intersectionRef, root, verticalOffset, horizontalOffset, useIdletime]);
79+
}, [intersectionRef, root, verticalOffset, horizontalOffset, useIdleTime]);
8080

8181
//Set height after render
8282
useEffect(() => {
8383
if (intersectionRef.current && isVisible && shouldRecordHeights) {
84-
runIdleTask(() => {
84+
heightIdleTaskRef.current?.cancel();
85+
heightIdleTaskRef.current = runIdleTask(() => {
8586
if (intersectionRef.current) placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`;
8687
});
8788
}
89+
return () => {
90+
heightIdleTaskRef.current?.cancel();
91+
heightIdleTaskRef.current = null;
92+
};
8893
}, [isVisible, intersectionRef, shouldRecordHeights]);
8994

9095
const child = isVisible ? <>{children}</> : placeholderChildren;

apps/web/core/components/issues/issue-layouts/kanban/default.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
204204
/>
205205
}
206206
defaultValue={groupIndex < 5 && subGroupIndex < 2}
207-
useIdletime
207+
useIdleTime
208208
>
209209
<KanbanGroup
210210
groupId={subList.id}

apps/web/core/lib/idle-task.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright (c) 2023-present Plane Software, Inc. and contributors
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
* See the LICENSE file for details.
5+
*/
6+
7+
export type IdleTaskHandle = {
8+
cancel: () => void;
9+
};
10+
11+
/**
12+
* Schedule lightweight work for idle time and return a cancel handle.
13+
* Falls back to setTimeout when requestIdleCallback is unavailable.
14+
*/
15+
export const runIdleTask = (callback: () => void): IdleTaskHandle => {
16+
if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
17+
const idleId = window.requestIdleCallback(callback, { timeout: 300 });
18+
return {
19+
cancel: () => window.cancelIdleCallback(idleId),
20+
};
21+
}
22+
23+
const timeoutId = window.setTimeout(callback, 0);
24+
return {
25+
cancel: () => window.clearTimeout(timeoutId),
26+
};
27+
};

0 commit comments

Comments
 (0)