diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index 6092618bfb3..71baf83c795 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -27,25 +27,26 @@ export default async function Layout({ children }: { children: ReactNode }) { - - - - - - - - {children} - - - - - - - - - - - + + + + + + + + {children} + + + + + + + + + + + + diff --git a/web/app/components/next-route-state/atoms.ts b/web/app/components/next-route-state/atoms.ts index 1997f284976..0378415452c 100644 --- a/web/app/components/next-route-state/atoms.ts +++ b/web/app/components/next-route-state/atoms.ts @@ -7,6 +7,8 @@ type NextRouteState = { params: NextRouteParams } +// Mirrors Next router state. NextRouteStateBridge force-hydrates this atom on +// render so feature atoms can read route state without calling router hooks. const nextRouteStateAtom = atom({ pathname: '', params: {}, diff --git a/web/app/components/next-route-state/index.tsx b/web/app/components/next-route-state/index.tsx index 7fb2d83c767..cc0bec4f4cb 100644 --- a/web/app/components/next-route-state/index.tsx +++ b/web/app/components/next-route-state/index.tsx @@ -1,24 +1,25 @@ 'use client' +import type { ReactNode } from 'react' import type { NextRouteParams } from './atoms' -import { useIsomorphicLayoutEffect } from 'foxact/use-isomorphic-layout-effect' -import { useSetAtom } from 'jotai' +import { useHydrateAtoms } from 'jotai/utils' import { useParams, usePathname } from '@/next/navigation' import { setNextRouteStateAtom, } from './atoms' -export function NextRouteStateBridge() { +export function NextRouteStateBridge({ children }: { + children: ReactNode +}) { const pathname = usePathname() const params = useParams() - const setNextRouteState = useSetAtom(setNextRouteStateAtom) - useIsomorphicLayoutEffect(() => { - setNextRouteState({ + useHydrateAtoms([ + [setNextRouteStateAtom, { pathname, params, - }) - }, [params, pathname, setNextRouteState]) + }], + ] as const, { dangerouslyForceHydrate: true }) - return null + return children } diff --git a/web/features/deployments/create-release/ui/dialog.tsx b/web/features/deployments/create-release/ui/dialog.tsx index a0b6dde2fc6..9dd2731af6f 100644 --- a/web/features/deployments/create-release/ui/dialog.tsx +++ b/web/features/deployments/create-release/ui/dialog.tsx @@ -43,9 +43,11 @@ function CreateReleaseCloseButton() { export function CreateReleaseDialogContent() { return ( - - - + + + + + ) } @@ -75,7 +77,7 @@ function CreateReleaseDialogSurface() { } return ( - + <>
-
+ ) } diff --git a/web/features/deployments/list/index.tsx b/web/features/deployments/list/index.tsx index af44215fe14..05eef13a39c 100644 --- a/web/features/deployments/list/index.tsx +++ b/web/features/deployments/list/index.tsx @@ -1,37 +1,8 @@ 'use client' -import type { ReactNode } from 'react' -import { ScopeProvider } from 'jotai-scope' -import { useQueryState } from 'nuqs' -import { - deploymentsListEnvironmentIdAtom, - deploymentsListKeywordsAtom, - envFilterQueryState, - keywordsQueryState, -} from './state' +import { DeploymentsListStateBoundary } from './state' import { DeploymentsListShell } from './ui/shell' -function DeploymentsListStateBoundary({ children }: { - children: ReactNode -}) { - const [envFilter] = useQueryState('env', envFilterQueryState) - const [keywords] = useQueryState('keywords', keywordsQueryState) - const stateKey = `${envFilter ?? 'all'}:${keywords}` - - return ( - - {children} - - ) -} - export function DeploymentsList() { return ( diff --git a/web/features/deployments/list/state/index.ts b/web/features/deployments/list/state/index.ts index 0b8b91deae7..fd8bf674aa7 100644 --- a/web/features/deployments/list/state/index.ts +++ b/web/features/deployments/list/state/index.ts @@ -2,10 +2,12 @@ import type { ListAppInstanceSummariesResponse } from '@dify/contracts/enterprise/types.gen' import type { InfiniteData, QueryKey } from '@tanstack/react-query' +import type { ReactNode } from 'react' import { keepPreviousData } from '@tanstack/react-query' import { atom } from 'jotai' import { atomWithInfiniteQuery, atomWithQuery } from 'jotai-tanstack-query' -import { parseAsString } from 'nuqs' +import { useHydrateAtoms } from 'jotai/utils' +import { parseAsString, useQueryState } from 'nuqs' import { consoleQuery } from '@/service/client' import { getNextPageParamFromPagination, SOURCE_APPS_PAGE_SIZE } from '../../shared/domain/pagination' import { deploymentStatusPollingInterval } from '../../shared/domain/runtime-status' @@ -13,8 +15,24 @@ import { deploymentStatusPollingInterval } from '../../shared/domain/runtime-sta export const envFilterQueryState = parseAsString.withOptions({ history: 'push' }) export const keywordsQueryState = parseAsString.withDefault('').withOptions({ history: 'push' }) -export const deploymentsListKeywordsAtom = atom('') -export const deploymentsListEnvironmentIdAtom = atom(null) +// Mirrors nuqs URL state. DeploymentsListStateBoundary force-hydrates these +// atoms on render so query atoms can read URL filters through Jotai. +const deploymentsListKeywordsAtom = atom('') +const deploymentsListEnvironmentIdAtom = atom(null) + +export function DeploymentsListStateBoundary({ children }: { + children: ReactNode +}) { + const [envFilter] = useQueryState('env', envFilterQueryState) + const [keywords] = useQueryState('keywords', keywordsQueryState) + + useHydrateAtoms([ + [deploymentsListEnvironmentIdAtom, envFilter], + [deploymentsListKeywordsAtom, keywords], + ] as const, { dangerouslyForceHydrate: true }) + + return children +} function listDeploymentStatusPollingInterval(data?: InfiniteData) { const rows = data?.pages?.flatMap(page =>