@@ -405,18 +405,20 @@ export function createHydrationFunctions(
405405 )
406406 let hasWarned = false
407407 while ( next ) {
408- if (
409- ( __DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ) &&
410- ! hasWarned
411- ) {
412- warn (
413- `Hydration children mismatch on` ,
414- el ,
415- `\nServer rendered element contains more child nodes than client vdom.` ,
416- )
417- hasWarned = true
408+ if ( ! isMismatchAllowed ( el , MismatchTypes . CHILDREN ) ) {
409+ if (
410+ ( __DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ) &&
411+ ! hasWarned
412+ ) {
413+ warn (
414+ `Hydration children mismatch on` ,
415+ el ,
416+ `\nServer rendered element contains more child nodes than client vdom.` ,
417+ )
418+ hasWarned = true
419+ }
420+ logMismatchError ( )
418421 }
419- logMismatchError ( )
420422
421423 // The SSRed DOM contains more nodes than it should. Remove them.
422424 const cur = next
@@ -425,14 +427,16 @@ export function createHydrationFunctions(
425427 }
426428 } else if ( shapeFlag & ShapeFlags . TEXT_CHILDREN ) {
427429 if ( el . textContent !== vnode . children ) {
428- ; ( __DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ) &&
429- warn (
430- `Hydration text content mismatch on` ,
431- el ,
432- `\n - rendered on server: ${ el . textContent } ` +
433- `\n - expected on client: ${ vnode . children as string } ` ,
434- )
435- logMismatchError ( )
430+ if ( ! isMismatchAllowed ( el , MismatchTypes . TEXT ) ) {
431+ ; ( __DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ) &&
432+ warn (
433+ `Hydration text content mismatch on` ,
434+ el ,
435+ `\n - rendered on server: ${ el . textContent } ` +
436+ `\n - expected on client: ${ vnode . children as string } ` ,
437+ )
438+ logMismatchError ( )
439+ }
436440
437441 el . textContent = vnode . children as string
438442 }
@@ -562,18 +566,20 @@ export function createHydrationFunctions(
562566 // because server rendered HTML won't contain a text node
563567 insert ( ( vnode . el = createText ( '' ) ) , container )
564568 } else {
565- if (
566- ( __DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ) &&
567- ! hasWarned
568- ) {
569- warn (
570- `Hydration children mismatch on` ,
571- container ,
572- `\nServer rendered element contains fewer child nodes than client vdom.` ,
573- )
574- hasWarned = true
569+ if ( ! isMismatchAllowed ( container , MismatchTypes . CHILDREN ) ) {
570+ if (
571+ ( __DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ) &&
572+ ! hasWarned
573+ ) {
574+ warn (
575+ `Hydration children mismatch on` ,
576+ container ,
577+ `\nServer rendered element contains fewer child nodes than client vdom.` ,
578+ )
579+ hasWarned = true
580+ }
581+ logMismatchError ( )
575582 }
576- logMismatchError ( )
577583
578584 // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
579585 patch (
@@ -637,19 +643,21 @@ export function createHydrationFunctions(
637643 slotScopeIds : string [ ] | null ,
638644 isFragment : boolean ,
639645 ) : Node | null => {
640- ; ( __DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ) &&
641- warn (
642- `Hydration node mismatch:\n- rendered on server:` ,
643- node ,
644- node . nodeType === DOMNodeTypes . TEXT
645- ? `(text)`
646- : isComment ( node ) && node . data === '['
647- ? `(start of fragment)`
648- : `` ,
649- `\n- expected on client:` ,
650- vnode . type ,
651- )
652- logMismatchError ( )
646+ if ( ! isMismatchAllowed ( node . parentElement ! , MismatchTypes . CHILDREN ) ) {
647+ ; ( __DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ) &&
648+ warn (
649+ `Hydration node mismatch:\n- rendered on server:` ,
650+ node ,
651+ node . nodeType === DOMNodeTypes . TEXT
652+ ? `(text)`
653+ : isComment ( node ) && node . data === '['
654+ ? `(start of fragment)`
655+ : `` ,
656+ `\n- expected on client:` ,
657+ vnode . type ,
658+ )
659+ logMismatchError ( )
660+ }
653661
654662 vnode . el = null
655663
@@ -747,7 +755,7 @@ function propHasMismatch(
747755 vnode : VNode ,
748756 instance : ComponentInternalInstance | null ,
749757) : boolean {
750- let mismatchType : string | undefined
758+ let mismatchType : MismatchTypes | undefined
751759 let mismatchKey : string | undefined
752760 let actual : string | boolean | null | undefined
753761 let expected : string | boolean | null | undefined
@@ -757,7 +765,8 @@ function propHasMismatch(
757765 actual = el . getAttribute ( 'class' )
758766 expected = normalizeClass ( clientValue )
759767 if ( ! isSetEqual ( toClassSet ( actual || '' ) , toClassSet ( expected ) ) ) {
760- mismatchType = mismatchKey = `class`
768+ mismatchType = MismatchTypes . CLASS
769+ mismatchKey = `class`
761770 }
762771 } else if ( key === 'style' ) {
763772 // style might be in different order, but that doesn't affect cascade
@@ -782,7 +791,8 @@ function propHasMismatch(
782791 }
783792
784793 if ( ! isMapEqual ( actualMap , expectedMap ) ) {
785- mismatchType = mismatchKey = 'style'
794+ mismatchType = MismatchTypes . STYLE
795+ mismatchKey = 'style'
786796 }
787797 } else if (
788798 ( el instanceof SVGElement && isKnownSvgAttr ( key ) ) ||
@@ -808,15 +818,15 @@ function propHasMismatch(
808818 : false
809819 }
810820 if ( actual !== expected ) {
811- mismatchType = `attribute`
821+ mismatchType = MismatchTypes . ATTRIBUTE
812822 mismatchKey = key
813823 }
814824 }
815825
816- if ( mismatchType ) {
826+ if ( mismatchType != null && ! isMismatchAllowed ( el , mismatchType ) ) {
817827 const format = ( v : any ) =>
818828 v === false ? `(not rendered)` : `${ mismatchKey } ="${ v } "`
819- const preSegment = `Hydration ${ mismatchType } mismatch on`
829+ const preSegment = `Hydration ${ MismatchTypeString [ mismatchType ] } mismatch on`
820830 const postSegment =
821831 `\n - rendered on server: ${ format ( actual ) } ` +
822832 `\n - expected on client: ${ format ( expected ) } ` +
@@ -898,3 +908,48 @@ function resolveCssVars(
898908 resolveCssVars ( instance . parent , instance . vnode , expectedMap )
899909 }
900910}
911+
912+ const allowMismatchAttr = 'data-allow-mismatch'
913+
914+ enum MismatchTypes {
915+ TEXT = 0 ,
916+ CHILDREN = 1 ,
917+ CLASS = 2 ,
918+ STYLE = 3 ,
919+ ATTRIBUTE = 4 ,
920+ }
921+
922+ const MismatchTypeString : Record < MismatchTypes , string > = {
923+ [ MismatchTypes . TEXT ] : 'text' ,
924+ [ MismatchTypes . CHILDREN ] : 'children' ,
925+ [ MismatchTypes . CLASS ] : 'class' ,
926+ [ MismatchTypes . STYLE ] : 'style' ,
927+ [ MismatchTypes . ATTRIBUTE ] : 'attribute' ,
928+ } as const
929+
930+ function isMismatchAllowed (
931+ el : Element | null ,
932+ allowedType : MismatchTypes ,
933+ ) : boolean {
934+ if (
935+ allowedType === MismatchTypes . TEXT ||
936+ allowedType === MismatchTypes . CHILDREN
937+ ) {
938+ while ( el && ! el . hasAttribute ( allowMismatchAttr ) ) {
939+ el = el . parentElement
940+ }
941+ }
942+ const allowedAttr = el && el . getAttribute ( allowMismatchAttr )
943+ if ( allowedAttr == null ) {
944+ return false
945+ } else if ( allowedAttr === '' ) {
946+ return true
947+ } else {
948+ const list = allowedAttr . split ( ',' )
949+ // text is a subset of children
950+ if ( allowedType === MismatchTypes . TEXT && list . includes ( 'children' ) ) {
951+ return true
952+ }
953+ return allowedAttr . split ( ',' ) . includes ( MismatchTypeString [ allowedType ] )
954+ }
955+ }
0 commit comments