@@ -405,18 +405,20 @@ export function createHydrationFunctions(
405
405
)
406
406
let hasWarned = false
407
407
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 ( )
418
421
}
419
- logMismatchError ( )
420
422
421
423
// The SSRed DOM contains more nodes than it should. Remove them.
422
424
const cur = next
@@ -425,14 +427,16 @@ export function createHydrationFunctions(
425
427
}
426
428
} else if ( shapeFlag & ShapeFlags . TEXT_CHILDREN ) {
427
429
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
+ }
436
440
437
441
el . textContent = vnode . children as string
438
442
}
@@ -562,18 +566,20 @@ export function createHydrationFunctions(
562
566
// because server rendered HTML won't contain a text node
563
567
insert ( ( vnode . el = createText ( '' ) ) , container )
564
568
} 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 ( )
575
582
}
576
- logMismatchError ( )
577
583
578
584
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
579
585
patch (
@@ -637,19 +643,21 @@ export function createHydrationFunctions(
637
643
slotScopeIds : string [ ] | null ,
638
644
isFragment : boolean ,
639
645
) : 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
+ }
653
661
654
662
vnode . el = null
655
663
@@ -747,7 +755,7 @@ function propHasMismatch(
747
755
vnode : VNode ,
748
756
instance : ComponentInternalInstance | null ,
749
757
) : boolean {
750
- let mismatchType : string | undefined
758
+ let mismatchType : MismatchTypes | undefined
751
759
let mismatchKey : string | undefined
752
760
let actual : string | boolean | null | undefined
753
761
let expected : string | boolean | null | undefined
@@ -757,7 +765,8 @@ function propHasMismatch(
757
765
actual = el . getAttribute ( 'class' )
758
766
expected = normalizeClass ( clientValue )
759
767
if ( ! isSetEqual ( toClassSet ( actual || '' ) , toClassSet ( expected ) ) ) {
760
- mismatchType = mismatchKey = `class`
768
+ mismatchType = MismatchTypes . CLASS
769
+ mismatchKey = `class`
761
770
}
762
771
} else if ( key === 'style' ) {
763
772
// style might be in different order, but that doesn't affect cascade
@@ -782,7 +791,8 @@ function propHasMismatch(
782
791
}
783
792
784
793
if ( ! isMapEqual ( actualMap , expectedMap ) ) {
785
- mismatchType = mismatchKey = 'style'
794
+ mismatchType = MismatchTypes . STYLE
795
+ mismatchKey = 'style'
786
796
}
787
797
} else if (
788
798
( el instanceof SVGElement && isKnownSvgAttr ( key ) ) ||
@@ -808,15 +818,15 @@ function propHasMismatch(
808
818
: false
809
819
}
810
820
if ( actual !== expected ) {
811
- mismatchType = `attribute`
821
+ mismatchType = MismatchTypes . ATTRIBUTE
812
822
mismatchKey = key
813
823
}
814
824
}
815
825
816
- if ( mismatchType ) {
826
+ if ( mismatchType != null && ! isMismatchAllowed ( el , mismatchType ) ) {
817
827
const format = ( v : any ) =>
818
828
v === false ? `(not rendered)` : `${ mismatchKey } ="${ v } "`
819
- const preSegment = `Hydration ${ mismatchType } mismatch on`
829
+ const preSegment = `Hydration ${ MismatchTypeString [ mismatchType ] } mismatch on`
820
830
const postSegment =
821
831
`\n - rendered on server: ${ format ( actual ) } ` +
822
832
`\n - expected on client: ${ format ( expected ) } ` +
@@ -898,3 +908,48 @@ function resolveCssVars(
898
908
resolveCssVars ( instance . parent , instance . vnode , expectedMap )
899
909
}
900
910
}
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