Admin Modal Management#
Overview#
The Strapi admin panel's relation modal allows editors to open, edit, and create related documents inline without leaving the parent edit view. It is the primary source of several cross-cutting modal-management concerns in the Content Manager: viewport containment, scroll affordance, unsaved-changes state persistence, and parent-document refetch side effects triggered on modal close.
All modal state and navigation logic lives in RelationModal.tsx. Supporting files include DocumentActions.tsx (relation-to-parent connection logic) and useDocumentLayout.ts (schema resolution guards).
Modal Architecture#
Renderer Hierarchy#
RelationModal.tsx uses a three-tier renderer pattern :
RelationModalRenderer— checks at render time whether a parentRelationModalcontext exists.RootRelationRenderer— no existing context: creates auseReducer, mountsRelationModalProvider.NestedRelationRenderer— context exists: readsdispatchfrom parent, creates no new reducer.
The active document is always state.documentHistory.at(-1) ?? rootDocumentMeta, so deeper navigation simply pushes onto the stack.
State Shape#
The State interface tracks:
| Field | Purpose |
|---|---|
documentHistory | Stack of DocumentMeta; top = active document |
isModalOpen | Modal visibility |
hasUnsavedChanges | Lifted from inner <Form> context |
confirmDialogIntent | null / 'close' / 'back' / 'navigate' / DocumentMeta — drives the discard-changes dialog |
fieldToConnect | Dot-path on parent for a newly created relation |
fieldToConnectUID | Component UID when the parent component is also new |
getParentFormValues | Closure capturing current parent form values at modal-open time |
Seven action types handle navigation: GO_TO_RELATION, GO_BACK, GO_FULL_PAGE, GO_TO_CREATED_RELATION, CANCEL_CONFIRM_DIALOG, CLOSE_MODAL, SET_HAS_UNSAVED_CHANGES. When hasUnsavedChanges is true, any navigation action sets confirmDialogIntent instead of executing, triggering a discard-changes dialog.
Component Context Isolation#
The modal shell resets the ComponentProvider context (id=undefined, level=-1, uid=undefined) to prevent inheriting the parent form's componentUID and level. Without this, relation search queries would resolve to the wrong model and ID .
Refetch Side Effect on Modal Close#
When the modal closes without unsaved changes, handleCloseModal calls:
triggerRefetchDocument(rootDocumentMeta, /* preferCache= */ true)
This re-fetches the root (parent) document via useLazyGetDocumentQuery to refresh relation counts and field values that may have changed inside the modal. The preferCache: true flag serves RTK Query cache when valid. An inline TODO comment flags this as a workaround — ideally invalidatesTags would handle the refetch automatically .
This refetch has two known downstream side effects:
- Pagination reset — The
RelationsFieldcomponent loses its localcurrentPagestate on refetch, reverting to 5 entries and making "Load More" non-functional . - DynamicZone crash —
SET_INITIAL_VALUESresets form state with the refetched payload, which may containnullfor empty DZ fields.DynamicZone/Field.tsxuses a default parameter= []that only fires forundefined, so accessing.lengthonnullthrowsTypeError.
Known Issues#
Parent Form State Discarded on Relation Creation — PR #27081#
Bug: Creating a relation on the fly (especially from inside a newly added dynamic-zone component) silently discards unsaved edits on the parent form .
Root cause: connectRelationToParent sourced parentDataToUpdate from getInitialFormValues() — the server-fetched initial state — rather than the current in-memory form values. Fields filled in after the last save were lost .
Fix (PR #27081): Modal State now carries a getParentFormValues closure captured at modal-open time . connectRelationToParent merges into existing connect entries (deduped by documentId + locale) instead of overwriting, preserving any pending relations in the same field .
DynamicZone Crash After Closing Relation Modal — Issue #26813#
Bug: React crashes with TypeError: Cannot read properties of null (reading 'length') at DynamicZone/Field.tsx after closing any relation modal on a content type that also has a DynamicZone .
Root cause (chain): Modal close triggers triggerRefetchDocument → SET_INITIAL_VALUES resets form state with the refetched payload → the payload contains null for empty DZ fields → DynamicZone/Field.tsx uses a default parameter = [] that only fires for undefined, not null, causing .length to throw .
Suggested fixes: Nullish coalescing in DynamicZone/Field.tsx (_rawDZValue ?? []), or fixing traverseData to not short-circuit for null before calling the predicate .
On-the-Fly Relation Creation Crash in DZ Component — Issue #27246#
Bug: Creating a new related entry from a relation field inside a component nested in a dynamic zone throws immediately on modal open: "this is likely a bug with Strapi" .
Status: Under investigation; root cause not yet documented. Selecting an existing relation works without error.
Relation Field Resets to 5 Entries After Modal Close — Issue #27199#
Bug: After loading more than 5 entries and opening a relation modal, closing the modal resets the relation field to 5 entries and "Load More" stops working .
Root cause: triggerRefetchDocument on modal close causes RelationsField to lose its local currentPage state, resetting to page 1. The RTK Query cache merge then replaces all accumulated entries with first-page results only.
Pattern note: Issues #26813 and #27199 share a common root: the
triggerRefetchDocumentcall on every clean modal close is a systemic source of state loss. The long-term fix is replacing it with RTK QueryinvalidatesTags.
Scroll and Layout Issues#
Duplicate Scrollbar in List View (Chromium) — PR #26133#
Bug: Large collection types produced a double scrollbar in Chromium, allowing users to scroll past the content .
Root cause: Absolutely-positioned descendants of OverflowingItem resolved their containing block to <html> (since all ancestors were position: static), inflating html.scrollHeight and surfacing a window-level scrollbar. Firefox computes scrollHeight differently and was unaffected .
Fix: OverflowingItem in Layout.tsx received position: relative and overflow-x: hidden, anchoring absolute descendants inside its clipping region.
Sidenav Scroll Bleeds into Main Content — PR #25379#
Bug: Scrolling inside the side navigation simultaneously scrolled the main content area .
Root cause: Sticky positioning caused scroll chaining between the sidenav and the main content.
Fix: MainSubNav in SubNav.tsx received overscroll-behavior: contain inside the medium breakpoint, isolating scroll to the hovered container.
Locale Picker Hides Entries Without Scroll Indicator — Issue #26562#
Bug: When 5+ locales are configured, the locale picker dropdown silently hides locales beyond the 4th. No scroll indicator is visible in most OS/browser themes .
Root cause: The SingleSelect component from @strapi/design-system applies max-height: 15.6rem (fits ~4 items) with no SelectScrollUpButton / SelectScrollDownButton from Radix UI. The fix is in the design system layer.
Key Files#
| File | Role |
|---|---|
RelationModal.tsx | All modal state, reducer, context, renderer hierarchy, and close/refetch logic |
DocumentActions.tsx | connectRelationToParent — merges newly created relation back into parent form |
useDocumentLayout.ts | formatEditLayout with guards against missing schemas in nested modals |
Layout.tsx | OverflowingItem — position: relative fix for Chromium double-scrollbar |
SubNav.tsx | MainSubNav — overscroll-behavior: contain fix for sidenav scroll bleed |
ComponentContext.tsx | useComponent — provides id, uid, level, type to nested forms; reset in modal shell |