Skip to content

Commit af1298f

Browse files
fix: client head code causing layout trashing
1 parent 8717fb8 commit af1298f

1 file changed

Lines changed: 83 additions & 59 deletions

File tree

packages/fresh/src/runtime/client/preact_hooks_client.ts

Lines changed: 83 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Fragment, h, options as preactOptions } from "preact";
1+
import { Fragment, h, options as preactOptions, type VNode } from "preact";
22
import {
33
assetHashingHook,
44
CLIENT_NAV_ATTR,
@@ -7,11 +7,74 @@ import {
77
} from "../shared_internal.tsx";
88
import { BUILD_ID } from "@fresh/build-id";
99
import { renderToString } from "preact-render-to-string";
10-
import { useEffect } from "preact/hooks";
10+
import { useContext, useEffect } from "preact/hooks";
11+
import { HeadContext } from "../head.tsx";
1112

1213
// deno-lint-ignore no-explicit-any
1314
const options: InternalPreactOptions = preactOptions as any;
1415

16+
const PATCHED = new WeakSet<VNode>();
17+
18+
function WrappedHead(
19+
// deno-lint-ignore no-explicit-any
20+
{ originalType, props, key }: { originalType: string; props: any; key: any },
21+
) {
22+
const enabled = useContext(HeadContext);
23+
24+
useEffect(() => {
25+
if (!enabled) return;
26+
27+
const text = renderToString(h(Fragment, null, props.children));
28+
29+
if (originalType === "title") {
30+
document.title = text;
31+
return;
32+
}
33+
34+
let matched: HTMLElement | null = null;
35+
if (key) {
36+
matched = document.head.querySelector(
37+
`head [data-key="${key}"]`,
38+
) as HTMLElement ?? null;
39+
}
40+
41+
if (matched === null && props.id) {
42+
matched = document.head.querySelector(
43+
`#${props.name}`,
44+
) as HTMLElement ??
45+
null;
46+
}
47+
48+
if (matched === null) {
49+
if (originalType === "meta") {
50+
matched = document.head.querySelector(
51+
`head [name="${props.name}"]`,
52+
) as HTMLElement ?? null;
53+
} else if (originalType === "base") {
54+
matched = document.head.querySelector(originalType) ?? null;
55+
}
56+
}
57+
58+
if (matched === null) {
59+
matched = document.createElement(originalType);
60+
}
61+
62+
if (matched.textContent !== text) {
63+
matched.textContent = text;
64+
}
65+
66+
applyProps(props, matched);
67+
}, []);
68+
69+
if (enabled) {
70+
return null;
71+
}
72+
73+
const inner = h(originalType, props);
74+
PATCHED.add(inner);
75+
return inner;
76+
}
77+
1578
const oldVNodeHook = options.vnode;
1679
options.vnode = (vnode) => {
1780
assetHashingHook(vnode, BUILD_ID);
@@ -28,63 +91,24 @@ options.vnode = (vnode) => {
2891
const originalType = vnode.type;
2992

3093
if (typeof originalType === "string") {
31-
switch (originalType) {
32-
case "title":
33-
case "meta":
34-
case "link":
35-
case "script":
36-
case "style":
37-
case "base":
38-
case "noscript":
39-
case "template":
40-
// deno-lint-ignore no-explicit-any
41-
vnode.type = (props: any) => {
42-
useEffect(() => {
43-
const text = renderToString(h(Fragment, null, props.children));
44-
45-
if (originalType === "title") {
46-
document.title = text;
47-
return;
48-
}
49-
50-
let matched: HTMLElement | null = null;
51-
if (vnode.key) {
52-
matched = document.head.querySelector(
53-
`head [data-key="${vnode.key}"]`,
54-
) as HTMLElement ?? null;
55-
}
56-
57-
if (matched === null && props.id) {
58-
matched = document.head.querySelector(
59-
`#${props.name}`,
60-
) as HTMLElement ??
61-
null;
62-
}
63-
64-
if (matched === null) {
65-
if (originalType === "meta") {
66-
matched = document.head.querySelector(
67-
`head [name="${props.name}"]`,
68-
) as HTMLElement ?? null;
69-
} else if (originalType === "base") {
70-
matched = document.head.querySelector(originalType) ?? null;
71-
}
72-
}
73-
74-
if (matched === null) {
75-
matched = document.createElement(originalType);
76-
}
77-
78-
if (matched.textContent !== text) {
79-
matched.textContent = text;
80-
}
81-
82-
applyProps(props, matched);
83-
}, []);
84-
85-
return null;
86-
};
87-
break;
94+
if (!PATCHED.has(vnode)) {
95+
switch (originalType) {
96+
case "title":
97+
case "meta":
98+
case "link":
99+
case "script":
100+
case "style":
101+
case "base":
102+
case "noscript":
103+
case "template":
104+
vnode = h(WrappedHead, {
105+
originalType,
106+
props: vnode.props,
107+
key: vnode.key,
108+
// deno-lint-ignore no-explicit-any
109+
}) as any;
110+
break;
111+
}
88112
}
89113
}
90114

0 commit comments

Comments
 (0)