feat: admin framework adapter pattern and tanstack support#16139
feat: admin framework adapter pattern and tanstack support#16139
Conversation
|
Hey, I am excited about this work! I had experimented with a similar approach a few months ago. I'm sure you might be aware that TanStack Start is planning to add RSC support soon (there is an draft blog post here, not sure how up to date it is with their current plans). I was wondering if this would make a difference in the approach to the framework adapter pattern, and if RSCs in Start make this significantly easier. If you haven't already, it might be good to have some communication with the TanStack team about this. Thanks for your work on this! |
I'm aware, currently I want to see if it is possible so we don't rely on whether a framework supports RSC or not (while still maintaining 100% the same approach in Next.js). This is a bit more complex indeed but would allow room for any React framework (or even a custom one on top of Vite), not just Next/Tanstack. In this case - when Tanstack will add RSC support and if we want to use it - the only place we'd have modify is the adapter itself. |
Add RouterAdapter, ServerAdapter, ComponentRenderer, and DevReloadStrategy type contracts in packages/payload/src/admin/adapters.ts. These types form the foundation for decoupling the admin panel from Next.js.
…imports - Create RouterAdapter pattern: adapter is a React component that wraps children and populates RouterAdapterContext with framework-specific values - Replace all 41 files importing from next/navigation.js, next/link.js, and next/dist/* with framework-agnostic RouterAdapter equivalents - Replace AppRouterInstance type with RouterAdapterRouter from payload - Replace ReadonlyRequestCookies with CookieStore from payload - Replace LinkProps from next/link with LinkAdapterProps from payload - Remove next from packages/ui peerDependencies - Wire RouterAdapter component into RootProvider - Export RouterAdapterContext from client entrypoint
- Create NextRouterAdapter component that calls Next.js hooks (useRouter, usePathname, useSearchParams, useParams) and populates the framework-agnostic RouterAdapterContext - Wire NextRouterAdapter into RootLayout as the RouterAdapter prop - Export NextRouterAdapter from @payloadcms/next/client
Move pure routing utilities from packages/next/src/views/Root/ to packages/ui/src/utilities/routeResolution/: - isPathMatchingRoute, getDocumentViewInfo, attachViewActions - getCustomViewByKey, getCustomViewByRoute - Shared ViewFromConfig type Original files in packages/next re-export from @payloadcms/ui for backward compatibility. getRouteData.ts updated to import from shared.
Move framework-agnostic presentational components from packages/next: - MinimalTemplate (template + styles) → packages/ui/src/templates/Minimal/ - FormHeader (element + styles) → packages/ui/src/elements/FormHeader/ Original locations in packages/next now re-export for backward compat.
Create serverFunctionRegistry in packages/ui with framework-agnostic handlers (form-state, table-state, copy-data-from-locale, etc.). packages/next handleServerFunctions now spreads the shared registry and adds RSC-specific handlers (render-document, render-list, etc.).
Create a client-only component renderer that treats all components as client components and never passes serverProps. This is the alternative to RenderServerComponent for frameworks without RSC support.
Add candidateDirectories parameter to resolveImportMapFilePath, allowing framework adapters to specify their own directory patterns instead of defaulting to Next.js app/(payload) convention. The default behavior is unchanged for backward compatibility.
Remove import of Metadata from 'next' in packages/payload config types. Define AdminMeta type that covers the commonly-used metadata subset (title, description, openGraph, icons, twitter, keywords). MetaConfig now intersects with AdminMeta instead of Next.js Metadata. The Next.js adapter can map AdminMeta to Next.js Metadata as needed.
Replace @next/env dependency with dotenv + dotenv-expand for framework-agnostic .env file loading. The new implementation supports the same file priority convention (.env.local, .env.development, etc.) without requiring Next.js packages.
Replace hardcoded Next.js webpack-hmr WebSocket with a DevReloadStrategy interface. getPayload() now accepts an optional devReloadStrategy parameter. The default fallback preserves the current Next.js HMR behavior. Framework adapters can provide their own strategy (e.g., Vite HMR for TanStack Start).
Replace ReadonlyRequestCookies from next/dist with CookieStore from the framework adapter contract in getRequestLanguage.ts. packages/payload now has zero imports from next/ or @next/.
Introduce PAYLOAD_FRAMEWORK env variable to control which framework adapter the dev server starts with. Extract Next.js-specific startup into test/adapters/nextDevServer.ts. The dev.ts script dispatches to the appropriate adapter based on PAYLOAD_FRAMEWORK (defaults to 'next'). This enables future adapters (e.g., tanstack-start) to add their own dev server module and be selected via PAYLOAD_FRAMEWORK=tanstack-start.
Thread a `renderComponent: ComponentRenderer` parameter through the entire form state and table state pipelines instead of hardcoding `RenderServerComponent` imports. Files modified: - renderField.tsx: accepts renderComponent param instead of importing directly - buildColumnState/index.tsx, renderCell.tsx: accept renderComponent param - renderTable.tsx, renderFilters: accept renderComponent param - buildFormState.ts, buildTableState.ts: pass RenderServerComponent as default - iterateFields.ts, addFieldStatePromise.ts: thread renderComponent through - fieldSchemasToFormState/index.tsx: accept and forward renderComponent - renderFieldServerFn.ts: pass RenderServerComponent explicitly - richtext-lexical rscEntry.tsx, buildInitialState.ts: thread renderComponent Non-RSC adapters can now pass RenderClientComponent instead.
Move framework-agnostic Nav, DocumentHeader, and Logo elements from packages/next to packages/ui. Replace next/navigation hooks with RouterAdapter hooks. Replace @payloadcms/ui barrel imports with direct source imports. Leave re-exports in packages/next for backward compatibility.
Move the Default template (Wrapper, NavHamburger) from packages/next to packages/ui. Replace @payloadcms/ui barrel imports with direct source imports. Leave re-exports in packages/next.
Move the following view helpers from packages/next to packages/ui: - Version/RenderFieldsToDiff (entire directory, 22+ files) - Version/fetchVersions.ts, VersionPillLabel/ - Versions/buildColumns.tsx, cells/, types.ts - Dashboard/ (entire tree, 18+ files) - Document/ helpers (getDocumentData, getDocumentPermissions, etc.) - List/ helpers (handleGroupBy, renderListViewSlots, etc.) All @payloadcms/ui imports converted to relative paths. Re-exports left in packages/next for backward compatibility.
Remove outdated TODO comments in PerPage and Autosave components that referenced next/navigation abstraction - these components already use RouterAdapter or don't need navigation hooks at all.
Move the following auth-related view components to packages/ui: - Login/LoginForm, Login/LoginField, Login styles - ForgotPassword (full view + ForgotPasswordForm) - ResetPassword (full view + ResetPasswordForm) - CreateFirstUser (full view + client component) - Verify (full view + client component) - Logout (full view + LogoutClient) - Unauthorized (full view) All next/navigation imports switched to RouterAdapter. All @payloadcms/ui barrel imports converted to relative paths. Re-exports left in packages/next for backward compatibility. Login entry point stays in packages/next (uses redirect()).
Move APIView, APIViewClient, RenderJSON, LocaleSelector and styles from packages/next to packages/ui. Switch useSearchParams from next/navigation to RouterAdapter. Convert @payloadcms/ui barrel imports to direct relative paths. Re-exports left in packages/next.
Move AccountClient, Settings, LanguageSelector, ToggleTheme, and ResetPreferences from packages/next to packages/ui. Account entry point stays in packages/next (uses notFound()). All @payloadcms/ui barrel imports converted to relative paths.
Move DefaultVersionView, Restore, SelectComparison, SelectLocales, VersionDrawer, VersionDrawerCreatedAtCell, SelectedLocalesContext, SetStepNav, and VersionsViewClient to packages/ui. All next/navigation imports switched to RouterAdapter. All @payloadcms/ui barrel imports converted to relative paths. Re-exports created in packages/next for backward compatibility. Also fixes missing B3 re-exports for VersionPillLabel, Versions buildColumns/cells, and RenderFieldsToDiff.
Move NotFoundClient and styles to packages/ui. The NotFoundPage entry point stays in packages/next (uses initReq, Metadata). Re-export created in packages/next for backward compatibility.
…nstruction sites Ensure every serverProps object across packages/next and packages/ui includes renderComponent so import map components receive the adapter-injected renderer. This allows custom server components to render other import map components without importing a framework-specific renderer directly.
…er + client component Split the only async component in packages/ui into framework-agnostic parts: - getCollectionCardsData() data fetcher in packages/ui - CollectionCardsClient 'use client' component in packages/ui - Async RSC wrapper moved to packages/next - Removed CollectionCards from @payloadcms/ui/rsc exports
… views - Add getLoginViewData() in packages/ui for framework-agnostic login data - Add getDocumentViewData() in packages/ui for comprehensive document data - Add getEntityPreferences() utility in packages/ui (no React.cache dependency) - Refactor Next.js LoginView and renderDocument to use shared data fetchers - Document view data fetcher handles doc loading, permissions, versions, form state, slots, live preview — returns redirect info instead of calling redirect()
- Add getListViewData() in packages/ui for framework-agnostic list data - Handles query processing, preferences, presets, document fetching, table rendering, filters, and slot rendering - Accepts ComponentRenderer parameter for adapter-specific rendering - Refactor Next.js renderListView to use shared data fetcher
- Add getRootViewData() in packages/ui for auth checks, first-user flow, collection/global resolution, client config, locale filtering - Inlines isPublicRoute and handleAuthRedirect as pure helper functions - Returns redirect URLs instead of calling framework-specific redirect() - Adapter-specific route-to-component mapping stays in each adapter
- Add getAccountViewData() — document edit pipeline for current user - Add getVersionViewData() — version comparison data with parallel fetches - Add getVersionsViewData() — paginated version list with published/draft state - All data fetchers are framework-agnostic, accept ComponentRenderer param
Implement dual-mode server functions (T1.2) — each RSC-returning handler
now has a data-only alternative that returns JSON-serializable data.
- Add ServerFunctionMode type ('rsc' | 'data-only') to DefaultServerFunctionArgs
- Create data-only handlers: renderDocument, renderList, renderWidget,
renderField, renderDocumentSlots, getDefaultLayout
- Export dataOnlyServerFunctions registry from @payloadcms/ui
- Non-RSC adapters (TanStack Start) merge this registry with
sharedServerFunctions instead of using the RSC handlers
…ions views Refactor the remaining Next.js view components to delegate data fetching to the framework-agnostic data fetchers in @payloadcms/ui: - AccountView → getAccountViewData() - VersionView → getVersionViewData() - VersionsView → getVersionsViewData()
Audited codebase against the blocker list — mark resolved items (CollectionCards, data-only server functions, view data fetchers, rsc exports), detail remaining blockers (RootPage wiring, React.cache, auth server actions, initReq, env var), and add unblocking priority table.
… with prop Move the Next.js-specific env var read out of RootProvider into the Next.js adapter, passing it as an `enableRouterCacheRefresh` prop. Non-Next adapters can supply their own value (or rely on the default `false`).
Remove `import { cache } from 'react'` from five files to make
packages/ui fully framework-agnostic (React.cache is RSC-specific):
- getClientConfig, getSchemaMap, getClientSchemaMap: already have
global caches (global._payload_*) making React.cache redundant
- getNavPrefs: replaced with a WeakMap keyed on the PayloadRequest
object for equivalent per-request deduplication
- upsertPreferences (getPreferences): called once per operation,
no deduplication needed
Refactor RootPage to delegate auth checks, first-user redirects, client config generation, locale filtering, and visible-entity resolution to the shared getRootViewData fetcher in packages/ui. RootPage now only handles adapter-specific concerns: initReq, getRouteData (Next.js component mapping), collection preferences for folder views, not-found/redirect dispatch, and RSC rendering. Also fix getRootViewData's getClientConfig user param to correctly pass `true` for the createFirstUser case (when !dbHasUser), and remove unused getEntityPreferences import.
All blockers requiring shared code changes are resolved: - RootPage wired to getRootViewData - React.cache() removed from packages/ui - NEXT_PUBLIC_ENABLE_ROUTER_CACHE_REFRESH converted to prop - Remaining items (initReq, auth server functions) are scoped to the new tanstack-start package
Phase T2 of the TanStack Start adapter implementation. Creates the `@payloadcms/tanstack-start` package with: - TanStackRouterAdapter: maps TanStack Router hooks to Payload's RouterAdapterContext (Link, params, pathname, searchParams, router) - initReq: initializes Payload request from TanStack Start server context using @tanstack/react-start/server getRequest() - ServerAdapter: framework-specific server utilities (cookies, headers, redirect, notFound) using TanStack Start/Router APIs - handleServerFunctions: server function dispatcher using data-only mode (no RSC flight payloads), merging sharedServerFunctions with dataOnlyServerFunctions - Auth server functions: login, logout, refresh, switchLanguage using createServerFn from @tanstack/react-start - Supporting utilities: setPayloadAuthCookie, getExistingAuthToken, getPreferences, getRequestLocale
…ta loaders Phase T3 of the TanStack Start adapter implementation. Adds the view/layout layer that mirrors Next.js's RootLayout + RootPage pattern: - RootLayout: HTML shell wrapping RootProvider with TanStackRouterAdapter, theme detection, RTL support, language options, and nav preferences - getLayoutData: server-side loader fetching all data needed by the root admin layout (auth, i18n, client config, permissions, theme) - getAdminPageData: catch-all page loader that uses getRootViewData for auth checks/redirects, then getRouteData for view resolution - getRouteData: route-to-view-type resolver adapted from Next.js version, handling all admin routes (dashboard, collections, globals, auth views, folders, versions, custom views) without RSC component references - Supporting utilities: getRequestTheme (framework-agnostic port) - New package exports: ./layouts, ./views The architecture provides three composable functions that users wire into their TanStack Router routes: getLayoutData (layout loader) → getAdminPageData (page loader) → client component rendering.
…p, and meta utilities Phases T4-T7 of the TanStack Start adapter: - T4: TanStackComponentRenderer wrapping RenderClientComponent — all custom Payload components must be client-safe (no async RSC) - T5: viteDevReloadStrategy using import.meta.hot for Vite HMR full-reload detection - T6: getImportMapOutputPath for adapter-specific import map location - T7: getAdminMeta generating TanStack Router head.meta entries for admin page titles and viewport tags
… adapter Rewrite auth server functions as plain async functions matching the Next.js adapter pattern instead of createServerFn wrappers. Fix type mismatches in router adapter, layout data, route resolution, and view components. Add proper null guards, type casts, and Vite HMR type declarations to pass build and lint with strict: true.
Wire up `pnpm dev:tanstack <suite>` to start a TanStack Start dev server that loads any test config. Key pieces: - test/tanstack-app: minimal TanStack Start app with file-based routes (__root, admin.$, admin.index, api.$, index) and a custom Vite SSR middleware that bypasses nitro's worker-based runner to avoid DataCloneError with native modules. - test/adapters/tanstackStartDevServer.ts: spawns the local vite binary with the correct env vars and resolves once the server is ready. - test/dev.ts, initDevAndTest.ts, testHooks.ts: framework-aware paths for import map generation, tsconfig patching and cache clearing. - vite.config.ts: SCSS tilde-import resolver, SSR externals for native packages, and optimizeDeps exclusions matching withPayload.js.
…stack-app to root Move test/tanstack-app/ to root tanstack-app/ as its own workspace package. Split @payloadcms/tanstack-start/views into client-only (./views) and server-only (./views/server) entry points. Route files now use createServerFn wrappers with dynamic imports, eliminating ~200 lines of Node.js module stubs from vite.config.ts.
…ed rendering Extract buildListViewClientProps and buildDocumentViewClientProps from @payloadcms/ui to reconstruct React nodes from serializable data on the client using RenderClientComponent + importMap. Rewrite AdminView.tsx to delegate to DefaultListView, DefaultEditView, DashboardView, and LoginForm. Import admin SCSS in Root layout for proper styling. Pass fieldPermissions through to renderTable so columns render correctly.
…dev server Stub server-only modules (pino, sharp, busboy, file-type, etc.) to prevent them from leaking into the client bundle and breaking React hydration. Add Payload base styles import to the root component. Forward request body in the SSR middleware for POST/PATCH/PUT methods using Readable.toWeb().
b0ca0a2 to
0a20737
Compare
- Import CollectionCards SCSS in client component to ensure CSS loads when the widget is resolved dynamically via the import map - Replace simplified DashboardNavGroups with proper CollectionCardsClient in TanStack Start, matching the Next.js dashboard layout - Silence Sass deprecation warnings for import and global-builtin
This is an experiment for now