Skip to content

Commit 5589478

Browse files
authored
feat(web): browser environment assertion (#707)
# Summary Both components `EnrichedText` and `EnrichedTextInput` are using DOM - `DOMPurify`, `DOMParser`, `TipTap`. An attempt of rendering them in a non-browser environment, e.g. in SSR applications, would result in a crash. Only `DOMParser` availability check was present, but it was silent. Now we check for the browser environment before any other component logic and throw a proper error. Documentation was also updated, to explicitly point the client-only nature of the components. ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ❌ | | Android | ❌ | | Web | ✅ | ## Checklist - [x] E2E tests are passing - [ ] Required E2E tests have been added (if applicable)
1 parent d547bff commit 5589478

8 files changed

Lines changed: 54 additions & 4 deletions

docs/WEB.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,9 @@ On web, HTML is sanitized automatically with [DOMPurify](https://github.com/cure
5959
### Custom mention attributes
6060

6161
To attach custom data to a mention, use the `data-` prefix (e.g. `data-user-id`) to make sure they survive sanitization. Attributes passed to the `setMention` ref method are properly sanitized.
62+
63+
## Client-only rendering (no SSR)
64+
65+
Both `EnrichedText` and `EnrichedTextInput` are **client-only** components. They rely on browser-only APIs (`DOMParser`, `DOMPurify`, `TipTap`) and are **not designed for server-side rendering (SSR)**.
66+
67+
If your application uses SSR (Next.js, Remix, Gatsby, etc.), make sure these components only render on the client.

src/web/EnrichedText.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { useImageErrorFallback } from './useImageErrorFallback';
2020
import { usePressInteractions } from './usePressInteractions';
2121
import { adaptWebToNativeEvent } from './adaptWebToNativeEvent';
2222
import { useStableRef } from './useStableRef';
23+
import { assertBrowserEnvironment } from './assertBrowserEnvironment';
2324

2425
export const EnrichedText = memo(
2526
({
@@ -35,6 +36,8 @@ export const EnrichedText = memo(
3536
onLinkPress,
3637
onMentionPress,
3738
}: EnrichedTextProps) => {
39+
assertBrowserEnvironment('EnrichedText');
40+
3841
const containerRef = useRef<HTMLDivElement>(null);
3942

4043
useImperativeHandle(ref, () => ({

src/web/EnrichedTextInput.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
checkMentionAttributes,
8585
sanitizeMentionAttributes,
8686
} from './sanitization/htmlSanitizer';
87+
import { assertBrowserEnvironment } from './assertBrowserEnvironment';
8788

8889
function runFocused(
8990
editor: Editor,
@@ -125,6 +126,8 @@ export const EnrichedTextInput = ({
125126
htmlStyle,
126127
useHtmlNormalizer = ENRICHED_TEXT_INPUT_DEFAULT_PROPS.useHtmlNormalizer,
127128
}: EnrichedTextInputProps) => {
129+
assertBrowserEnvironment('EnrichedTextInput');
130+
128131
const tiptapContent =
129132
defaultValue != null
130133
? prepareHtmlForTiptap(defaultValue, useHtmlNormalizer)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { assertBrowserEnvironment } from '../assertBrowserEnvironment';
2+
3+
describe('assertBrowserEnvironment', () => {
4+
// jsdom provides a full DOM, so the browser APIs are present by default.
5+
test('does not throw when the DOM globals are available', () => {
6+
expect(() => assertBrowserEnvironment('EnrichedText')).not.toThrow();
7+
});
8+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
// Because of the docblock above, jsdom test environment does not exist here
5+
import { assertBrowserEnvironment } from '../assertBrowserEnvironment';
6+
7+
describe('assertBrowserEnvironment', () => {
8+
test('throws when DOM is missing', () => {
9+
expect(() => assertBrowserEnvironment('EnrichedText')).toThrow(
10+
/client-only/
11+
);
12+
});
13+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* `EnrichedText` and `EnrichedTextInput` rely on browser-only APIs (DOMParser,
3+
* DOMPurify, TipTap) and therefore cannot render without a DOM — e.g. during
4+
* server-side rendering (SSR). They are client-only components.
5+
*
6+
* This asserts a DOM is available and throws a clear error otherwise.
7+
*/
8+
export function assertBrowserEnvironment(componentName: string): void {
9+
const hasDOM =
10+
typeof window !== 'undefined' &&
11+
typeof document !== 'undefined' &&
12+
typeof DOMParser !== 'undefined' &&
13+
typeof Node !== 'undefined';
14+
15+
if (!hasDOM) {
16+
throw new Error(
17+
`[react-native-enriched-html] ${componentName} is a client-only component and cannot be rendered without a DOM. ` +
18+
`If you are running an SSR application, make sure the component is only rendered on the client.`
19+
);
20+
}
21+
}

src/web/normalization/htmlNormalizer.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -679,8 +679,6 @@ function walkNode(node: Node, out: { buf: string }): void {
679679
}
680680

681681
export function normalizeHtml(html: string): string {
682-
if (typeof DOMParser === 'undefined') return html;
683-
684682
const parser = new DOMParser();
685683
const doc = parser.parseFromString(`<body>${html}</body>`, 'text/html');
686684
const body = doc.body;

src/web/normalization/prepareHtmlForWeb.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ export function prepareHtmlForWeb(
44
html: string,
55
useHtmlNormalizer: boolean | undefined
66
): string {
7-
if (typeof DOMParser === 'undefined') return html;
8-
97
if (useHtmlNormalizer) {
108
html = normalizeHtml(html);
119
}

0 commit comments

Comments
 (0)