From 6285a59508134d35a235c8ba56676b5aad770c6f Mon Sep 17 00:00:00 2001 From: yyh Date: Sat, 10 Jan 2026 12:51:39 +0800 Subject: [PATCH] refactor(web): extract isServer/isClient utility for consistent environment detection Centralize server/client environment detection by introducing a dedicated utility file instead of repeating `typeof window === 'undefined'` checks across the codebase. This improves code maintainability and consistency across 8 files with 15 occurrences. Closes #30802 --- web/app/components/apps/list.tsx | 3 ++- .../block-selector/featured-tools.tsx | 7 ++--- .../block-selector/featured-triggers.tsx | 7 ++--- .../rag-tool-recommendations/index.tsx | 7 ++--- .../hooks/use-trigger-events-limit-modal.ts | 3 ++- web/context/query-client.tsx | 3 ++- web/hooks/use-query-params.ts | 3 ++- web/utils/client.ts | 26 +++++++++++++++++++ web/utils/gtag.ts | 4 ++- 9 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 web/utils/client.ts diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index e5c9954626..095ed3f696 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -29,6 +29,7 @@ import { CheckModal } from '@/hooks/use-pay' import { useInfiniteAppList } from '@/service/use-apps' import { AppModeEnum } from '@/types/app' import { cn } from '@/utils/classnames' +import { isServer } from '@/utils/client' import AppCard from './app-card' import { AppCardSkeleton } from './app-card-skeleton' import Empty from './empty' @@ -71,7 +72,7 @@ const List = () => { // 1) Normalize legacy/incorrect query params like ?mode=discover -> ?category=all useEffect(() => { // avoid running on server - if (typeof window === 'undefined') + if (isServer) return const mode = searchParams.get('mode') if (!mode) diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index f2795e7ec0..12a1c59e0e 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -13,6 +13,7 @@ import Tooltip from '@/app/components/base/tooltip' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' +import { isServer } from '@/utils/client' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' import BlockIcon from '../block-icon' @@ -49,14 +50,14 @@ const FeaturedTools = ({ const language = useGetLanguage() const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [isCollapsed, setIsCollapsed] = useState(() => { - if (typeof window === 'undefined') + if (isServer) return false const stored = window.localStorage.getItem(STORAGE_KEY) return stored === 'true' }) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return const stored = window.localStorage.getItem(STORAGE_KEY) if (stored !== null) @@ -64,7 +65,7 @@ const FeaturedTools = ({ }, []) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 9ae6181a4f..01cb5d100f 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -12,6 +12,7 @@ import Tooltip from '@/app/components/base/tooltip' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' +import { isServer } from '@/utils/client' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' import BlockIcon from '../block-icon' @@ -42,14 +43,14 @@ const FeaturedTriggers = ({ const language = useGetLanguage() const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [isCollapsed, setIsCollapsed] = useState(() => { - if (typeof window === 'undefined') + if (isServer) return false const stored = window.localStorage.getItem(STORAGE_KEY) return stored === 'true' }) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return const stored = window.localStorage.getItem(STORAGE_KEY) if (stored !== null) @@ -57,7 +58,7 @@ const FeaturedTriggers = ({ }, []) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx index 3c62f488dc..e934f27fd1 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx @@ -11,6 +11,7 @@ import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid import Loading from '@/app/components/base/loading' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' import { useRAGRecommendedPlugins } from '@/service/use-tools' +import { isServer } from '@/utils/client' import { getMarketplaceUrl } from '@/utils/var' import List from './list' @@ -29,14 +30,14 @@ const RAGToolRecommendations = ({ }: RAGToolRecommendationsProps) => { const { t } = useTranslation() const [isCollapsed, setIsCollapsed] = useState(() => { - if (typeof window === 'undefined') + if (isServer) return false const stored = window.localStorage.getItem(STORAGE_KEY) return stored === 'true' }) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return const stored = window.localStorage.getItem(STORAGE_KEY) if (stored !== null) @@ -44,7 +45,7 @@ const RAGToolRecommendations = ({ }, []) useEffect(() => { - if (typeof window === 'undefined') + if (isServer) return window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) }, [isCollapsed]) diff --git a/web/context/hooks/use-trigger-events-limit-modal.ts b/web/context/hooks/use-trigger-events-limit-modal.ts index 403df58378..72342cd0d3 100644 --- a/web/context/hooks/use-trigger-events-limit-modal.ts +++ b/web/context/hooks/use-trigger-events-limit-modal.ts @@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { NUM_INFINITE } from '@/app/components/billing/config' import { Plan } from '@/app/components/billing/type' import { IS_CLOUD_EDITION } from '@/config' +import { isServer } from '@/utils/client' export type TriggerEventsLimitModalPayload = { usage: number @@ -46,7 +47,7 @@ export const useTriggerEventsLimitModal = ({ useEffect(() => { if (!IS_CLOUD_EDITION) return - if (typeof window === 'undefined') + if (isServer) return if (!currentWorkspaceId) return diff --git a/web/context/query-client.tsx b/web/context/query-client.tsx index a72393490c..1cd64b168b 100644 --- a/web/context/query-client.tsx +++ b/web/context/query-client.tsx @@ -5,12 +5,13 @@ import type { FC, PropsWithChildren } from 'react' import { QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react' import { TanStackDevtoolsLoader } from '@/app/components/devtools/tanstack/loader' +import { isServer } from '@/utils/client' import { makeQueryClient } from './query-client-server' let browserQueryClient: QueryClient | undefined function getQueryClient() { - if (typeof window === 'undefined') { + if (isServer) { return makeQueryClient() } if (!browserQueryClient) diff --git a/web/hooks/use-query-params.ts b/web/hooks/use-query-params.ts index 73798a4a4f..0749a1ffa5 100644 --- a/web/hooks/use-query-params.ts +++ b/web/hooks/use-query-params.ts @@ -21,6 +21,7 @@ import { } from 'nuqs' import { useCallback } from 'react' import { ACCOUNT_SETTING_MODAL_ACTION } from '@/app/components/header/account-setting/constants' +import { isServer } from '@/utils/client' /** * Modal State Query Parameters @@ -176,7 +177,7 @@ export function usePluginInstallation() { * clearQueryParams(['param1', 'param2']) */ export function clearQueryParams(keys: string | string[]) { - if (typeof window === 'undefined') + if (isServer) return const url = new URL(window.location.href) diff --git a/web/utils/client.ts b/web/utils/client.ts new file mode 100644 index 0000000000..9ea55cafcc --- /dev/null +++ b/web/utils/client.ts @@ -0,0 +1,26 @@ +/** + * Server/Client environment detection utilities + * + * Use these constants and functions to safely detect the runtime environment + * in Next.js applications where code may execute on both server and client. + */ + +/** + * Check if code is running on server-side (SSR) + * + * @example + * if (isServer) { + * // Server-only logic + * } + */ +export const isServer = typeof window === 'undefined' + +/** + * Check if code is running on client-side (browser) + * + * @example + * if (isClient) { + * localStorage.setItem('key', 'value') + * } + */ +export const isClient = typeof window !== 'undefined' diff --git a/web/utils/gtag.ts b/web/utils/gtag.ts index 5af51a6564..5f199f1fbc 100644 --- a/web/utils/gtag.ts +++ b/web/utils/gtag.ts @@ -1,3 +1,5 @@ +import { isServer } from '@/utils/client' + /** * Send Google Analytics event * @param eventName - event name @@ -7,7 +9,7 @@ export const sendGAEvent = ( eventName: string, eventParams?: GtagEventParams, ): void => { - if (typeof window === 'undefined' || typeof (window as any).gtag !== 'function') { + if (isServer || typeof (window as any).gtag !== 'function') { return } (window as any).gtag('event', eventName, eventParams)