diff --git a/apps/web/app/provider.tsx b/apps/web/app/provider.tsx index be2ed2e9e..233b4250d 100644 --- a/apps/web/app/provider.tsx +++ b/apps/web/app/provider.tsx @@ -1,4 +1,3 @@ -import type { FC, ReactNode } from "react"; import { lazy, Suspense } from "react"; import { useTheme, ThemeProvider } from "next-themes"; import { SWRConfig } from "swr"; @@ -31,7 +30,7 @@ const IntercomProvider = lazy(function IntercomProvider() { }); export interface IAppProvider { - children: ReactNode; + children: React.ReactNode; } function ToastWithTheme() { diff --git a/apps/web/ce/components/home/peek-overviews.tsx b/apps/web/ce/components/home/peek-overviews.tsx index 9ee24b799..74d2d7d5a 100644 --- a/apps/web/ce/components/home/peek-overviews.tsx +++ b/apps/web/ce/components/home/peek-overviews.tsx @@ -1,9 +1,9 @@ +import { observer } from "mobx-react"; import { IssuePeekOverview } from "@/components/issues/peek-overview"; +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; -export function HomePeekOverviewsRoot() { - return ( - <> - - - ); -} +export const HomePeekOverviewsRoot = observer(function HomePeekOverviewsRoot() { + const { peekIssue } = useIssueDetail(); + + return peekIssue ? : null; +}); diff --git a/apps/web/core/components/core/content-overflow-HOC.tsx b/apps/web/core/components/core/content-overflow-HOC.tsx index 02725f73f..7463c2ae3 100644 --- a/apps/web/core/components/core/content-overflow-HOC.tsx +++ b/apps/web/core/components/core/content-overflow-HOC.tsx @@ -77,19 +77,20 @@ export const ContentOverflowWrapper = observer(function ContentOverflowWrapper(p resizeObserver.disconnect(); mutationObserver.disconnect(); }; - }, [contentRef?.current]); + }, []); useEffect(() => { - if (!containerRef.current) return; + const container = containerRef.current; + if (!container) return; const handleTransitionEnd = () => { setIsTransitioning(false); }; - containerRef.current.addEventListener("transitionend", handleTransitionEnd); + container.addEventListener("transitionend", handleTransitionEnd); return () => { - containerRef.current?.removeEventListener("transitionend", handleTransitionEnd); + container.removeEventListener("transitionend", handleTransitionEnd); }; }, []); diff --git a/apps/web/core/components/home/widgets/links/root.tsx b/apps/web/core/components/home/widgets/links/root.tsx index f05700b0b..39706b445 100644 --- a/apps/web/core/components/home/widgets/links/root.tsx +++ b/apps/web/core/components/home/widgets/links/root.tsx @@ -20,7 +20,7 @@ export const DashboardQuickLinks = observer(function DashboardQuickLinks(props: const handleCreateLinkModal = useCallback(() => { toggleLinkModal(true); setLinkData(undefined); - }, []); + }, [toggleLinkModal, setLinkData]); useSWR( workspaceSlug ? `HOME_LINKS_${workspaceSlug}` : null, diff --git a/apps/web/core/components/power-k/global-shortcuts.tsx b/apps/web/core/components/power-k/global-shortcuts.tsx index f0bda6776..606d6d705 100644 --- a/apps/web/core/components/power-k/global-shortcuts.tsx +++ b/apps/web/core/components/power-k/global-shortcuts.tsx @@ -1,9 +1,8 @@ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // hooks import { usePowerK } from "@/hooks/store/use-power-k"; -import { useAppRouter } from "@/hooks/use-app-router"; // local imports import { detectContextFromURL } from "./core/context-detector"; import { ShortcutHandler } from "./core/shortcut-handler"; @@ -22,7 +21,6 @@ type GlobalShortcutsProps = { export const GlobalShortcutsProvider = observer(function GlobalShortcutsProvider(props: GlobalShortcutsProps) { const { context, commands } = props; // router - const router = useAppRouter(); const params = useParams(); // store hooks const { commandRegistry, isShortcutsListModalOpen, setActiveContext, togglePowerKModal, toggleShortcutsListModal } = @@ -40,21 +38,40 @@ export const GlobalShortcutsProvider = observer(function GlobalShortcutsProvider commandRegistry.registerMultiple(commands); }, [commandRegistry, commands]); - // Setup global shortcut handler + // Store context in ref to avoid recreation on context changes + const contextRef = useRef(context); useEffect(() => { - const handler = new ShortcutHandler( + contextRef.current = context; + }, [context]); + + // Store handler in ref to avoid recreation on context changes + const handlerRef = useRef(null); + + // Setup global shortcut handler - only recreate when commandRegistry or togglePowerKModal changes + useEffect(() => { + // Clean up previous handler if it exists + if (handlerRef.current) { + document.removeEventListener("keydown", handlerRef.current.handleKeyDown); + handlerRef.current.destroy(); + } + + // Create new handler with function that reads from ref + handlerRef.current = new ShortcutHandler( commandRegistry, - () => context, + () => contextRef.current, () => togglePowerKModal(true) ); - document.addEventListener("keydown", handler.handleKeyDown); + document.addEventListener("keydown", handlerRef.current.handleKeyDown); return () => { - document.removeEventListener("keydown", handler.handleKeyDown); - handler.destroy(); + if (handlerRef.current) { + document.removeEventListener("keydown", handlerRef.current.handleKeyDown); + handlerRef.current.destroy(); + handlerRef.current = null; + } }; - }, [context, router, commandRegistry, togglePowerKModal]); + }, [commandRegistry, togglePowerKModal]); return toggleShortcutsListModal(false)} />; }); diff --git a/apps/web/core/hooks/use-current-time.tsx b/apps/web/core/hooks/use-current-time.tsx index fc37129c6..4f361cea8 100644 --- a/apps/web/core/hooks/use-current-time.tsx +++ b/apps/web/core/hooks/use-current-time.tsx @@ -2,11 +2,11 @@ import { useEffect, useState } from "react"; export const useCurrentTime = () => { const [currentTime, setCurrentTime] = useState(new Date()); - // update the current time every second + // update the current time every minute (60000ms) useEffect(() => { const intervalId = setInterval(() => { setCurrentTime(new Date()); - }, 1000); + }, 60000); return () => clearInterval(intervalId); }, []); diff --git a/apps/web/core/hooks/use-extended-sidebar-overview-outside-click.tsx b/apps/web/core/hooks/use-extended-sidebar-overview-outside-click.tsx index cb654eacf..ebe873f6c 100644 --- a/apps/web/core/hooks/use-extended-sidebar-overview-outside-click.tsx +++ b/apps/web/core/hooks/use-extended-sidebar-overview-outside-click.tsx @@ -1,42 +1,44 @@ import type React from "react"; -import { useEffect } from "react"; +import { useEffect, useCallback } from "react"; const useExtendedSidebarOutsideClickDetector = ( ref: React.RefObject, callback: () => void, targetId: string ) => { - const handleClick = (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) { - // check for the closest element with attribute name data-prevent-outside-click - const preventOutsideClickElement = (event.target as HTMLElement | undefined)?.closest( - "[data-prevent-outside-click]" - ); - // if the closest element with attribute name data-prevent-outside-click is found, return - if (preventOutsideClickElement) { - return; - } - // check if the click target is the current issue element or its children - let targetElement = event.target as HTMLElement | null; - while (targetElement) { - if (targetElement.id === targetId) { - // if the click target is the current issue element, return + const handleClick = useCallback( + (event: MouseEvent) => { + if (!(event.target instanceof HTMLElement)) return; + if (ref.current && !ref.current.contains(event.target)) { + // check for the closest element with attribute name data-prevent-outside-click + const preventOutsideClickElement = event.target.closest("[data-prevent-outside-click]"); + // if the closest element with attribute name data-prevent-outside-click is found, return + if (preventOutsideClickElement) { return; } - targetElement = targetElement.parentElement; + // check if the click target is the current issue element or its children + let targetElement: HTMLElement | null = event.target; + while (targetElement) { + if (targetElement.id === targetId) { + // if the click target is the current issue element, return + return; + } + targetElement = targetElement.parentElement; + } + const delayOutsideClickElement = event.target.closest("[data-delay-outside-click]"); + if (delayOutsideClickElement) { + // if the click target is the closest element with attribute name data-delay-outside-click, delay the callback + setTimeout(() => { + callback(); + }, 0); + return; + } + // else, call the callback immediately + callback(); } - const delayOutsideClickElement = (event.target as HTMLElement | undefined)?.closest("[data-delay-outside-click]"); - if (delayOutsideClickElement) { - // if the click target is the closest element with attribute name data-delay-outside-click, delay the callback - setTimeout(() => { - callback(); - }, 0); - return; - } - // else, call the callback immediately - callback(); - } - }; + }, + [ref, callback, targetId] + ); useEffect(() => { document.addEventListener("mousedown", handleClick); @@ -44,7 +46,7 @@ const useExtendedSidebarOutsideClickDetector = ( return () => { document.removeEventListener("mousedown", handleClick); }; - }, []); + }, [handleClick]); }; export default useExtendedSidebarOutsideClickDetector; diff --git a/apps/web/core/hooks/use-peek-overview-outside-click.tsx b/apps/web/core/hooks/use-peek-overview-outside-click.tsx index 02908ca11..1183a1c12 100644 --- a/apps/web/core/hooks/use-peek-overview-outside-click.tsx +++ b/apps/web/core/hooks/use-peek-overview-outside-click.tsx @@ -1,42 +1,44 @@ import type React from "react"; -import { useEffect } from "react"; +import { useEffect, useCallback } from "react"; const usePeekOverviewOutsideClickDetector = ( ref: React.RefObject, callback: () => void, issueId: string ) => { - const handleClick = (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) { - // check for the closest element with attribute name data-prevent-outside-click - const preventOutsideClickElement = (event.target as HTMLElement | undefined)?.closest( - "[data-prevent-outside-click]" - ); - // if the closest element with attribute name data-prevent-outside-click is found, return - if (preventOutsideClickElement) { - return; - } - // check if the click target is the current issue element or its children - let targetElement = event.target as HTMLElement | null; - while (targetElement) { - if (targetElement.id === `issue-${issueId}`) { - // if the click target is the current issue element, return + const handleClick = useCallback( + (event: MouseEvent) => { + if (!(event.target instanceof HTMLElement)) return; + if (ref.current && !ref.current.contains(event.target)) { + // check for the closest element with attribute name data-prevent-outside-click + const preventOutsideClickElement = event.target.closest("[data-prevent-outside-click]"); + // if the closest element with attribute name data-prevent-outside-click is found, return + if (preventOutsideClickElement) { return; } - targetElement = targetElement.parentElement; + // check if the click target is the current issue element or its children + let targetElement: HTMLElement | null = event.target; + while (targetElement) { + if (targetElement.id === `issue-${issueId}`) { + // if the click target is the current issue element, return + return; + } + targetElement = targetElement.parentElement; + } + const delayOutsideClickElement = event.target.closest("[data-delay-outside-click]"); + if (delayOutsideClickElement) { + // if the click target is the closest element with attribute name data-delay-outside-click, delay the callback + setTimeout(() => { + callback(); + }, 0); + return; + } + // else, call the callback immediately + callback(); } - const delayOutsideClickElement = (event.target as HTMLElement | undefined)?.closest("[data-delay-outside-click]"); - if (delayOutsideClickElement) { - // if the click target is the closest element with attribute name data-delay-outside-click, delay the callback - setTimeout(() => { - callback(); - }, 0); - return; - } - // else, call the callback immediately - callback(); - } - }; + }, + [ref, callback, issueId] + ); useEffect(() => { document.addEventListener("mousedown", handleClick); @@ -44,7 +46,7 @@ const usePeekOverviewOutsideClickDetector = ( return () => { document.removeEventListener("mousedown", handleClick); }; - }); + }, [handleClick]); }; export default usePeekOverviewOutsideClickDetector; diff --git a/apps/web/core/lib/intercom-provider.tsx b/apps/web/core/lib/intercom-provider.tsx index c589e0bc2..b9757711b 100644 --- a/apps/web/core/lib/intercom-provider.tsx +++ b/apps/web/core/lib/intercom-provider.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Intercom, show, hide, onHide } from "@intercom/messenger-js-sdk"; import { observer } from "mobx-react"; // store hooks @@ -16,27 +16,38 @@ const IntercomProvider = observer(function IntercomProvider(props: IntercomProvi const { data: user } = useUser(); const { config } = useInstance(); const { isIntercomToggle, toggleIntercom } = useTransient(); + // refs + const isInitializedRef = useRef(false); + // states + const [hydrated, setHydrated] = useState(false); + // derived values + const isIntercomEnabled = user && config && config.is_intercom_enabled && config.intercom_app_id; useEffect(() => { + if (!hydrated) return; if (isIntercomToggle) show(); else hide(); - }, [isIntercomToggle]); - - onHide(() => { - toggleIntercom(false); - }); + }, [hydrated, isIntercomToggle]); useEffect(() => { - if (user && config?.is_intercom_enabled && config.intercom_app_id) { - Intercom({ - app_id: config.intercom_app_id || "", - user_id: user.id, - name: `${user.first_name} ${user.last_name}`, - email: user.email, - hide_default_launcher: true, - }); - } - }, [user, config, toggleIntercom]); + if (!hydrated) return; + onHide(() => { + toggleIntercom(false); + }); + }, [hydrated, toggleIntercom]); + + useEffect(() => { + if (!isIntercomEnabled || isInitializedRef.current) return; // prevent multiple initializations + Intercom({ + app_id: config.intercom_app_id || "", + user_id: user.id, + name: `${user.first_name} ${user.last_name}`, + email: user.email, + hide_default_launcher: true, + }); + isInitializedRef.current = true; + setHydrated(true); + }, [isIntercomEnabled, config, user]); return <>{children}; }); diff --git a/apps/web/core/lib/posthog-provider.tsx b/apps/web/core/lib/posthog-provider.tsx index 5a76fa0eb..cb3d691a9 100644 --- a/apps/web/core/lib/posthog-provider.tsx +++ b/apps/web/core/lib/posthog-provider.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { lazy, Suspense, useEffect, useState } from "react"; +import { lazy, Suspense, useEffect, useCallback, useRef, useState } from "react"; import { PostHogProvider as PHProvider } from "@posthog/react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; @@ -29,6 +29,8 @@ const PostHogProvider = observer(function PostHogProvider(props: IPosthogWrapper const { instance } = useInstance(); const { workspaceSlug, projectId } = useParams(); const { getWorkspaceRoleByWorkspaceSlug, getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions(); + // refs + const isInitializedRef = useRef(false); // states const [hydrated, setHydrated] = useState(false); // derived values @@ -61,10 +63,11 @@ const PostHogProvider = observer(function PostHogProvider(props: IPosthogWrapper }, [user, currentProjectRole, currentWorkspaceRole, currentWorkspace, hydrated]); useEffect(() => { + if (isInitializedRef.current) return; // prevent multiple initializations const posthogKey = process.env.VITE_POSTHOG_KEY; const posthogHost = process.env.VITE_POSTHOG_HOST; const isDebugMode = process.env.VITE_POSTHOG_DEBUG === "1"; - if (posthogKey && posthogHost) { + if (posthogKey && posthogHost && !posthog.__loaded) { posthog.init(posthogKey, { api_host: posthogHost, ui_host: posthogHost, @@ -74,31 +77,32 @@ const PostHogProvider = observer(function PostHogProvider(props: IPosthogWrapper capture_pageleave: true, disable_session_recording: true, }); + isInitializedRef.current = true; setHydrated(true); } }, []); - useEffect(() => { - const clickHandler = (event: MouseEvent) => { - const target = event.target as HTMLElement; - // Use closest to find the nearest parent element with data-ph-element attribute - const elementWithAttribute = target.closest("[data-ph-element]") as HTMLElement; - if (elementWithAttribute) { - const element = elementWithAttribute.getAttribute("data-ph-element"); - if (element) { - captureClick({ elementName: element }); - } + const clickHandler = useCallback((event: MouseEvent) => { + const target = event.target as HTMLElement; + // Use closest to find the nearest parent element with data-ph-element attribute + const elementWithAttribute = target.closest("[data-ph-element]") as HTMLElement; + if (elementWithAttribute) { + const element = elementWithAttribute.getAttribute("data-ph-element"); + if (element) { + captureClick({ elementName: element }); } - }; - - if (is_posthog_enabled && hydrated) { - document.addEventListener("click", clickHandler); } + }, []); + + useEffect(() => { + if (!is_posthog_enabled || !hydrated) return; + + document.addEventListener("click", clickHandler); return () => { document.removeEventListener("click", clickHandler); }; - }, [hydrated, is_posthog_enabled]); + }, [hydrated, is_posthog_enabled, clickHandler]); if (is_posthog_enabled && hydrated) return (