@estusana/tiptap-react-native-renderer
v0.1.1
Published
Lightweight TipTap JSON renderer for React Native
Maintainers
Readme
TipTap React Native Renderer
A 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.
🚀 Features
Core Rendering
- 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.
Designed for Performance & Stability
- 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.
Excellent Developer Experience
- 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.
Customization
- 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.
📦 Installation
npm install @estusana/tiptap-react-native-renderer🚀 Quick Start
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 (
<ScrollView style={{ flex: 1, padding: 20 }}>
<TipTapRenderer
content={content}
performanceConfig={{ enabled: true }}
devConfig={{ warnings: true }}
/>
</ScrollView>
);
};📋 Supported Content Types
Node Types
- Document (
doc) - Root container - Paragraph (
paragraph) - Text paragraphs with formatting - Heading (
heading) - Headings (levels 1-6) with dynamic sizing - Blockquote (
blockquote) - Quoted content with nesting support - Code Block (
codeBlock) - Code with syntax highlighting labels - Bullet List (
bulletList) - Unordered lists with custom styling - Ordered List (
orderedList) - Numbered lists with custom start numbers - List Item (
listItem) - Individual list items with proper indentation - Hard Break (
hardBreak) - Line breaks for text formatting - Horizontal Rule (
horizontalRule) - Divider lines with custom styling - Image (
image) - Images with error handling and accessibility - Table (
table) - Tables with headers and proper styling - Table Row (
tableRow) - Table rows with consistent formatting - Table Cell (
tableCell) - Table cells with alignment support - Table Header (
tableHeader) - Table headers with distinct styling
Mark Types
- Bold (
bold) - Bold text formatting - Italic (
italic) - Italic text formatting - Underline (
underline) - Underlined text - Strike (
strike) - Strikethrough text - Code (
code) - Inline code with monospace font - Link (
link) - Clickable links with React Native integration - Highlight (
highlight) - Highlighted text with custom colors - Subscript (
subscript) - Subscript text positioning - Superscript (
superscript) - Superscript text positioning
⚙️ Configuration
Complete Style Configuration
<TipTapRenderer
content={content}
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,
},
}}
/>Advanced Link Configuration
<TipTapRenderer
content={content}
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 }) => (
<TouchableOpacity onPress={() => handleCustomLink(url)}>
<Text style={customLinkStyle}>{children}</Text>
</TouchableOpacity>
),
}}
/>Image Configuration
<TipTapRenderer
content={content}
imageConfig={{
defaultStyle: {
borderRadius: 8,
marginVertical: 12,
},
onError: (error) => {
console.warn("Image failed to load:", error);
// Custom error handling or fallback
},
customImageComponent: ({ src, alt, style }) => (
<FastImage
source={{ uri: src }}
style={style}
alt={alt}
resizeMode="contain"
/>
),
}}
/>Note: The default React Native <Image> component does not support SVG files. To render SVGs, you will need to install a library like react-native-svg and provide a customImageComponent.
import { SvgUri } from "react-native-svg";
<TipTapRenderer
content={content}
imageConfig={{
onError: (error) => console.warn("Image failed to load:", error),
customImageComponent: ({ src, alt, style }) => {
if (src.endsWith(".svg")) {
return <SvgUri uri={src} style={style} accessibilityLabel={alt} />;
}
return <Image source={{ uri: src }} style={style} alt={alt} />;
},
}}
/>;Performance Monitoring
<TipTapRenderer
content={content}
performanceConfig={{
enabled: true,
renderTimeWarning: 100, // Warn if render takes > 100ms
onMetrics: (metrics) => {
console.log(
`Rendered ${metrics.nodeCount} nodes in ${metrics.renderTime}ms`
);
// Send to analytics service
analytics.track("render_performance", metrics);
},
}}
/>Development Configuration
<TipTapRenderer
content={content}
devConfig={{
warnings: __DEV__, // Enable warnings in development
performance: __DEV__, // Enable performance monitoring in development
validation: true, // Enable prop validation
}}
/>Simple Mode (High Performance)
For maximum performance with large documents or when you need minimal rendering overhead, enable simple mode:
<TipTapRenderer
content={content}
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
🎨 Custom Handlers
Custom Node Handlers
<TipTapRenderer
content={content}
customHandlers={{
paragraph: ({ node, renderNode }) => (
<View style={[defaultParagraphStyle, customParagraphStyle]}>
{node.content?.map((child, index) => (
<React.Fragment key={`${child.type}-${index}`}>
{renderNode(child)}
</React.Fragment>
))}
</View>
),
heading: ({ node, renderNode }) => {
const level = node.attrs?.level || 1;
const HeadingComponent = level === 1 ? H1 : level === 2 ? H2 : H3;
return (
<HeadingComponent>
{node.content?.map((child, index) => (
<React.Fragment key={index}>{renderNode(child)}</React.Fragment>
))}
</HeadingComponent>
);
},
}}
/>Custom Mark Handlers
<TipTapRenderer
content={content}
customMarkHandlers={{
bold: (mark, children) => (
<Text style={{ fontWeight: "900", color: "#1f2937" }}>{children}</Text>
),
link: (mark, children) => {
const url = mark.attrs?.href;
return (
<TouchableOpacity onPress={() => handleCustomLink(url)}>
<Text style={customLinkStyle}>{children}</Text>
</TouchableOpacity>
);
},
highlight: (mark, children) => (
<View style={{ backgroundColor: "#fef08a", paddingHorizontal: 4 }}>
<Text>{children}</Text>
</View>
),
}}
/>📚 API Reference
TipTapRendererProps
| Prop | Type | Required | Description |
| -------------------- | ------------------------------------- | -------- | ----------------------------------------- |
| content | TipTapDocument \| TipTapNode | ✅ | The TipTap content to render |
| customHandlers | Record<string, NodeHandler> | ❌ | Custom node handlers to override defaults |
| customMarkHandlers | Record<string, MarkHandler> | ❌ | 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<string, React.ComponentType> | ❌ | Custom components for specific node types |
StyleConfig
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;
};
}Performance Metrics
interface PerformanceMetrics {
renderTime: number; // Total render time in milliseconds
nodeCount: number; // Number of nodes rendered
timestamp: number; // Timestamp of measurement
}🎯 Advanced Examples
Complete Blog Post Renderer
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 (
<ScrollView style={styles.container}>
<TipTapRenderer
content={post.content}
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,
}}
/>
</ScrollView>
);
};Custom Table Renderer
const CustomTableRenderer = ({ content }) => {
return (
<TipTapRenderer
content={content}
customHandlers={{
table: ({ node, renderNode }) => (
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.table}>
{node.content?.map((child, index) => (
<React.Fragment key={index}>{renderNode(child)}</React.Fragment>
))}
</View>
</ScrollView>
),
tableCell: ({ node, renderNode }) => (
<View style={[styles.tableCell, { minWidth: 120 }]}>
{node.content?.map((child, index) => (
<React.Fragment key={index}>{renderNode(child)}</React.Fragment>
))}
</View>
),
}}
styleConfig={{
colors: {
primary: "#374151",
},
}}
/>
);
};🔧 Performance Guidelines
Optimization Tips
- Use Performance Monitoring: Enable
performanceConfigto identify potential bottlenecks. - Memoize Custom Handlers: If passing custom handlers as props, wrap them in
useCallbackoruseMemoto prevent unnecessary re-renders. - Optimize Images: Use appropriately sized raster images and consider lazy loading for documents with many images.
Performance Observations
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
sliceprop can be used to improve initial render times for very long content by rendering only a subset of nodes.
🤝 Contributing
This project is new and contributions are incredibly valuable. The best way you can help is by using the library and providing feedback.
How You Can Help
- 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.
📄 License
MIT License
