Skip to content

Commit 4eafef1

Browse files
committedFeb 21, 2025
feat(react): simplified API, useScript()
1 parent 68b8907 commit 4eafef1

18 files changed

+125
-320
lines changed
 

‎packages/react/build.config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default defineBuildConfig({
88
{ input: 'src/index', name: 'index' },
99
{ input: 'src/server', name: 'server' },
1010
{ input: 'src/client', name: 'client' },
11-
{ input: 'src/types/index', name: 'types' },
11+
{ input: 'src/utils', name: 'utils' },
12+
{ input: 'src/plugins', name: 'plugins' },
1213
],
1314
})

‎packages/react/package.json

+14-6
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@
2828
"types": "./dist/client.d.ts",
2929
"import": "./dist/client.mjs"
3030
},
31-
"./types": {
32-
"types": "./dist/types.d.ts",
33-
"import": "./dist/types.mjs"
31+
"./utils": {
32+
"types": "./dist/utils.d.ts",
33+
"import": "./dist/utils.mjs"
34+
},
35+
"./plugins": {
36+
"types": "./dist/plugins.d.ts",
37+
"import": "./dist/plugins.mjs"
3438
}
3539
},
3640
"main": "dist/index.mjs",
@@ -44,16 +48,20 @@
4448
"client": [
4549
"dist/client"
4650
],
47-
"types": [
48-
"dist/types"
51+
"plugins": [
52+
"dist/plugins"
53+
],
54+
"utils": [
55+
"dist/utils"
4956
]
5057
}
5158
},
5259
"files": [
5360
"client.d.ts",
5461
"dist",
62+
"plugins.d.ts",
5563
"server.d.ts",
56-
"types.d.ts"
64+
"utils.d.ts"
5765
],
5866
"scripts": {
5967
"build": "unbuild .",

‎packages/react/plugins.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './dist/plugins'

‎packages/react/src/client.ts

+4-13
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
11
import type { ReactNode } from 'react'
2-
import type { CreateClientHeadOptions, MergeHead } from 'unhead/types'
3-
import type { MaybeComputedRef, ReactiveHead, ReactUnhead } from './types'
2+
import type { CreateClientHeadOptions, Unhead } from 'unhead/types'
43
import { createElement } from 'react'
54
import { createHead as _createHead, createDebouncedFn, renderDOMHead } from 'unhead/client'
65
import { UnheadContext } from './context'
76

87
export { renderDOMHead } from 'unhead/client'
98

10-
export function createHead<T extends MergeHead>(options: Omit<CreateClientHeadOptions, 'propResolvers'> = {}): ReactUnhead<T> {
11-
const head = _createHead<MaybeComputedRef<ReactiveHead<T>>>({
9+
export function createHead(options: CreateClientHeadOptions = {}): Unhead {
10+
const head = _createHead({
1211
domOptions: {
1312
render: createDebouncedFn(() => renderDOMHead(head), fn => setTimeout(fn, 0)),
1413
},
1514
...options,
16-
propResolvers: [
17-
(_: string, r: any) => {
18-
if (typeof r === 'object' && 'current' in r) {
19-
return r.current
20-
}
21-
return r
22-
},
23-
],
24-
}) as ReactUnhead<T>
15+
})
2516
return head
2617
}
2718

‎packages/react/src/composables.ts

+89-31
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,111 @@
1-
import type { ActiveHeadEntry, HeadEntryOptions, HeadSafe, MergeHead } from 'unhead/types'
2-
import type { UseHeadInput, UseSeoMetaInput } from './types'
3-
import { useContext, useEffect } from 'react'
4-
import { FlatMetaPlugin, SafeInputPlugin } from 'unhead/plugins'
1+
import type {
2+
ActiveHeadEntry,
3+
HeadEntryOptions,
4+
HeadSafe,
5+
Unhead,
6+
UseHeadInput,
7+
UseScriptInput,
8+
UseScriptOptions,
9+
UseScriptReturn,
10+
UseSeoMetaInput,
11+
} from 'unhead/types'
12+
import { useContext, useEffect, useState } from 'react'
13+
import { useHead as baseHead, useHeadSafe as baseHeadSafe, useSeoMeta as baseSeoMeta, useScript as baseUseScript } from 'unhead'
514
import { UnheadContext } from './context'
615

7-
export function useUnhead() {
16+
export function useUnhead(): Unhead {
817
// fallback to react context
9-
const instance = useContext(UnheadContext)
18+
const instance = useContext<Unhead | null>(UnheadContext)
1019
if (!instance) {
1120
throw new Error('useHead() was called without provide context.')
1221
}
1322
return instance
1423
}
1524

16-
export function useHead<T extends MergeHead>(input: UseHeadInput<T>, options: HeadEntryOptions = {}): ActiveHeadEntry<UseHeadInput<T>> {
17-
const head = options.head || useUnhead()
18-
// @ts-expect-error untyped
19-
const entry = head.push(input, options)
20-
25+
function withSideEffects<T extends ActiveHeadEntry<any>>(input: any, options: any, fn: any): T {
26+
const unhead = options.head || useUnhead()
27+
const [entry] = useState<T>(() => fn(unhead, input, options))
2128
useEffect(() => {
22-
// @ts-expect-error untyped
2329
entry.patch(input)
2430
}, [input])
25-
2631
useEffect(() => {
2732
return () => {
28-
entry?.dispose()
33+
// unmount
34+
entry.dispose()
2935
}
3036
}, [])
31-
32-
// @ts-expect-error untyped
3337
return entry
3438
}
3539

36-
export function useHeadSafe(input: HeadSafe, options: HeadEntryOptions = {}): ActiveHeadEntry<HeadSafe> {
37-
const head = options.head || useUnhead()
38-
head.use(SafeInputPlugin)
39-
options._safe = true
40-
// @ts-expect-error untyped
41-
return useHead(input, options)
40+
export function useHead(input: UseHeadInput = {}, options: HeadEntryOptions = {}): ActiveHeadEntry<UseHeadInput> {
41+
return withSideEffects(input, options, baseHead)
4242
}
4343

44-
export function useSeoMeta(input: UseSeoMetaInput, options: HeadEntryOptions = {}): ActiveHeadEntry<any> {
45-
const head = options.head || useUnhead()
46-
head.use(FlatMetaPlugin)
47-
const { title, titleTemplate, ...meta } = input
48-
return useHead({
49-
title,
50-
titleTemplate,
51-
_flatMeta: meta,
52-
}, options)
44+
export function useHeadSafe(input: HeadSafe = {}, options: HeadEntryOptions = {}): ActiveHeadEntry<HeadSafe> {
45+
return withSideEffects<ActiveHeadEntry<HeadSafe>>(input, options, baseHeadSafe)
46+
}
47+
48+
export function useSeoMeta(input: UseSeoMetaInput = {}, options: HeadEntryOptions = {}): ActiveHeadEntry<UseSeoMetaInput> {
49+
return withSideEffects<ActiveHeadEntry<UseSeoMetaInput>>(input, options, baseSeoMeta)
50+
}
51+
52+
export function useScript<T extends Record<symbol | string, any> = Record<symbol | string, any>>(_input: UseScriptInput, _options?: UseScriptOptions<T>): UseScriptReturn<T> {
53+
const input = (typeof _input === 'string' ? { src: _input } : _input) as UseScriptInput
54+
const options = _options || {} as UseScriptOptions<T>
55+
const head = options?.head || useUnhead()
56+
options.head = head
57+
58+
const mountCbs: (() => void)[] = []
59+
let isMounted = false
60+
useEffect(() => {
61+
isMounted = true
62+
mountCbs.forEach(i => i())
63+
return () => {
64+
isMounted = false
65+
}
66+
}, [])
67+
68+
if (typeof options.trigger === 'undefined') {
69+
options.trigger = (load) => {
70+
if (isMounted) {
71+
load()
72+
}
73+
else {
74+
mountCbs.push(load)
75+
}
76+
}
77+
}
78+
// @ts-expect-error untyped
79+
const script = baseUseScript(head, input as BaseUseScriptInput, options)
80+
// Note: we don't remove scripts on unmount as it's not a common use case and reloading the script may be expensive
81+
const sideEffects: (() => void)[] = []
82+
useEffect(() => {
83+
return () => {
84+
script._triggerAbortController?.abort()
85+
sideEffects.forEach(i => i())
86+
}
87+
}, [])
88+
const _registerCb = (key: 'loaded' | 'error', cb: any) => {
89+
let i: number | null
90+
const destroy = () => {
91+
// avoid removing the wrong callback
92+
if (i) {
93+
script._cbs[key]?.splice(i - 1, 1)
94+
i = null
95+
}
96+
}
97+
mountCbs.push(() => {
98+
if (!script._cbs[key]) {
99+
cb(script.instance)
100+
return () => {}
101+
}
102+
i = script._cbs[key].push(cb)
103+
sideEffects.push(destroy)
104+
return destroy
105+
})
106+
}
107+
// if we have a scope we should make these callbacks reactive
108+
script.onLoaded = (cb: (instance: T) => void | Promise<void>) => _registerCb('loaded', cb)
109+
script.onError = (cb: (err?: Error) => void | Promise<void>) => _registerCb('error', cb)
110+
return script
53111
}

‎packages/react/src/context.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import type { Unhead } from 'unhead/types'
22
import { createContext } from 'react'
33

4-
export const UnheadContext = createContext<Unhead<any> | null>(null)
4+
export const UnheadContext = createContext<Unhead | null>(null)

‎packages/react/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { hookImports } from './autoImports'
22
export { Head } from './components'
3-
export { useHead, useHeadSafe, useSeoMeta, useUnhead } from './composables'
3+
export { useHead, useHeadSafe, useScript, useSeoMeta, useUnhead } from './composables'

‎packages/react/src/plugins.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from 'unhead/plugins'

‎packages/react/src/server.ts

+3-19
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,10 @@
11
import type { ReactNode } from 'react'
2-
import type { CreateServerHeadOptions, MergeHead } from 'unhead/types'
3-
import type { MaybeComputedRef, ReactiveHead, ReactUnhead } from './types'
2+
import type { Unhead } from 'unhead/types'
43
import { createElement } from 'react'
5-
import { createHead as _createHead } from 'unhead/server'
64
import { UnheadContext } from './context'
75

8-
export { extractUnheadInputFromHtml, renderSSRHead, type SSRHeadPayload, transformHtmlTemplate } from 'unhead/server'
6+
export { createHead, extractUnheadInputFromHtml, renderSSRHead, type SSRHeadPayload, transformHtmlTemplate } from 'unhead/server'
97

10-
export function createHead<T extends MergeHead>(options: Omit<CreateServerHeadOptions, 'propResolvers'> = {}): ReactUnhead<T> {
11-
return _createHead<MaybeComputedRef<ReactiveHead<T>>>({
12-
...options,
13-
propResolvers: [
14-
(_: string, r: any) => {
15-
if (typeof r === 'object' && 'current' in r) {
16-
return r.current
17-
}
18-
return r
19-
},
20-
],
21-
}) as ReactUnhead<T>
22-
}
23-
24-
export function UnheadProvider({ children, value }: { children: ReactNode, value: ReturnType<typeof createHead> }) {
8+
export function UnheadProvider({ children, value }: { children: ReactNode, value: Unhead }) {
259
return createElement(UnheadContext.Provider, { value }, children)
2610
}

‎packages/react/src/types/index.ts

-4
This file was deleted.

‎packages/react/src/types/safeSchema.ts

-14
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.