You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is about how the check requested in #317 is implemented, not about whether it should exist. The
goal there — make an unusual, IDE-undiscoverable API shape fail loudly instead of silently — is worth
keeping. But comparing element identity over-rejects: it also refuses code that does exactly what the
error message asks for.
validateChildren in src/MasterDetailLayout.tsx rejects any child whose type is not identity-equal to Master, Detail or DetailPlaceholder:
functionvalidateChildren(children: React.ReactNode){React.Children.forEach(children,(child)=>{if(React.isValidElement(child)&&child.type!==Master&&child.type!==Detail&&child.type!==DetailPlaceholder){thrownewError('Invalid child in MasterDetailLayout. Only <MasterDetailLayout.Master>, …');}});}
Identity equality holds only when the element is created from the very same function object. It does
not hold for a component that renders one of the three internally, nor for one of the three behind memo or forwardRef — so the throw fires on code whose intent is exactly the documented one. MasterDetailLayout is the only component in @vaadin/react-components that cannot be composed
indirectly.
Reproduction
npm create vite@latest mdl-repro -- --template react-ts && cd mdl-repro && npm i @vaadin/react-components@25.2.8 @vaadin/aura@25.2.8,
then:
importReact,{typePropsWithChildren}from'react';import{MasterDetailLayout}from'@vaadin/react-components/MasterDetailLayout.js';constmaster=<MasterDetailLayout.Master>master</MasterDetailLayout.Master>;// 1. documented usage — rendersexportconstDirect=()=>(<MasterDetailLayout>{master}<MasterDetailLayout.Detail>detail</MasterDetailLayout.Detail></MasterDetailLayout>);// 2. a wrapper of your own that renders Detail — THROWSconstMyDetail=({ children }: PropsWithChildren)=><MasterDetailLayout.Detail>{children}</MasterDetailLayout.Detail>;exportconstWrapped=()=>(<MasterDetailLayout>{master}<MyDetail>detail</MyDetail></MasterDetailLayout>);// 3. React.memo around Detail — THROWS. The child IS Detail, behind a memo object.constMemoDetail=React.memo(MasterDetailLayout.Detail);exportconstMemoized=()=>(<MasterDetailLayout>{master}<MemoDetail>detail</MemoDetail></MasterDetailLayout>);// 4. a wrapper that FORWARDS children it did not create — rendersconstShell=({ children }: PropsWithChildren)=><MasterDetailLayout>{children}</MasterDetailLayout>;exportconstForwarded=()=>(<Shell>{master}<MasterDetailLayout.Detail>detail</MasterDetailLayout.Detail></Shell>);
Results on @vaadin/react-components@25.2.8, Chromium, verified under React 19.2.8 and React 18.3.1
(each case mounted behind an error boundary so one throw does not hide the others):
case
how the Detail element is produced
outcome
1
written at the call site
renders
2
rendered inside a wrapper component
throws
3
React.memo(MasterDetailLayout.Detail)
throws
4
created at the call site, passed through a wrapper
renders
Case 4 bounds the problem: forwarding children preserves identity and is fine. What breaks is producing one of the three slot elements inside another component — the ordinary way to factor out a
layout used on more than one screen.
Case 3 is the clearest evidence that identity is the wrong test: the child is literally Detail, and
the error still says only Detail is allowed.
The same reasoning covers a case that is much harder to debug: if two copies of @vaadin/react-components end up on the page — a dual ESM/CJS resolution, or a bundle that
externalizes the package while the host also loads it — then MasterDetailLayout and the Master the
caller imported come from different module instances, the identities differ, and correct code throws.
Expected
Producing the three slot components indirectly — a wrapper, memo, forwardRef — should work, as it
does for every other component in the package. A genuinely wrong child should still produce a clear
error.
Why this costs more than it looks
It is a throw, not a warning. The subtree unmounts; without an error boundary the app is blank.
There is no type-level signal.children is ReactNode, so every case above compiles and then
fails at runtime. The docs' warning ("Using any other component as a child will throw an error")
reads as being about mistakes, and gives no hint that a correct wrapper is one.
The underlying web component is more permissive than its wrapper.vaadin-master-detail-layout
takes slot="detail" children, so following the web component's documentation produces React code
that compiles and throws.
It runs on every render, in the component body, in production builds too.
Relationship to existing issues
[MasterDetailLayout] Verify proper usage of Master / Detail wrapper components #317 asked for this check ("Throw an error if any other type of child component is used"), to
make the wrapper-component API discoverable. Nothing proposed below removes that diagnostic — it only
stops it firing on children that are the wrapper components.
Caveat worth stating plainly: relaxing this check is necessary but not sufficient for the wrapper
use case. A shared shell that renders Detail internally would pass validation and then hit #315 —
the same wrapping validation objects to is the wrapping that defeats transition detection. This issue
is the smaller, self-contained half. It is worth fixing on its own because it turns working code into a
blank screen, but it does not resolve #315.
Suggested fix
The aim is to keep #317's diagnostic for genuine mistakes while letting the three slot components be
produced indirectly.
Any check on child.type alone cannot recognise a wrapper — MyDetail is an opaque function, and
what it renders is unknowable without rendering it. So the identity comparison cannot be repaired, only
relaxed. Two changes that together keep the helpful error without blocking composition:
Recognise the slot components by a marker rather than by identity, so a wrapper can opt in and
two module instances agree:
exportconstMASTER_DETAIL_SLOT=Symbol.for('vaadin.master-detail-layout.slot');Master[MASTER_DETAIL_SLOT]='master';Detail[MASTER_DETAIL_SLOT]='detail';DetailPlaceholder[MASTER_DETAIL_SLOT]='detail-placeholder';// in validateChildrenconsttype=child.typeasany;constslot=type?.[MASTER_DETAIL_SLOT]??type?.type?.[MASTER_DETAIL_SLOT];// unwraps memo/forwardRefif(!slot){/* warn */}
Symbol.for is a cross-realm registry, so this survives duplicate copies of the package, and
unwrapping type.type covers memo and forwardRef with no change at the call site.
Accepting slot="detail" children the way the web component does would also resolve it, and would
close the gap between the two documentation sets.
Found while building a component library on top of the React wrappers, where the layout is rendered
by a shared shell component rather than written out at each call site.
Description
This is about how the check requested in #317 is implemented, not about whether it should exist. The
goal there — make an unusual, IDE-undiscoverable API shape fail loudly instead of silently — is worth
keeping. But comparing element identity over-rejects: it also refuses code that does exactly what the
error message asks for.
validateChildreninsrc/MasterDetailLayout.tsxrejects any child whosetypeis notidentity-equal to
Master,DetailorDetailPlaceholder:Identity equality holds only when the element is created from the very same function object. It does
not hold for a component that renders one of the three internally, nor for one of the three behind
memoorforwardRef— so the throw fires on code whose intent is exactly the documented one.MasterDetailLayoutis the only component in@vaadin/react-componentsthat cannot be composedindirectly.
Reproduction
npm create vite@latest mdl-repro -- --template react-ts && cd mdl-repro && npm i @vaadin/react-components@25.2.8 @vaadin/aura@25.2.8,then:
Results on
@vaadin/react-components@25.2.8, Chromium, verified under React 19.2.8 and React 18.3.1(each case mounted behind an error boundary so one throw does not hide the others):
Detailelement is producedReact.memo(MasterDetailLayout.Detail)Case 4 bounds the problem: forwarding children preserves identity and is fine. What breaks is
producing one of the three slot elements inside another component — the ordinary way to factor out a
layout used on more than one screen.
Case 3 is the clearest evidence that identity is the wrong test: the child is literally
Detail, andthe error still says only
Detailis allowed.The same reasoning covers a case that is much harder to debug: if two copies of
@vaadin/react-componentsend up on the page — a dual ESM/CJS resolution, or a bundle thatexternalizes the package while the host also loads it — then
MasterDetailLayoutand theMasterthecaller imported come from different module instances, the identities differ, and correct code throws.
Expected
Producing the three slot components indirectly — a wrapper,
memo,forwardRef— should work, as itdoes for every other component in the package. A genuinely wrong child should still produce a clear
error.
Why this costs more than it looks
childrenisReactNode, so every case above compiles and thenfails at runtime. The docs' warning ("Using any other component as a child will throw an error")
reads as being about mistakes, and gives no hint that a correct wrapper is one.
vaadin-master-detail-layouttakes
slot="detail"children, so following the web component's documentation produces React codethat compiles and throws.
Relationship to existing issues
[MasterDetailLayout] Verify proper usage of Master / Detail wrapper components #317 asked for this check ("Throw an error if any other type of child component is used"), to
make the wrapper-component API discoverable. Nothing proposed below removes that diagnostic — it only
stops it firing on children that are the wrapper components.
[MasterDetailLayout] View transitions do not work when using MDL as Hilla router layout #315 / [MasterDetailLayout] Hilla routing integration is broken #313 are the same identity assumption in the sibling function
areChildrenDifferent,~30 lines up, failing in the opposite direction: a router wraps every child view in a provider, so
the type never changes and the view transition never starts. Two functions, one premise — that
child.typeidentity carries semantic meaning about what the child is — and both break as soon asanything wraps children.
validateChildrenareChildrenDifferent([MasterDetailLayout] View transitions do not work when using MDL as Hilla router layout #315)The two do not collide today, which is why this is filed separately: [MasterDetailLayout] View transitions do not work when using MDL as Hilla router layout #315's pattern puts the router
outlet inside
.Detail, andvalidateChildrenonly inspects the direct children ofMasterDetailLayout.Caveat worth stating plainly: relaxing this check is necessary but not sufficient for the wrapper
use case. A shared shell that renders
Detailinternally would pass validation and then hit #315 —the same wrapping validation objects to is the wrapping that defeats transition detection. This issue
is the smaller, self-contained half. It is worth fixing on its own because it turns working code into a
blank screen, but it does not resolve #315.
Suggested fix
The aim is to keep #317's diagnostic for genuine mistakes while letting the three slot components be
produced indirectly.
Any check on
child.typealone cannot recognise a wrapper —MyDetailis an opaque function, andwhat it renders is unknowable without rendering it. So the identity comparison cannot be repaired, only
relaxed. Two changes that together keep the helpful error without blocking composition:
Warn instead of throwing, and only in development.
console.errorwith the same message keepsthe diagnostic [MasterDetailLayout] Verify proper usage of Master / Detail wrapper components #317 wanted, costs nothing in production, and lets a wrapper work. This is what the
React ecosystem generally does for "unexpected child" checks.
Recognise the slot components by a marker rather than by identity, so a wrapper can opt in and
two module instances agree:
Symbol.foris a cross-realm registry, so this survives duplicate copies of the package, andunwrapping
type.typecoversmemoandforwardRefwith no change at the call site.Accepting
slot="detail"children the way the web component does would also resolve it, and wouldclose the gap between the two documentation sets.
Environment
@vaadin/react-components25.2.8,@vaadin/master-detail-layout25.2.8by a shared shell component rather than written out at each call site.