Skip to content

Commit adfdc79

Browse files
authored
Move next/head to Typescript (#6131)
Solves a bunch of inconsistencies in handling React elements too.
1 parent 6c49bee commit adfdc79

File tree

4 files changed

+152
-106
lines changed

4 files changed

+152
-106
lines changed

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@
3333
"git add"
3434
],
3535
"*.ts": [
36-
"tslint -c tslint.json 'packages/**/*.ts'",
36+
"tslint -c tslint.json 'packages/**/*.ts' --fix",
37+
"git add"
38+
],
39+
"*.tsx": [
40+
"tslint -c tslint.json 'packages/**/*.ts' --fix",
3741
"git add"
3842
],
3943
"packages/**/bin/*": [

packages/next-server/lib/head.js

Lines changed: 0 additions & 93 deletions
This file was deleted.

packages/next-server/lib/head.tsx

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import React from "react";
2+
import withSideEffect from "./side-effect";
3+
import { HeadManagerContext } from "./head-manager-context";
4+
5+
export function defaultHead(className = 'next-head') {
6+
return [
7+
<meta key="charSet" charSet="utf-8" className={className} />,
8+
];
9+
}
10+
11+
function onlyReactElement(
12+
list: Array<React.ReactElement<any>>,
13+
child: React.ReactChild,
14+
): Array<React.ReactElement<any>> {
15+
// React children can be "string" or "number" in this case we ignore them for backwards compat
16+
if (typeof child === "string" || typeof child === "number") {
17+
return list;
18+
}
19+
// Adds support for React.Fragment
20+
if (child.type === React.Fragment) {
21+
return list.concat(
22+
React.Children.toArray(child.props.children).reduce((
23+
fragmentList: Array<React.ReactElement<any>>,
24+
fragmentChild: React.ReactChild,
25+
): Array<React.ReactElement<any>> => {
26+
if (
27+
typeof fragmentChild === "string" ||
28+
typeof fragmentChild === "number"
29+
) {
30+
return fragmentList;
31+
}
32+
return fragmentList.concat(fragmentChild);
33+
},
34+
[]),
35+
);
36+
}
37+
return list.concat(child);
38+
}
39+
40+
const METATYPES = ["name", "httpEquiv", "charSet", "itemProp"];
41+
42+
/*
43+
returns a function for filtering head child elements
44+
which shouldn't be duplicated, like <title/>
45+
Also adds support for deduplicated `key` properties
46+
*/
47+
function unique() {
48+
const keys = new Set();
49+
const tags = new Set();
50+
const metaTypes = new Set();
51+
const metaCategories: { [metatype: string]: Set<string> } = {};
52+
53+
return (h: React.ReactElement<any>) => {
54+
if (h.key && typeof h.key !== 'number' && h.key.indexOf(".$") === 0) {
55+
if (keys.has(h.key)) return false;
56+
keys.add(h.key);
57+
return true;
58+
}
59+
switch (h.type) {
60+
case "title":
61+
case "base":
62+
if (tags.has(h.type)) return false;
63+
tags.add(h.type);
64+
break;
65+
case "meta":
66+
for (let i = 0, len = METATYPES.length; i < len; i++) {
67+
const metatype = METATYPES[i];
68+
if (!h.props.hasOwnProperty(metatype)) continue;
69+
70+
if (metatype === "charSet") {
71+
if (metaTypes.has(metatype)) return false;
72+
metaTypes.add(metatype);
73+
} else {
74+
const category = h.props[metatype];
75+
const categories = metaCategories[metatype] || new Set();
76+
if (categories.has(category)) return false;
77+
categories.add(category);
78+
metaCategories[metatype] = categories;
79+
}
80+
}
81+
break;
82+
}
83+
return true;
84+
};
85+
}
86+
87+
/**
88+
*
89+
* @param headElement List of multiple <Head> instances
90+
*/
91+
function reduceComponents(headElements: Array<React.ReactElement<any>>) {
92+
return headElements
93+
.reduce(
94+
(list: React.ReactChild[], headElement: React.ReactElement<any>) => {
95+
const headElementChildren = React.Children.toArray(
96+
headElement.props.children,
97+
);
98+
return list.concat(headElementChildren);
99+
},
100+
[],
101+
)
102+
.reduce(onlyReactElement, [])
103+
.reverse()
104+
.concat(defaultHead(''))
105+
.filter(unique())
106+
.reverse()
107+
.map((c: React.ReactElement<any>, i: number) => {
108+
const className =
109+
(c.props && c.props.className ? c.props.className + " " : "") +
110+
"next-head";
111+
const key = c.key || i;
112+
return React.cloneElement(c, { key, className });
113+
});
114+
}
115+
116+
const Effect = withSideEffect();
117+
118+
function Head({ children }: { children: React.ReactNode }) {
119+
return (
120+
<HeadManagerContext.Consumer>
121+
{(updateHead) => (
122+
<Effect
123+
reduceComponentsToState={reduceComponents}
124+
handleStateChange={updateHead}
125+
>
126+
{children}
127+
</Effect>
128+
)}
129+
</HeadManagerContext.Consumer>
130+
);
131+
}
132+
133+
Head.rewind = Effect.rewind;
134+
135+
export default Head;

packages/next-server/lib/side-effect.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,53 @@ import React, { Component } from 'react'
22

33
const isServer = typeof window === 'undefined'
44

5-
type State = React.DetailedReactHTMLElement<any, any>[] | undefined
5+
type State = Array<React.ReactElement<any>> | undefined
66

77
type SideEffectProps = {
8-
reduceComponentsToState: (components: React.ReactElement<any>[]) => State,
9-
handleStateChange?: (state: State) => void
8+
reduceComponentsToState: (components: Array<React.ReactElement<any>>) => State,
9+
handleStateChange?: (state: State) => void,
1010
}
1111

12-
export default function withSideEffect () {
12+
export default function withSideEffect() {
1313
const mountedInstances: Set<any> = new Set()
1414
let state: State
1515

16-
function emitChange (component: React.Component<SideEffectProps>) {
16+
function emitChange(component: React.Component<SideEffectProps>) {
1717
state = component.props.reduceComponentsToState([...mountedInstances])
18-
if(component.props.handleStateChange) {
18+
if (component.props.handleStateChange) {
1919
component.props.handleStateChange(state)
2020
}
2121
}
2222

2323
class SideEffect extends Component<SideEffectProps> {
2424
// Used when server rendering
25-
static rewind () {
25+
static rewind() {
2626
const recordedState = state
2727
state = undefined
2828
mountedInstances.clear()
2929
return recordedState
3030
}
3131

32-
constructor (props: any) {
32+
constructor(props: any) {
3333
super(props)
3434
if (isServer) {
3535
mountedInstances.add(this)
3636
emitChange(this)
3737
}
3838
}
39-
componentDidMount () {
39+
componentDidMount() {
4040
mountedInstances.add(this)
4141
emitChange(this)
4242
}
43-
componentDidUpdate () {
43+
componentDidUpdate() {
4444
emitChange(this)
4545
}
46-
componentWillUnmount () {
46+
componentWillUnmount() {
4747
mountedInstances.delete(this)
4848
emitChange(this)
4949
}
5050

51-
render () {
51+
render() {
5252
return null
5353
}
5454
}

0 commit comments

Comments
 (0)