diff --git a/packages/next/src/views/Root/SyncClientConfig.tsx b/packages/next/src/views/Root/SyncClientConfig.tsx
deleted file mode 100644
index 46579be35..000000000
--- a/packages/next/src/views/Root/SyncClientConfig.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-'use client'
-import type { ClientConfig } from 'payload'
-
-import { useConfig } from '@payloadcms/ui'
-import { useEffect } from 'react'
-
-/**
- * This component is required in order for the _page_ to be able to refresh the client config,
- * which may have been cached on the _layout_ level, where the ConfigProvider is managed.
- * Since the layout does not re-render on page navigation / authentication, we need to manually
- * update the config, as the user may have been authenticated in the process, which affects the client config.
- */
-export const SyncClientConfig = ({ clientConfig }: { clientConfig: ClientConfig }) => {
- const { setConfig } = useConfig()
-
- useEffect(() => {
- setConfig(clientConfig)
- }, [clientConfig, setConfig])
-
- return null
-}
diff --git a/packages/next/src/views/Root/index.tsx b/packages/next/src/views/Root/index.tsx
index 4de69cfb0..5b6a9b90e 100644
--- a/packages/next/src/views/Root/index.tsx
+++ b/packages/next/src/views/Root/index.tsx
@@ -10,6 +10,7 @@ import type {
SanitizedGlobalConfig,
} from 'payload'
+import { RootPageConfigProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { notFound, redirect } from 'next/navigation.js'
@@ -27,7 +28,6 @@ import { isCustomAdminView } from '../../utilities/isCustomAdminView.js'
import { isPublicAdminRoute } from '../../utilities/isPublicAdminRoute.js'
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
import { getRouteData } from './getRouteData.js'
-import { SyncClientConfig } from './SyncClientConfig.js'
export type GenerateViewMetadata = (args: {
config: SanitizedConfig
@@ -300,8 +300,7 @@ export const RootPage = async ({
})
return (
-
-
+
{!templateType && {RenderedView}}
{templateType === 'minimal' && (
{RenderedView}
@@ -332,6 +331,6 @@ export const RootPage = async ({
{RenderedView}
)}
-
+
)
}
diff --git a/packages/payload/src/config/client.ts b/packages/payload/src/config/client.ts
index 09ba5d756..7d410ecdd 100644
--- a/packages/payload/src/config/client.ts
+++ b/packages/payload/src/config/client.ts
@@ -12,7 +12,6 @@ import type {
import {
type ClientCollectionConfig,
- createClientCollectionConfig,
createClientCollectionConfigs,
} from '../collections/config/client.js'
import { createClientBlocks } from '../fields/config/client.js'
@@ -43,16 +42,6 @@ export type ServerOnlyRootProperties = keyof Pick<
export type ServerOnlyRootAdminProperties = keyof Pick
-export type UnsanitizedClientConfig = {
- admin: {
- livePreview?: Omit
- } & Omit
- blocks: ClientBlock[]
- collections: ClientCollectionConfig[]
- custom?: Record
- globals: ClientGlobalConfig[]
-} & Omit
-
export type ClientConfig = {
admin: {
livePreview?: Omit
@@ -62,6 +51,7 @@ export type ClientConfig = {
collections: ClientCollectionConfig[]
custom?: Record
globals: ClientGlobalConfig[]
+ unauthenticated?: boolean
} & Omit
export type UnauthenticatedClientConfig = {
@@ -73,6 +63,7 @@ export type UnauthenticatedClientConfig = {
globals: []
routes: ClientConfig['routes']
serverURL: ClientConfig['serverURL']
+ unauthenticated: ClientConfig['unauthenticated']
}
export const serverOnlyAdminConfigProperties: readonly Partial[] = []
@@ -132,6 +123,7 @@ export const createUnauthenticatedClientConfig = ({
globals: [],
routes: clientConfig.routes,
serverURL: clientConfig.serverURL,
+ unauthenticated: true,
}
}
@@ -189,6 +181,17 @@ export const createClientConfig = ({
importMap,
}).filter((block) => typeof block !== 'string') as ClientBlock[]
+ clientConfig.blocksMap = {}
+ if (clientConfig.blocks?.length) {
+ for (const block of clientConfig.blocks) {
+ if (!block?.slug) {
+ continue
+ }
+
+ clientConfig.blocksMap[block.slug] = block as ClientBlock
+ }
+ }
+
break
}
diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts
index f45e75c0a..7a95952f8 100644
--- a/packages/payload/src/index.ts
+++ b/packages/payload/src/index.ts
@@ -1274,7 +1274,6 @@ export {
serverOnlyAdminConfigProperties,
serverOnlyConfigProperties,
type UnauthenticatedClientConfig,
- type UnsanitizedClientConfig,
} from './config/client.js'
export { defaults } from './config/defaults.js'
diff --git a/packages/ui/src/exports/client/index.ts b/packages/ui/src/exports/client/index.ts
index 243ed854b..ad62e9f3c 100644
--- a/packages/ui/src/exports/client/index.ts
+++ b/packages/ui/src/exports/client/index.ts
@@ -303,7 +303,7 @@ export {
RouteTransitionProvider,
useRouteTransition,
} from '../../providers/RouteTransition/index.js'
-export { ConfigProvider, useConfig } from '../../providers/Config/index.js'
+export { ConfigProvider, RootPageConfigProvider, useConfig } from '../../providers/Config/index.js'
export { DocumentEventsProvider, useDocumentEvents } from '../../providers/DocumentEvents/index.js'
export { DocumentInfoProvider, useDocumentInfo } from '../../providers/DocumentInfo/index.js'
export { useDocumentTitle } from '../../providers/DocumentTitle/index.js'
diff --git a/packages/ui/src/providers/Config/index.tsx b/packages/ui/src/providers/Config/index.tsx
index 4014a0480..20e3bfc8b 100644
--- a/packages/ui/src/providers/Config/index.tsx
+++ b/packages/ui/src/providers/Config/index.tsx
@@ -6,7 +6,6 @@ import type {
ClientGlobalConfig,
CollectionSlug,
GlobalSlug,
- UnsanitizedClientConfig,
} from 'payload'
import React, { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -40,36 +39,14 @@ export type ClientConfigContext = {
const RootConfigContext = createContext(undefined)
-function sanitizeClientConfig(
- unSanitizedConfig: ClientConfig | UnsanitizedClientConfig,
-): ClientConfig {
- if (!unSanitizedConfig?.blocks?.length || (unSanitizedConfig as ClientConfig).blocksMap) {
- ;(unSanitizedConfig as ClientConfig).blocksMap = {}
- return unSanitizedConfig as ClientConfig
- }
- const sanitizedConfig: ClientConfig = { ...unSanitizedConfig } as ClientConfig
-
- sanitizedConfig.blocksMap = {}
-
- for (const block of unSanitizedConfig.blocks) {
- sanitizedConfig.blocksMap[block.slug] = block
- }
-
- return sanitizedConfig
-}
-
export const ConfigProvider: React.FC<{
readonly children: React.ReactNode
- readonly config: ClientConfig | UnsanitizedClientConfig
+ readonly config: ClientConfig
}> = ({ children, config: configFromProps }) => {
- const [config, setConfigFn] = useState(() => sanitizeClientConfig(configFromProps))
+ const [config, setConfig] = useState(configFromProps)
const isFirstRenderRef = useRef(true)
- const setConfig = useCallback((newConfig: ClientConfig | UnsanitizedClientConfig) => {
- setConfigFn(sanitizeClientConfig(newConfig))
- }, [])
-
// Need to update local config state if config from props changes, for HMR.
// That way, config changes will be updated in the UI immediately without needing a refresh.
useEffect(() => {
@@ -112,9 +89,52 @@ export const ConfigProvider: React.FC<{
[collectionsBySlug, globalsBySlug],
)
- const value = useMemo(() => ({ config, getEntityConfig, setConfig }), [config, getEntityConfig])
+ const value = useMemo(
+ () => ({ config, getEntityConfig, setConfig }),
+ [config, getEntityConfig, setConfig],
+ )
return {children}
}
export const useConfig = (): ClientConfigContext => use(RootConfigContext)
+
+/**
+ * This provider shadows the ConfigProvider on the _page_ level, allowing us to
+ * update the config when needed, e.g. after authentication.
+ * The layout ConfigProvider is not updated on page navigation / authentication,
+ * as the layout does not re-render in those cases.
+ *
+ * If the config here has the same reference as the config from the layout, we
+ * simply reuse the context from the layout to avoid unnecessary re-renders.
+ */
+export const RootPageConfigProvider: React.FC<{
+ readonly children: React.ReactNode
+ readonly config: ClientConfig
+}> = ({ children, config: configFromProps }) => {
+ const rootLayoutConfig = useConfig()
+ const { config, getEntityConfig, setConfig } = rootLayoutConfig
+
+ /**
+ * This useEffect is required in order for the _page_ to be able to refresh the client config,
+ * which may have been cached on the _layout_ level, where the ConfigProvider is managed.
+ * Since the layout does not re-render on page navigation / authentication, we need to manually
+ * update the config, as the user may have been authenticated in the process, which affects the client config.
+ */
+ useEffect(() => {
+ setConfig(configFromProps)
+ }, [configFromProps, setConfig])
+
+ if (config !== configFromProps && config.unauthenticated !== configFromProps.unauthenticated) {
+ // Between the unauthenticated config becoming authenticated (or the other way around) and the useEffect
+ // running, the config will be stale. In order to avoid having the wrong config in the context in that
+ // brief moment, we shadow the context here on the _page_ level and provide the updated config immediately.
+ return (
+
+ {children}
+
+ )
+ }
+
+ return {children}
+}
diff --git a/test/auth/e2e.spec.ts b/test/auth/e2e.spec.ts
index 53aa87b23..c6217a172 100644
--- a/test/auth/e2e.spec.ts
+++ b/test/auth/e2e.spec.ts
@@ -52,6 +52,7 @@ describe('Auth', () => {
page = await context.newPage()
initPageConsoleErrorCatch(page)
})
+
describe('create first user', () => {
beforeAll(async () => {
await reInitializeDB({
@@ -386,6 +387,36 @@ describe('Auth', () => {
await saveDocAndAssert(page)
})
+
+ test('ensure login page with redirect to users document redirects properly after login, without client error', async () => {
+ await page.goto(url.admin)
+
+ await page.goto(`${serverURL}/admin/logout`)
+
+ await expect(page.locator('.login')).toBeVisible()
+
+ const users = await payload.find({
+ collection: slug,
+ limit: 1,
+ })
+ const userDocumentRoute = `${serverURL}/admin/collections/users/${users?.docs?.[0]?.id}`
+
+ await page.goto(userDocumentRoute)
+
+ await expect(page.locator('#field-email')).toBeVisible()
+ await expect(page.locator('#field-password')).toBeVisible()
+
+ await page.locator('.form-submit > button').click()
+
+ // Expect to be redirected to the correct page
+ await expect
+ .poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT })
+ .toBe(userDocumentRoute)
+
+ // Previously, this would crash the page with a "Cannot read properties of undefined (reading 'match')" error
+
+ await expect(page.locator('#field-roles')).toBeVisible()
+ })
})
})
})