Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/WEB.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,9 @@ On web, HTML is sanitized automatically with [DOMPurify](https://github.com/cure
### Custom mention attributes

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.

## Client-only rendering (no SSR)

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)**.

If your application uses SSR (Next.js, Remix, Gatsby, etc.), make sure these components only render on the client.
3 changes: 3 additions & 0 deletions src/web/EnrichedText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useImageErrorFallback } from './useImageErrorFallback';
import { usePressInteractions } from './usePressInteractions';
import { adaptWebToNativeEvent } from './adaptWebToNativeEvent';
import { useStableRef } from './useStableRef';
import { assertBrowserEnvironment } from './assertBrowserEnvironment';

export const EnrichedText = memo(
({
Expand All @@ -35,6 +36,8 @@ export const EnrichedText = memo(
onLinkPress,
onMentionPress,
}: EnrichedTextProps) => {
assertBrowserEnvironment('EnrichedText');

const containerRef = useRef<HTMLDivElement>(null);

useImperativeHandle(ref, () => ({
Expand Down
3 changes: 3 additions & 0 deletions src/web/EnrichedTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {
checkMentionAttributes,
sanitizeMentionAttributes,
} from './sanitization/htmlSanitizer';
import { assertBrowserEnvironment } from './assertBrowserEnvironment';

function runFocused(
editor: Editor,
Expand Down Expand Up @@ -125,6 +126,8 @@ export const EnrichedTextInput = ({
htmlStyle,
useHtmlNormalizer = ENRICHED_TEXT_INPUT_DEFAULT_PROPS.useHtmlNormalizer,
}: EnrichedTextInputProps) => {
assertBrowserEnvironment('EnrichedTextInput');

const tiptapContent =
defaultValue != null
? prepareHtmlForTiptap(defaultValue, useHtmlNormalizer)
Expand Down
8 changes: 8 additions & 0 deletions src/web/__tests__/assertBrowserEnvironment.dom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { assertBrowserEnvironment } from '../assertBrowserEnvironment';

describe('assertBrowserEnvironment', () => {
// jsdom provides a full DOM, so the browser APIs are present by default.
test('does not throw when the DOM globals are available', () => {
expect(() => assertBrowserEnvironment('EnrichedText')).not.toThrow();
});
});
13 changes: 13 additions & 0 deletions src/web/__tests__/assertBrowserEnvironment.ssr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @jest-environment node
*/
// Because of the docblock above, jsdom test environment does not exist here
import { assertBrowserEnvironment } from '../assertBrowserEnvironment';

describe('assertBrowserEnvironment', () => {
test('throws when DOM is missing', () => {
expect(() => assertBrowserEnvironment('EnrichedText')).toThrow(
/client-only/
);
});
});
21 changes: 21 additions & 0 deletions src/web/assertBrowserEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* `EnrichedText` and `EnrichedTextInput` rely on browser-only APIs (DOMParser,
* DOMPurify, TipTap) and therefore cannot render without a DOM — e.g. during
* server-side rendering (SSR). They are client-only components.
*
* This asserts a DOM is available and throws a clear error otherwise.
*/
export function assertBrowserEnvironment(componentName: string): void {
const hasDOM =
typeof window !== 'undefined' &&
typeof document !== 'undefined' &&
typeof DOMParser !== 'undefined' &&
typeof Node !== 'undefined';

if (!hasDOM) {
throw new Error(
`[react-native-enriched-html] ${componentName} is a client-only component and cannot be rendered without a DOM. ` +
`If you are running an SSR application, make sure the component is only rendered on the client.`
);
Comment thread
hejsztynx marked this conversation as resolved.
Comment thread
Copilot marked this conversation as resolved.
}
Comment thread
hejsztynx marked this conversation as resolved.
}
Comment thread
Copilot marked this conversation as resolved.
2 changes: 0 additions & 2 deletions src/web/normalization/htmlNormalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,8 +679,6 @@ function walkNode(node: Node, out: { buf: string }): void {
}

export function normalizeHtml(html: string): string {
if (typeof DOMParser === 'undefined') return html;

const parser = new DOMParser();
const doc = parser.parseFromString(`<body>${html}</body>`, 'text/html');
const body = doc.body;
Comment thread
hejsztynx marked this conversation as resolved.
Expand Down
2 changes: 0 additions & 2 deletions src/web/normalization/prepareHtmlForWeb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ export function prepareHtmlForWeb(
html: string,
useHtmlNormalizer: boolean | undefined
): string {
if (typeof DOMParser === 'undefined') return html;

if (useHtmlNormalizer) {
html = normalizeHtml(html);
}
Expand Down