Skip to content

Commit e5fd630

Browse files
committed
Fix MDX hash hydration regressions
1 parent 0139a69 commit e5fd630

File tree

3 files changed

+58
-33
lines changed

3 files changed

+58
-33
lines changed

src/components/MDX/ExpandableExample.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {IconDeepDive} from '../Icon/IconDeepDive';
1818
import {IconCodeBlock} from '../Icon/IconCodeBlock';
1919
import {Button} from '../Button';
2020
import {H4} from './Heading';
21-
import {useState} from 'react';
21+
import {Children, isValidElement, useState} from 'react';
2222
import {useLocationHash} from 'hooks/useLocationHash';
2323
import {getMDXName} from './getMDXName';
2424

@@ -28,15 +28,32 @@ interface ExpandableExampleProps {
2828
type: 'DeepDive' | 'Example';
2929
}
3030

31+
function getExpandableChildren(children: React.ReactNode) {
32+
return Children.toArray(children).filter((child) => {
33+
return !(typeof child === 'string' && child.trim() === '');
34+
});
35+
}
36+
3137
function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {
32-
if (!Array.isArray(children) || getMDXName(children[0]) !== 'h4') {
38+
const expandableChildren = getExpandableChildren(children);
39+
const titleChild = expandableChildren[0];
40+
41+
if (
42+
!isValidElement(titleChild) ||
43+
!(
44+
getMDXName(titleChild) === 'h4' ||
45+
getMDXName(titleChild) === 'H4' ||
46+
titleChild.type === H4
47+
)
48+
) {
3349
throw Error(
3450
`Expandable content ${type} is missing a corresponding title at the beginning`
3551
);
3652
}
53+
3754
const isDeepDive = type === 'DeepDive';
3855
const isExample = type === 'Example';
39-
const id = children[0].props.id;
56+
const id = titleChild.props.id;
4057
const hash = useLocationHash();
4158
const [isExpanded, setIsExpanded] = useState(false);
4259
const [isAutoExpandedDismissed, setIsAutoExpandedDismissed] = useState(false);
@@ -88,7 +105,7 @@ function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {
88105
<H4
89106
id={id}
90107
className="text-xl font-bold text-primary dark:text-primary-dark">
91-
{children[0].props.children}
108+
{titleChild.props.children}
92109
</H4>
93110
{excerpt && <div>{excerpt}</div>}
94111
</div>
@@ -120,7 +137,7 @@ function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {
120137
'dark:border-purple-60 border-purple-10 ': isDeepDive,
121138
'dark:border-yellow-60 border-yellow-50': isExample,
122139
})}>
123-
{children.slice(1)}
140+
{expandableChildren.slice(1)}
124141
</div>
125142
</details>
126143
);

src/components/MDX/TerminalBlock.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* Copyright (c) Facebook, Inc. and its affiliates.
1212
*/
1313

14-
import {isValidElement, useState, useEffect} from 'react';
14+
import {Children, isValidElement, useState, useEffect} from 'react';
1515
import * as React from 'react';
1616
import {IconTerminal} from '../Icon/IconTerminal';
1717
import {IconCopy} from 'components/Icon/IconCopy';
@@ -23,6 +23,26 @@ interface TerminalBlockProps {
2323
children: React.ReactNode;
2424
}
2525

26+
function getTerminalText(node: React.ReactNode): string {
27+
let text = '';
28+
29+
Children.forEach(node, (child) => {
30+
if (typeof child === 'string' || typeof child === 'number') {
31+
text += child;
32+
return;
33+
}
34+
35+
if (!isValidElement(child)) {
36+
return;
37+
}
38+
39+
const props = child.props as {children?: React.ReactNode} | null;
40+
text += getTerminalText(props?.children ?? null);
41+
});
42+
43+
return text;
44+
}
45+
2646
function LevelText({type}: {type: LogLevel}) {
2747
switch (type) {
2848
case 'warning':
@@ -35,17 +55,8 @@ function LevelText({type}: {type: LogLevel}) {
3555
}
3656

3757
function TerminalBlock({level = 'info', children}: TerminalBlockProps) {
38-
let message: string | undefined;
39-
if (typeof children === 'string') {
40-
message = children;
41-
} else if (
42-
isValidElement(children) &&
43-
typeof (children as React.ReactElement<{children: string}>).props
44-
.children === 'string'
45-
) {
46-
message = (children as React.ReactElement<{children: string}>).props
47-
.children;
48-
} else {
58+
const message = getTerminalText(children).trim();
59+
if (message.length === 0) {
4960
throw Error('Expected TerminalBlock children to be a plain string.');
5061
}
5162

@@ -72,7 +83,7 @@ function TerminalBlock({level = 'info', children}: TerminalBlockProps) {
7283
<button
7384
className="w-full text-start text-primary-dark dark:text-primary-dark "
7485
onClick={() => {
75-
window.navigator.clipboard.writeText(message ?? '');
86+
window.navigator.clipboard.writeText(message);
7687
setCopied(true);
7788
}}>
7889
<IconCopy className="inline-flex me-2 self-center" />{' '}

src/hooks/useLocationHash.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* LICENSE file in the root directory of this source tree.
88
*/
99

10-
import {useEffect, useState} from 'react';
10+
import {useSyncExternalStore} from 'react';
1111

1212
function getHashValue() {
1313
if (typeof window === 'undefined') {
@@ -17,20 +17,17 @@ function getHashValue() {
1717
return window.location.hash.slice(1);
1818
}
1919

20-
export function useLocationHash() {
21-
const [hash, setHash] = useState(getHashValue);
22-
23-
useEffect(() => {
24-
const updateHash = () => {
25-
setHash(getHashValue());
26-
};
20+
function subscribeToHashChange(callback: () => void) {
21+
if (typeof window === 'undefined') {
22+
return () => {};
23+
}
2724

28-
updateHash();
29-
window.addEventListener('hashchange', updateHash);
30-
return () => {
31-
window.removeEventListener('hashchange', updateHash);
32-
};
33-
}, []);
25+
window.addEventListener('hashchange', callback);
26+
return () => {
27+
window.removeEventListener('hashchange', callback);
28+
};
29+
}
3430

35-
return hash;
31+
export function useLocationHash() {
32+
return useSyncExternalStore(subscribeToHashChange, getHashValue, () => '');
3633
}

0 commit comments

Comments
 (0)