1
1
import type { ReactNode } from 'react'
2
2
import type { ActiveHeadEntry , ResolvableHead as UseHeadInput } from 'unhead/types'
3
- import React , { useCallback , useEffect , useRef } from 'react'
3
+ import React , { useCallback , useEffect , useMemo , useRef } from 'react'
4
4
import { HasElementTags , TagsWithInnerContent , ValidHeadTags } from 'unhead/utils'
5
5
import { useUnhead } from './composables'
6
6
@@ -11,40 +11,59 @@ interface HeadProps {
11
11
12
12
const Head : React . FC < HeadProps > = ( { children, titleTemplate } ) => {
13
13
const head = useUnhead ( )
14
+ const headRef = useRef < ActiveHeadEntry < UseHeadInput > | null > ( null )
15
+
16
+ // Process children only when they change
17
+ const processedElements = useMemo ( ( ) =>
18
+ React . Children . toArray ( children ) . filter ( React . isValidElement ) , [ children ] )
14
19
15
20
const getHeadChanges = useCallback ( ( ) => {
16
21
const input : UseHeadInput = {
17
22
titleTemplate,
18
23
}
19
- const elements = React . Children . toArray ( children ) . filter ( React . isValidElement )
20
24
21
- elements . forEach ( ( element : React . ReactElement ) => {
22
- const { type, props } = element
25
+ for ( const element of processedElements ) {
26
+ const reactElement = element as React . ReactElement
27
+ const { type, props } = reactElement
28
+ const tagName = String ( type )
23
29
24
- if ( ! ValidHeadTags . has ( type as string ) ) {
25
- return
30
+ if ( ! ValidHeadTags . has ( tagName ) ) {
31
+ continue
26
32
}
33
+
27
34
const data : Record < string , any > = { ...( typeof props === 'object' ? props : { } ) }
28
- if ( TagsWithInnerContent . has ( type as string ) && data . children ) {
29
- data [ type === 'script' ? 'innerHTML' : 'textContent' ] = Array . isArray ( data . children ) ? data . children . map ( String ) . join ( '' ) : data . children
35
+
36
+ if ( TagsWithInnerContent . has ( tagName ) && data . children ) {
37
+ const contentKey = tagName === 'script' ? 'innerHTML' : 'textContent'
38
+ data [ contentKey ] = Array . isArray ( data . children )
39
+ ? data . children . map ( String ) . join ( '' )
40
+ : String ( data . children )
30
41
}
31
42
delete data . children
32
- if ( HasElementTags . has ( type as string ) ) {
33
- input [ type as 'meta' ] = input [ type as 'meta' ] || [ ]
34
- // @ts -expect-error untyped
35
- input [ type as 'meta' ] ! . push ( data )
43
+ if ( HasElementTags . has ( tagName ) ) {
44
+ const key = tagName as keyof UseHeadInput
45
+ if ( ! Array . isArray ( input [ key ] ) ) {
46
+ input [ key ] = [ ]
47
+ }
48
+ ( input [ key ] as any [ ] ) ! . push ( data )
36
49
}
37
50
else {
38
- // @ts -expect-error untyped
39
- input [ type ] = data
51
+ // For singleton tags (title, base, etc.)
52
+ input [ tagName as keyof UseHeadInput ] = data
40
53
}
41
- } )
54
+ }
55
+
42
56
return input
43
- } , [ children , titleTemplate ] )
57
+ } , [ processedElements , titleTemplate ] )
58
+
59
+ const headChanges = useMemo ( ( ) => getHeadChanges ( ) , [ getHeadChanges ] )
44
60
45
- const headRef = useRef < ActiveHeadEntry < any > | null > (
46
- head . push ( getHeadChanges ( ) ) ,
47
- )
61
+ if ( ! headRef . current ) {
62
+ headRef . current = head . push ( headChanges )
63
+ }
64
+ else {
65
+ headRef . current . patch ( headChanges )
66
+ }
48
67
49
68
useEffect ( ( ) => {
50
69
return ( ) => {
@@ -55,10 +74,6 @@ const Head: React.FC<HeadProps> = ({ children, titleTemplate }) => {
55
74
}
56
75
} , [ ] )
57
76
58
- useEffect ( ( ) => {
59
- headRef . current ?. patch ( getHeadChanges ( ) )
60
- } , [ getHeadChanges ] )
61
-
62
77
return null
63
78
}
64
79
0 commit comments