Lightweight TipTap JSON renderer for React Native
npm install @estusana/tiptap-react-native-rendererA feature-rich and highly customizable React Native renderer for TipTap editor content. This library is designed to provide a flexible bridge between TipTap's JSON output and native React Native components.
> ### ⚠️ Current Status: Beta
>
> This is a new library and should be considered beta. It has been developed with a focus on performance and stability, but it has not yet been extensively tested in a large-scale production environment.
>
> iOS testing, in particular, is needed. Community feedback, bug reports, and contributions are highly welcome as we work to make this library ready for production use.
- Broad Node Support: Renders most standard TipTap nodes, including paragraphs, headings, lists, blockquotes, code blocks, images, and tables.
- Rich Text Formatting: Supports common text marks like bold, italic, underline, strikethrough, links, highlights, subscript, and superscript.
- Advanced Rendering: Handles nested blockquotes, tables, and images with configurable fallbacks.
- Performance-Focused: Uses React.memo, memoized handlers, and efficient key generation to optimize rendering performance.
- Error Boundaries: Includes component-level error boundaries to help prevent a single faulty node from crashing the entire render view.
- Zero TipTap Dependencies: The renderer is self-contained and does not require any TipTap packages to function.
- TypeScript First: Written entirely in TypeScript with comprehensive type definitions.
- JSDoc Documentation: The API is documented with examples and usage guidance.
- Development Warnings: Provides helpful console warnings for common issues and performance tips in development builds.
- Flexible Theming: Configure colors, typography, and spacing to match your app's design system.
- Custom Components & Handlers: Override the rendering for any node or mark type to implement custom logic or use your own components.
- Simple Mode: A lightweight rendering mode designed for maximum performance, ideal for previews or very large documents.
``bash`
npm install @estusana/tiptap-react-native-renderer
`tsx
import React from "react";
import { ScrollView } from "react-native";
import { TipTapRenderer } from "@estusana/tiptap-react-native-renderer";
const MyComponent = () => {
const content = {
type: "doc",
content: [
{
type: "heading",
attrs: { level: 1 },
content: [{ type: "text", text: "Welcome to TipTap React Native" }],
},
{
type: "paragraph",
content: [
{ type: "text", text: "This is a " },
{ type: "text", text: "bold", marks: [{ type: "bold" }] },
{ type: "text", text: " and " },
{ type: "text", text: "italic", marks: [{ type: "italic" }] },
{ type: "text", text: " text with a " },
{
type: "text",
text: "link",
marks: [{ type: "link", attrs: { href: "https://tiptap.dev" } }],
},
{ type: "text", text: "." },
],
},
],
};
return (
performanceConfig={{ enabled: true }}
devConfig={{ warnings: true }}
/>
);
};
`
- Document (doc) - Root containerparagraph
- Paragraph () - Text paragraphs with formattingheading
- Heading () - Headings (levels 1-6) with dynamic sizingblockquote
- Blockquote () - Quoted content with nesting supportcodeBlock
- Code Block () - Code with syntax highlighting labelsbulletList
- Bullet List () - Unordered lists with custom stylingorderedList
- Ordered List () - Numbered lists with custom start numberslistItem
- List Item () - Individual list items with proper indentationhardBreak
- Hard Break () - Line breaks for text formattinghorizontalRule
- Horizontal Rule () - Divider lines with custom stylingimage
- Image () - Images with error handling and accessibilitytable
- Table () - Tables with headers and proper stylingtableRow
- Table Row () - Table rows with consistent formattingtableCell
- Table Cell () - Table cells with alignment supporttableHeader
- Table Header () - Table headers with distinct styling
- Bold (bold) - Bold text formattingitalic
- Italic () - Italic text formattingunderline
- Underline () - Underlined textstrike
- Strike () - Strikethrough textcode
- Code () - Inline code with monospace fontlink
- Link () - Clickable links with React Native integrationhighlight
- Highlight () - Highlighted text with custom colorssubscript
- Subscript () - Subscript text positioningsuperscript
- Superscript () - Superscript text positioning
`tsx`
styleConfig={{
colors: {
primary: "#2563eb",
secondary: "#64748b",
background: "#ffffff",
text: "#1f2937",
link: "#3b82f6",
highlight: "#fef08a",
codeBackground: "#f1f5f9",
blockquoteBackground: "#f8fafc",
horizontalRule: "#e5e7eb",
},
typography: {
fontFamily: "System",
fontSize: 16,
lineHeight: 24,
headingFontFamily: "System-Bold",
codeFontFamily: "Courier",
},
spacing: {
paragraph: 16,
heading: 20,
list: 12,
blockquote: 16,
codeBlock: 16,
},
}}
/>
`tsx`
linkConfig={{
openInBrowser: true,
onPress: (url) => {
console.log("Link pressed:", url);
// Custom analytics or validation
if (url.includes("unsafe")) {
Alert.alert("Warning", "This link may be unsafe");
return;
}
},
customLinkComponent: ({ url, children }) => (
),
}}
/>
`tsx`
imageConfig={{
defaultStyle: {
borderRadius: 8,
marginVertical: 12,
},
onError: (error) => {
console.warn("Image failed to load:", error);
// Custom error handling or fallback
},
customImageComponent: ({ src, alt, style }) => (
style={style}
alt={alt}
resizeMode="contain"
/>
),
}}
/>
Note: The default React Native component does not support SVG files. To render SVGs, you will need to install a library like react-native-svg and provide a customImageComponent.
`tsx
import { SvgUri } from "react-native-svg";
imageConfig={{
onError: (error) => console.warn("Image failed to load:", error),
customImageComponent: ({ src, alt, style }) => {
if (src.endsWith(".svg")) {
return
}
return
},
}}
/>;
`
`tsxRendered ${metrics.nodeCount} nodes in ${metrics.renderTime}ms
performanceConfig={{
enabled: true,
renderTimeWarning: 100, // Warn if render takes > 100ms
onMetrics: (metrics) => {
console.log(
`
);
// Send to analytics service
analytics.track("render_performance", metrics);
},
}}
/>
`tsx`
devConfig={{
warnings: __DEV__, // Enable warnings in development
performance: __DEV__, // Enable performance monitoring in development
validation: true, // Enable prop validation
}}
/>
For maximum performance with large documents or when you need minimal rendering overhead, enable simple mode:
`tsx`
simpleMode={{
enabled: true,
textStyle: { fontSize: 16, color: "#333" },
linkStyle: { fontSize: 16, color: "#007AFF" },
spacing: 10,
}}
/>
Simple mode features:
- Lightweight rendering: Minimal component overhead and optimized for speed
- Essential nodes only: Supports paragraphs, headings, blockquotes, lists, and basic text formatting
- Reduced complexity: No extensions, image loading states, or advanced features
- Customizable styling: Basic text and link styling options
- Still extensible: Custom handlers can still override simple mode behaviors
Perfect for:
- Preview modes
- Large document rendering
- Performance-critical applications
- Simple content display
`tsx
customHandlers={{
paragraph: ({ node, renderNode }) => (
{node.content?.map((child, index) => (
}>
{renderNode(child)}
))}
),
heading: ({ node, renderNode }) => {
const level = node.attrs?.level || 1;
const HeadingComponent = level === 1 ? H1 : level === 2 ? H2 : H3;
return (
{node.content?.map((child, index) => (
))}
);
},
}}
/>
`
`tsx`
customMarkHandlers={{
bold: (mark, children) => (
),
link: (mark, children) => {
const url = mark.attrs?.href;
return (
);
},
highlight: (mark, children) => (
),
}}
/>
| Prop | Type | Required | Description |
| -------------------- | ------------------------------------- | -------- | ----------------------------------------- |
| content | TipTapDocument \| TipTapNode | ✅ | The TipTap content to render |customHandlers
| | Record | ❌ | Custom node handlers to override defaults |customMarkHandlers
| | Record | ❌ | Custom mark handlers to override defaults |styleConfig
| | StyleConfig | ❌ | Style configuration for theming |linkConfig
| | LinkConfig | ❌ | Link handling configuration |imageConfig
| | ImageConfig | ❌ | Image handling configuration |performanceConfig
| | PerformanceConfig | ❌ | Performance monitoring configuration |devConfig
| | DevConfig | ❌ | Development mode configuration |customComponents
| | Record | ❌ | Custom components for specific node types |
`tsx`
interface StyleConfig {
colors?: {
primary?: string;
secondary?: string;
background?: string;
text?: string;
link?: string;
highlight?: string;
codeBackground?: string;
blockquoteBackground?: string;
horizontalRule?: string;
};
typography?: {
fontFamily?: string;
fontSize?: number;
lineHeight?: number;
headingFontFamily?: string;
codeFontFamily?: string;
};
spacing?: {
paragraph?: number;
heading?: number;
list?: number;
blockquote?: number;
codeBlock?: number;
};
}
`tsx`
interface PerformanceMetrics {
renderTime: number; // Total render time in milliseconds
nodeCount: number; // Number of nodes rendered
timestamp: number; // Timestamp of measurement
}
`tsx
const BlogPostRenderer = ({ post }) => {
const handleLinkPress = (url: string) => {
if (url.startsWith("/")) {
// Internal navigation
navigation.navigate("Article", { slug: url.slice(1) });
} else {
// External link
Linking.openURL(url);
}
};
const trackPerformance = (metrics: PerformanceMetrics) => {
if (metrics.renderTime > 200) {
analytics.track("slow_render", {
renderTime: metrics.renderTime,
nodeCount: metrics.nodeCount,
postId: post.id,
});
}
};
return (
styleConfig={{
colors: {
primary: "#1a202c",
link: "#3182ce",
highlight: "#faf089",
codeBackground: "#f7fafc",
},
typography: {
fontSize: 18,
lineHeight: 28,
fontFamily: "Georgia",
},
spacing: {
paragraph: 20,
heading: 24,
},
}}
linkConfig={{
onPress: handleLinkPress,
openInBrowser: false,
}}
imageConfig={{
defaultStyle: {
borderRadius: 12,
marginVertical: 16,
},
onError: () => {
// Track image loading errors
analytics.track("image_load_error", { postId: post.id });
},
}}
performanceConfig={{
enabled: true,
onMetrics: trackPerformance,
renderTimeWarning: 200,
}}
/>
);
};
`
`tsx`
const CustomTableRenderer = ({ content }) => {
return (
customHandlers={{
table: ({ node, renderNode }) => (
{node.content?.map((child, index) => (
))}
),
tableCell: ({ node, renderNode }) => (
{node.content?.map((child, index) => (
))}
),
}}
styleConfig={{
colors: {
primary: "#374151",
},
}}
/>
);
};
1. Use Performance Monitoring: Enable performanceConfig to identify potential bottlenecks.useCallback
2. Memoize Custom Handlers: If passing custom handlers as props, wrap them in or useMemo to prevent unnecessary re-renders.
3. Optimize Images: Use appropriately sized raster images and consider lazy loading for documents with many images.
While more extensive benchmarking is needed, the design of this library aims for high performance:
- Simple Mode: This mode is designed to offer a significant performance improvement for large documents by using a minimal component tree.
- Document Slicing: The slice` prop can be used to improve initial render times for very long content by rendering only a subset of nodes.
This project is new and contributions are incredibly valuable. The best way you can help is by using the library and providing feedback.
- Testing on iOS: The library has primarily been tested on Android. Testing on various iOS devices and versions is a top priority.
- Reporting Bugs: If you find an issue, please open a detailed bug report on GitHub.
- Performance Testing: Share your results using the built-in performance monitor on large or complex documents.
- Pull Requests: Feel free to fix bugs or add features. Please open an issue to discuss significant changes first.
MIT License