Skip to content

Commit 895b427

Browse files
fix: react hydration mismatches (#1198)
[![PR App][icn]][demo] | Fix CX-2284 :-------------------:|:----------: We've got a few markdown components that are causing hydration mismatches when they get rendered in the Hub. This PR patches them up! ## 🧰 Changes 1. Wait until after hydration to apply syntax highlighting to the`<Code>` component 2. Prevent mermaid's default initialization on module load from racing with the client side render in `<CodeTabs>` I tried to comb through all of our custom blocks to find any others that cause these errors and only found the code blocks, mermaid charts and custom components causing issues. Fix for the custom components hydration is [up in this PR](readmeio/readme#15890) on the monorepo. ## 🧬 QA & Testing - [Broken on production][prod]. - [Working in this PR app][demo]. [demo]: https://markdown-pr-PR_NUMBER.herokuapp.com [prod]: https://SUBDOMAIN.readme.io [icn]: https://user-images.githubusercontent.com/886627/160426047-1bee9488-305a-4145-bb2b-09d8b757d38a.svg --------- Co-authored-by: Kelly Joseph Price <[email protected]>
1 parent d86abb1 commit 895b427

File tree

7 files changed

+29
-7
lines changed

7 files changed

+29
-7
lines changed

__tests__/browser/markdown.test.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
55

66
describe('visual regression tests', () => {
77
describe('rdmd syntax', () => {
8+
beforeAll(async () => {
9+
// try warming up the browser???
10+
const uri = 'http://localhost:9966/#/callouts?ci=true&darkModeDataAttribute=true';
11+
await page.goto(uri, { waitUntil: 'networkidle0' });
12+
});
13+
814
beforeEach(async () => {
915
// The ToC disappears somewhere below 1200, 1175-ish?
1016
await page.setViewport({ width: 1400, height: 800 });
11-
});
17+
}, 10000);
1218

1319
const docs = [
1420
'callouts',

components/Code/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { createRef, useContext } from 'react';
33

44
import CodeOptsContext from '../../contexts/CodeOpts';
55
import ThemeContext from '../../contexts/Theme';
6+
import useHydrated from '../../hooks/useHydrated';
67

78
// Only load CodeMirror in the browser, for SSR
89
// apps. Necessary because of people like this:
@@ -57,8 +58,9 @@ const Code = (props: CodeProps) => {
5758
const { children, lang, value } = props;
5859
const theme = useContext(ThemeContext);
5960
const copyButtons = useContext(CodeOptsContext) || props.copyButtons;
61+
const isHydrated = useHydrated();
6062

61-
const language = canonicalLanguage(lang);
63+
const language = isHydrated ? canonicalLanguage(lang) : '';
6264

6365
const codeRef = createRef<HTMLElement>();
6466

@@ -71,7 +73,7 @@ const Code = (props: CodeProps) => {
7173
const code = value ?? (Array.isArray(children) ? children[0] : children) ?? '';
7274

7375
const highlightedCode =
74-
syntaxHighlighter && typeof syntaxHighlighter === 'function' && code
76+
syntaxHighlighter && typeof syntaxHighlighter === 'function' && code && isHydrated
7577
? syntaxHighlighter(code, language, codeOpts, { mdx: true })
7678
: code;
7779

@@ -86,7 +88,6 @@ const Code = (props: CodeProps) => {
8688
ref={codeRef}
8789
className={['rdmd-code', `lang-${language}`, `theme-${theme}`].join(' ')}
8890
data-lang={language}
89-
suppressHydrationWarning={true}
9091
>
9192
{highlightedCode}
9293
</code>

components/CodeTabs/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ const CodeTabs = (props: Props) => {
2222
import('mermaid').then(module => {
2323
mermaid = module.default;
2424
mermaid.initialize({
25+
startOnLoad: false,
2526
theme: theme === 'dark' ? 'dark' : 'default',
2627
});
27-
mermaid.contentLoaded();
28+
mermaid.run({
29+
nodes: document.querySelectorAll('.mermaid-render'),
30+
});
2831
});
2932
}
3033
}, [hasMermaid, theme]);
@@ -44,7 +47,7 @@ const CodeTabs = (props: Props) => {
4447
// render single Mermaid diagram
4548
if (hasMermaid) {
4649
const value = children.props.children.props.value;
47-
return <pre className="mermaid mermaid_single">{value}</pre>;
50+
return <pre className="mermaid-render mermaid_single">{value}</pre>;
4851
}
4952

5053
return (

hooks/useHydrated/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useEffect, useState } from 'react';
2+
3+
/**
4+
* A hook that returns whether or not the component has been hydrated.
5+
* Useful for components that should only render in the browser, and not during SSR.
6+
* Waiting to render until after hydration avoids React hydration mismatches.
7+
*/
8+
export default function useHydrated(): boolean {
9+
const [isHydrated, setIsHydrated] = useState(false);
10+
useEffect(() => setIsHydrated(true), []);
11+
return isHydrated;
12+
}

processor/transform/mermaid.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const mermaidTransformer = () => (tree: Node) => {
1111
if (child.type === 'element' && child.tagName === 'code' && child.properties.lang === 'mermaid') {
1212
node.properties = {
1313
...node.properties,
14-
className: ['mermaid', ...((node.properties.className as string[]) || [])],
14+
className: ['mermaid-render', ...((node.properties.className as string[]) || [])],
1515
};
1616
}
1717
});

0 commit comments

Comments
 (0)