fix(web): stabilize deployment state hydration (#37818)

This commit is contained in:
Stephen Zhou 2026-06-23 19:07:15 +08:00 committed by GitHub
parent 4ac8c5a30e
commit b56e5f74e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 61 additions and 66 deletions

View File

@ -27,25 +27,26 @@ export default async function Layout({ children }: { children: ReactNode }) {
<OAuthRegistrationAnalytics />
<EducationVerifyActionRecorder />
<CommonLayoutHydrationBoundary>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
<ModalContextProvider>
<NextRouteStateBridge />
<MainNavLayout>
<RoleRouteGuard>
{children}
</RoleRouteGuard>
</MainNavLayout>
<InSiteMessageNotification />
<PartnerStack />
<ReadmePanel />
<GotoAnything />
<WorkflowGeneratorMount />
</ModalContextProvider>
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
<NextRouteStateBridge>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
<ModalContextProvider>
<MainNavLayout>
<RoleRouteGuard>
{children}
</RoleRouteGuard>
</MainNavLayout>
<InSiteMessageNotification />
<PartnerStack />
<ReadmePanel />
<GotoAnything />
<WorkflowGeneratorMount />
</ModalContextProvider>
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</NextRouteStateBridge>
</CommonLayoutHydrationBoundary>
<Zendesk />
</>

View File

@ -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<NextRouteState>({
pathname: '',
params: {},

View File

@ -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<NextRouteParams>()
const setNextRouteState = useSetAtom(setNextRouteStateAtom)
useIsomorphicLayoutEffect(() => {
setNextRouteState({
useHydrateAtoms([
[setNextRouteStateAtom, {
pathname,
params,
})
}, [params, pathname, setNextRouteState])
}],
] as const, { dangerouslyForceHydrate: true })
return null
return children
}

View File

@ -43,9 +43,11 @@ function CreateReleaseCloseButton() {
export function CreateReleaseDialogContent() {
return (
<ScopeProvider atoms={[createReleaseFormAtom]}>
<CreateReleaseDialogSurface />
</ScopeProvider>
<DialogContent className="top-[18dvh] w-140 max-w-[calc(100vw-32px)] translate-y-0 overflow-hidden p-0">
<ScopeProvider atoms={[createReleaseFormAtom]} name="CreateReleaseForm">
<CreateReleaseDialogSurface />
</ScopeProvider>
</DialogContent>
)
}
@ -75,7 +77,7 @@ function CreateReleaseDialogSurface() {
}
return (
<DialogContent className="top-[18dvh] w-140 max-w-[calc(100vw-32px)] translate-y-0 overflow-hidden p-0">
<>
<CreateReleaseCloseButton />
<form
noValidate
@ -105,6 +107,6 @@ function CreateReleaseDialogSurface() {
<CreateReleaseActions />
</form>
</DialogContent>
</>
)
}

View File

@ -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 (
<ScopeProvider
key={stateKey}
atoms={[
[deploymentsListEnvironmentIdAtom, envFilter],
[deploymentsListKeywordsAtom, keywords],
]}
name="DeploymentsList"
>
{children}
</ScopeProvider>
)
}
export function DeploymentsList() {
return (
<DeploymentsListStateBoundary>

View File

@ -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<string | null>(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<string | null>(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<ListAppInstanceSummariesResponse>) {
const rows = data?.pages?.flatMap(page =>