rn-markdown-editor
v1.0.20
Published
A fully-featured React Native Markdown Editor with toolbar, renderer, and themeable UI components
Maintainers
Readme
rn-markdown-editor
A fully-featured React Native Markdown Editor with a customisable toolbar, rich renderer, full-screen image viewer, and a complete UI component kit — all in one package. Works with Expo and bare React Native on iOS, Android, and Web.
Table of Contents
Installation
npm install rn-markdown-editor
# or
yarn add rn-markdown-editorPeer Dependencies
Install these if not already in your project:
npx expo install react react-native react-native-svg nativewind @react-native-async-storage/async-storage expo-file-system expo-media-library expo-video react-native-gesture-handler react-native-reanimatedNote:
@expo/vector-iconsis no longer required. All icons are rendered viareact-native-svgusing the built-inIconscomponent.
Import Paths
The package supports sub-path imports so you can import from granular entry points:
// ─── Root (everything) ───────────────────────────────────────────
import { MarkdownEditor, Button, useColors } from "rn-markdown-editor";
// ─── Components only ─────────────────────────────────────────────
import { MarkdownEditor, Button, Badge } from "rn-markdown-editor/components";
// ─── Hooks only ──────────────────────────────────────────────────
import { useColors, useDebouncedInput } from "rn-markdown-editor/hooks";
// ─── Types only ──────────────────────────────────────────────────
import type { ToolbarItem, ThemeColors } from "rn-markdown-editor/types";
import { DEFAULT_TOOLBAR_ITEMS } from "rn-markdown-editor/types";
// ─── Contexts only ───────────────────────────────────────────────
import { ThemeProvider, useTheme } from "rn-markdown-editor/contexts";Exported Modules Map
| Sub-path | Exports |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| rn-markdown-editor | Everything below |
| rn-markdown-editor/components | Badge, Button, Input, Icons, ImageViewer, MarkdownTable, MarkdownEditor, MarkdownRenderer, Skeleton + all prop types |
| rn-markdown-editor/hooks | useColors, useDebouncedInput, useDisclosure, useMergeState, themeColors |
| rn-markdown-editor/types | DEFAULT_TOOLBAR_ITEMS, DefaultToolbarItemId, ToolbarItem, ThemeColors, ThemeContextType |
| rn-markdown-editor/contexts | ThemeProvider, useTheme, ThemeContextType, ThemeProviderProps, CustomThemeColors |
Root also exports: defaultLightColors, defaultDarkColors, ThemeColors, ThemeProviderProps, CustomThemeColors.
Quick Start
import {
ThemeProvider,
MarkdownEditor,
MarkdownRenderer,
} from "rn-markdown-editor";
import { useState } from "react";
import { View } from "react-native";
export default function App() {
const [text, setText] = useState("# Hello world\n\nStart writing…");
return (
<ThemeProvider>
<View style={{ flex: 1, padding: 16 }}>
<MarkdownEditor value={text} onChangeText={setText} />
<MarkdownRenderer body={text} />
</View>
</ThemeProvider>
);
}Theme System
ThemeProvider
File: src/contexts/ThemeContext.tsx
Wraps your app and provides light/dark theme context. Persists the user's choice to AsyncStorage and falls back to the system colour scheme on first launch. On web it toggles .dark on <html>; on native it syncs with NativeWind's setColorScheme.
Props (ThemeProviderProps)
| Prop | Type | Default | Description |
| ------------- | ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------ |
| children | ReactNode | — | Required. |
| lightColors | CustomThemeColors | defaultLightColors | Override the light-mode palette. All keys from ThemeColors are required; extra keys are allowed. |
| darkColors | CustomThemeColors | defaultDarkColors | Override the dark-mode palette. All keys from ThemeColors are required; extra keys are allowed. |
Basic (default colours)
<ThemeProvider>{children}</ThemeProvider>Custom colours
To override the palette, import the defaults and spread your changes on top. Because all ThemeColors keys are required, spreading the defaults ensures nothing is missing:
import {
ThemeProvider,
defaultLightColors,
defaultDarkColors,
} from "rn-markdown-editor";
<ThemeProvider
lightColors={{
...defaultLightColors,
primary: "hsl(260, 60%, 50%)",
accent: "hsl(330, 80%, 55%)",
// You can also add custom keys:
brandGradientStart: "#6366f1",
brandGradientEnd: "#a855f7",
}}
darkColors={{
...defaultDarkColors,
primary: "hsl(260, 60%, 70%)",
accent: "hsl(330, 80%, 65%)",
brandGradientStart: "#818cf8",
brandGradientEnd: "#c084fc",
}}
>
{children}
</ThemeProvider>;How it works: On mount, reads
@markdown_editor_themefrom AsyncStorage. If no saved value, usesuseColorScheme()from React Native. Whenever the theme changes, it persists the new value and applies it to the platform (web: CSS class, native: NativeWind runtime). The resolved colour palette (user-supplied or default) is provided through context so every component and hook receives the correct colours.
CustomThemeColors
// All keys from ThemeColors are required + any additional string keys
export type CustomThemeColors = ThemeColors & { [key: string]: string };This means:
- ✅ All 37 built-in color tokens (
background,foreground,card, …frozen) must be provided. - ✅ Any extra keys you add (e.g.
brandGradientStart) are passed through and accessible viauseColors().
useTheme
Returns the current theme state, setters, and the resolved color palette.
import { useTheme } from "rn-markdown-editor";
function ThemeToggle() {
const { theme, isDark, setTheme, toggleTheme, colors } = useTheme();
return <Button onPress={toggleTheme}>{isDark ? "☀️" : "🌙"}</Button>;
}Returns:
| Property | Type | Description |
| ------------- | ------------------- | ------------------------------------------------------- |
| theme | "light" \| "dark" | Current active theme |
| isDark | boolean | Convenience boolean |
| setTheme | (theme) => void | Set theme explicitly (persists to storage) |
| toggleTheme | () => void | Toggle between light and dark |
| colors | CustomThemeColors | The resolved color palette (user overrides or defaults) |
useColors
File: src/hooks/useTheme.ts
Returns the resolved CustomThemeColors object for the current theme. If the ThemeProvider was given custom lightColors / darkColors, those are returned; otherwise the built-in defaults from theme.ts.
import { useColors } from "rn-markdown-editor";
function MyComponent() {
const colors = useColors();
return (
<View
style={{ backgroundColor: colors.card, borderColor: colors.border }}
/>
);
}Theme Tokens
File: src/theme.ts
Defines the ThemeColors interface and exports defaultLightColors / defaultDarkColors. Every UI component reads from these.
| Token | Purpose |
| --------------------------------------------- | ---------------------------------------------------- |
| background | App background |
| foreground | Primary text |
| card / cardForeground | Card surfaces |
| popover / popoverForeground | Popover/dialog surfaces |
| primary / primaryForeground | Primary action buttons |
| secondary / secondaryForeground | Secondary surfaces |
| muted / mutedForeground | Muted/disabled elements |
| accent / accentForeground | Accent highlights |
| earnings / earningsForeground | Success/earnings indicator |
| grey400 | Neutral hover background |
| destructive / destructiveForeground | Danger actions |
| border | General borders |
| input | Input field borders |
| ring | Focus ring |
| warning / warningForeground | Warning indicators |
| sidebarBackground / sidebarForeground | Sidebar colours |
| sidebarPrimary / sidebarPrimaryForeground | Sidebar primary actions |
| sidebarAccent / sidebarAccentForeground | Sidebar accents |
| sidebarBorder / sidebarRing | Sidebar borders & focus |
| flameon | Brand/accent color (used for links, primary buttons) |
| fire | Orange accent |
| frozen | Blue accent |
Components
MarkdownEditor
File: src/components/MarkdownEditor.tsx
A full markdown editing experience with a scrollable toolbar, undo/redo history, image/table insert dialogs, and inline/block formatting actions.
How it works: Maintains an internal history stack (max 50 entries). Toolbar buttons call insertMarkup() for inline formatting (wraps selection in prefix/suffix) or insertBlock() for block-level formatting (inserts prefix at line start). The image and table buttons open themed Modal dialogs. Selection tracking is done via onSelectionChange to know where to insert.
Props
| Prop | Type | Default | Description |
| ---------------- | ------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------ |
| value | string | — | Required. Current markdown text |
| onChangeText | (text: string) => void | — | Required. Called on every edit |
| preview | boolean | false | Hides toolbar when true |
| placeholder | string | "Write your story here…" | Input placeholder |
| style | StyleProp<ViewStyle> | — | Style for the text input |
| containerStyle | StyleProp<ViewStyle> | — | Style for the outer container |
| toolbar | DefaultToolbarItemId[] \| false | All items | Subset/order of toolbar buttons, or false to hide |
| toolbarExtra | ToolbarItem[] | [] | Custom buttons appended after built-in ones |
| toolbarIcons | Partial<Record<DefaultToolbarItemId, (color) => ReactNode>> | {} | Override icons for built-in actions |
| onPickImage | (alt?: string) => Promise<string \| null> | — | Image picker callback. Receives the alt text entered by the user. If omitted, the upload tab is hidden |
| minHeight | number | 200 | Minimum height of the text area |
| fontSize | number | 16 | Font size of the text area |
Usage
<MarkdownEditor
value={text}
onChangeText={setText}
toolbar={[
"bold",
"italic",
"underline",
"divider",
"h1",
"h2",
"link",
"image",
]}
toolbarIcons={{
bold: (color) => <MyBoldIcon color={color} />,
}}
toolbarExtra={[
{
id: "emoji",
label: "Emoji",
icon: (c) => <Text style={{ color: c }}>😊</Text>,
onPress: () => {},
},
]}
onPickImage={async (alt) => {
// `alt` contains the alt text the user entered in the image dialog
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: "Images",
});
return result.canceled ? null : result.assets[0].uri;
}}
minHeight={300}
fontSize={16}
/>Built-in Toolbar Item IDs
| ID | Action |
| ---------------------------------------------------------------- | ---------------------- |
| undo / redo | History navigation |
| divider / divider2 / divider3 / divider4 | Visual separators |
| bold / italic / underline / strikethrough / inlineCode | Inline formatting |
| h1 / h2 / h3 / blockquote | Block formatting |
| bulletList / orderedList | List insertion |
| alignLeft / alignCenter / alignJustify | Text alignment |
| link / image / table | Media & insert dialogs |
MarkdownRenderer
File: src/components/MarkdownRenderer.tsx
Renders a markdown string as native React Native views. Includes a full lexer (tokenizer) and inline renderer supporting bold, italic, underline, strikethrough, inline code, links, images, @mentions, #hashtags, code blocks, blockquotes, lists, tables, videos, pull-column layouts, and horizontal rules. Tappable images open the built-in full-screen ImageViewer.
How it works:
- HTML Preprocessor — Converts common HTML tags (
<center>,<strong>,<table>,<img>,<video>, pull-left/pull-right column divs, etc.) to their markdown equivalents or internal tokens. - Lexer — Splits the markdown into block-level
Tokenobjects (heading, paragraph, codeBlock, list, table, image, imageRow, video, columns, etc.). - Block Renderers — Each token type has a memoized React component (
HeadingBlock,ParagraphBlock,CodeBlock,ListBlock,ImageBlock,ImageRowBlock,VideoBlock,ColumnsBlock, etc.). - Inline Renderer — Parses inline formatting within text blocks using a single regex pass, including @mention and #hashtag detection.
Props
| Prop | Type | Default | Description |
| ------------------------ | ---------------------------- | ------------------- | -------------------------------------------------------------------------------- |
| body | string | — | Required. Markdown string to render |
| width | number | Screen width | Layout width for image/column sizing |
| containerStyle | StyleProp<ViewStyle> | — | Container style |
| scrollable | boolean | false | Wrap in a ScrollView |
| baseFontSize | number | 14 | Base paragraph font size |
| lineHeightMultiplier | number | 1.6 | Line height = fontSize × this |
| colors | MarkdownColors | {} | Fine-grained color overrides (see below) |
| onPressLink | (href: string) => void | Opens in browser | Link press handler |
| onPressUser | (username: string) => void | — | @mention press handler |
| onPressHashtag | (tag: string) => void | — | #hashtag press handler |
| paddingHorizontal | number | 16 | Inner horizontal padding |
| imageViewerAccentColor | string | colors.flameon | Accent color for the full-screen image viewer's download button |
| numberOfLines | number | — | Clamp text to N lines (flat-text mode — non-text blocks are hidden) |
| maxBodyLength | number | — | Truncate the markdown source to this many characters before rendering |
| bodyColor | string | colors.foreground | Override the default text color for all rendered text |
| renderTable | boolean | true | Set to false to suppress table rendering |
| allowRenderImage | boolean | true | Set to false to suppress all image and image-row blocks |
| renderVideo | boolean | true | Set to false to render video tokens as plain links instead of embedded players |
| removeEmptyLine | boolean | false | Strip horizontal-rule tokens (useful in compact previews) |
| disableLinks | boolean | false | Prevent all link/mention/hashtag tap interactions |
| isImageLoading | boolean | false | Show skeleton placeholders over images while external assets load |
MarkdownColors Override
Pass a colors prop to fine-tune individual element colors without overriding the whole theme:
<MarkdownRenderer
body={markdown}
colors={{
text: "#1a1a1a",
link: "#6366f1",
codeText: "#d97706",
codeBackground: "#fef3c7",
codeBlockBackground: "#1e293b",
blockquoteBorder: "#6366f1",
blockquoteText: "#6b7280",
blockquoteBackground: "#f5f3ff",
hr: "#e5e7eb",
listMarker: "#6366f1",
headingBorder: "#e5e7eb",
imageCaption: "#9ca3af",
}}
onPressLink={(href) => openInAppBrowser(href)}
onPressUser={(username) => navigate(`/@${username}`)}
onPressHashtag={(tag) => navigate(`/trending/${tag}`)}
imageViewerAccentColor="#6366f1"
/>MarkdownColors Keys
| Key | Applies to |
| ---------------------- | ------------------------------- |
| text | All paragraph/inline text |
| link | Hyperlinks and @mentions |
| codeText | Inline code text |
| codeBackground | Inline code background |
| codeBlockBackground | Fenced code block background |
| blockquoteBorder | Left border of blockquote |
| blockquoteText | Text inside blockquote |
| blockquoteBackground | Background of blockquote |
| hr | Horizontal rule line color |
| listMarker | Bullet/number list markers |
| headingBorder | Bottom border on H1/H2 headings |
| imageCaption | Image caption text below images |
Supported Markdown Syntax
| Syntax | Result |
| -------------------- | ----------------------------------- |
| **bold** | bold |
| *italic* | italic |
| <u>underline</u> | underline |
| ~~strike~~ | ~~strike~~ |
| `code` | inline code |
| ***bold italic*** | bold italic |
| # H1 … ###### H6 | Headings |
| > blockquote | Blockquote |
| ``` … ``` | Code block (with optional lang) |
| - item | Unordered list |
| 1. item | Ordered list |
| [text](url) | Link |
|  | Image (tappable, opens ImageViewer) |
| \| col \| col \| | Table |
| --- | Horizontal rule |
| :center:text | Centred line |
| @username | Mention (fires onPressUser) |
| #hashtag | Hashtag (fires onPressHashtag) |
| !! | Embedded video (shorthand) |
| <video src="…" /> | Embedded video (HTML tag) |
Content Limiting
When used in list/feed contexts you can limit rendered output without slicing the string manually:
// Show only the first 2 lines of text, no images
<MarkdownRenderer
body={post.body}
numberOfLines={2}
allowRenderImage={false}
renderTable={false}
renderVideo={false}
maxBodyLength={500}
removeEmptyLine
/>MarkdownTable
File: src/components/MarkdownTable.tsx
A horizontally-scrollable table component with alternating row backgrounds, auto-calculated column widths, and full inline formatting support (bold, italic, code, links, images, @mentions, #hashtags) inside every cell.
How it works: Column widths are calculated based on content character length (min 80px, max 220px). The renderInline function is passed in from MarkdownRenderer so cell content stays visually in sync with the rest of the document. Header rows use the theme's secondary background; data rows alternate between card and muted.
Props
| Prop | Type | Default | Description |
| ---------------- | ------------------------------------- | ------- | -------------------------------------------------------------------- |
| headers | string[] | — | Required. Column header labels |
| rows | string[][] | — | Required. 2D array of cell values |
| renderInline | RenderInline | — | Required. Shared inline renderer from MarkdownRenderer |
| cc | MarkdownColors | {} | Color overrides forwarded from the parent MarkdownRenderer |
| openImage | (src: string, alt?: string) => void | no-op | Called when an image inside a cell is tapped |
| onPressLink | (href: string) => void | browser | Link handler (defaults to Linking.openURL or window.open on web) |
| onPressUser | (username: string) => void | — | @mention handler |
| onPressHashtag | (tag: string) => void | — | #hashtag handler |
| fontSize | number | 13 | Font size for all cell text |
Note:
MarkdownTableis normally rendered automatically byMarkdownRenderer. Use it directly only when constructing custom table layouts.
<MarkdownTable
headers={["Name", "Role", "Status"]}
rows={[
["Alice", "Engineer", "Active"],
["Bob", "Designer", "**Lead**"],
]}
renderInline={myRenderInline}
onPressLink={(href) => Linking.openURL(href)}
onPressUser={(username) => navigate(`/@${username}`)}
onPressHashtag={(tag) => navigate(`/trending/${tag}`)}
fontSize={14}
/>ImageViewer
File: src/components/ImageViewer.tsx
A full-screen image viewer with pinch-to-zoom, pan, animated backdrop, and one-tap download support. Works on iOS, Android, and Web. Controlled via an imperative ref.
How it works: Mounts as a transparent Modal. The PinchPanImage inner component tracks multi-touch distances to drive a scale Animated.Value and single-finger pan while zoomed. On web, mouse-wheel events are wired up for zoom. The modal subtree is only mounted while the viewer is open, keeping gesture responders and image decoding out of the render tree when closed. Downloads use expo-file-system + expo-media-library on native (saved directly to the camera roll with a progress indicator) and a <a download> anchor on web.
Ref API (ImageViewerRef)
export interface ImageViewerRef {
open: (src: string, alt?: string) => void;
close: () => void;
}| Method | Description |
| ------- | --------------------------------------------------------------- |
| open | Opens the viewer with the given image URL and optional alt text |
| close | Closes the viewer with a fade-out animation |
Props
| Prop | Type | Default | Description |
| ------------- | ------------ | ----------- | ---------------------------------------- |
| accentColor | string | "#3B82F6" | Color of the download button |
| onClose | () => void | — | Called after the viewer has fully closed |
Usage
import { useRef } from "react";
import { ImageViewer, ImageViewerRef } from "rn-markdown-editor";
function MyScreen() {
const viewerRef = useRef<ImageViewerRef>(null);
return (
<>
<Pressable
onPress={() =>
viewerRef.current?.open("https://…/photo.jpg", "A sunset")
}
>
<Text>View Image</Text>
</Pressable>
<ImageViewer
ref={viewerRef}
accentColor="#6366f1"
onClose={() => console.log("closed")}
/>
</>
);
}Gestures & Interactions
| Interaction | Behaviour |
| -------------------------------- | -------------------------------------------------------------------------------- |
| Pinch (two fingers) | Zoom in/out (clamped 0.5× – 5×) |
| Single-finger drag | Pan when zoomed in |
| Tap on backdrop | Reset zoom to 1× |
| Mouse wheel (web) | Zoom in/out |
| Download button | Saves to camera roll (native) or triggers download (web) with progress indicator |
| Hardware back / onRequestClose | Closes the viewer |
Skeleton
File: src/components/Skeleton.tsx
A simple animated placeholder component that pulses between full opacity and 40% opacity on a loop. Use it to indicate loading state for content areas.
Props
| Prop | Type | Default | Description |
| -------------- | ---------------------- | -------- | ---------------------------- |
| style | StyleProp<ViewStyle> | — | Additional style overrides |
| width | number \| string | "100%" | Width of the skeleton block |
| height | number \| string | — | Height of the skeleton block |
| borderRadius | number | 6 | Corner radius |
// Single line placeholder
<Skeleton height={16} width="60%" />
// Card placeholder
<Skeleton height={120} borderRadius={12} style={{ marginBottom: 8 }} />Button
File: src/components/Button.tsx
A theme-aware pressable button with hover/press state tracking and 5 variants × 5 sizes. Supports both string children and render-prop children for full control.
How it works: Uses Pressable with onHoverIn/Out and onPressIn/Out to track interaction state. Each variant defines default colors for normal/hover/pressed states. All color props can be overridden individually.
Variants (5)
| Variant | Normal | Hover | Use case |
| ----------- | ------------------------ | ------------------------ | ----------------- |
| primary | flameon bg, white text | 80% opacity bg | Primary CTA |
| secondary | secondary bg | grey400 bg | Secondary actions |
| outline | Transparent bg, border | flameon bg, white text | Bordered actions |
| ghost | Transparent | grey400 bg | Subtle actions |
| link | No bg, flameon text | Underlined text | Inline links |
Sizes (5)
| Size | Padding |
| --------- | --------- |
| default | 16h × 8v |
| sm | 12h × 6v |
| lg | 32h × 12v |
| icon | 8 all |
| none | 0 |
Props
| Prop | Type | Default |
| ----------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------- |
| variant | "primary" \| "secondary" \| "outline" \| "ghost" \| "link" | "primary" |
| size | "default" \| "sm" \| "lg" \| "icon" \| "none" | "default" |
| textColor / hoveredTextColor / pressedTextColor | string | Auto from variant |
| backgroundColor / hoveredBackgroundColor / pressedBackgroundColor | string | Auto from variant |
| borderColor / hoveredBorderColor / pressedBorderColor | string | Auto from variant |
| children | ReactNode \| ((state) => ReactNode) | — |
// String children
<Button variant="primary" size="default" onPress={save}>Save</Button>
// Render-prop children
<Button variant="outline">
{({ textColor, hovered, pressed }) => (
<Text style={{ color: textColor }}>Custom</Text>
)}
</Button>Badge
File: src/components/Badge.tsx
A small label/tag component with 7 variants. Renders as a pill-shaped View with auto text styling. Supports string, ReactNode, or render-prop children.
Variants (7)
| Variant | Background | Text |
| ------------- | -------------------- | ----------------------- |
| default | primary | primaryForeground |
| secondary | secondary | secondaryForeground |
| accent | accent | accentForeground |
| warning | warning | foreground |
| destructive | destructive | destructiveForeground |
| success | earnings | earningsForeground |
| outline | Transparent + border | foreground |
Props
| Prop | Type | Default |
| ------------- | ---------------------------------------------------- | ----------------- |
| variant | See above | "default" |
| textColor | string | Auto from variant |
| bgColor | string | Auto from variant |
| borderColor | string | Auto from variant |
| children | ReactNode \| ((props: { textColor }) => ReactNode) | — |
<Badge variant="accent">New</Badge>
<Badge variant="success">Published</Badge>
<Badge bgColor="#f0abfc" textColor="#4a044e">Custom</Badge>Input
File: src/components/Input.tsx
A themed TextInput wrapper with focus ring styling. Applies theme colors automatically and removes the web outline.
How it works: Wraps React Native TextInput with forwardRef. Tracks focus state to toggle borderColor between colors.border (unfocused) and colors.ring (focused). On web, sets outlineStyle: "none" to replace the browser default with the custom border.
<Input value={value} onChangeText={setValue} placeholder="Type here…" />Accepts all standard TextInputProps.
Icons
File: src/components/Icons.tsx
42 SVG icons built with react-native-svg. No external icon font required — @expo/vector-icons is not a dependency. Each icon is created via a makeIcon() factory that reads the current theme color for its default stroke.
Icon Props
| Prop | Type | Default |
| ------------- | -------- | -------------------------------- |
| size | number | 24 |
| color | string | colors.foreground (from theme) |
| fill | string | "none" |
| strokeWidth | number | 2 |
<Icons.Bold size={20} color="#000" />
<Icons.Image size={24} />
<Icons.Link size={18} color="blue" />Available Icons (42)
Toolbar: Undo, Redo, Bold, Italic, Underline, Strikethrough, Code, Heading1, Heading2, Heading3, Quote, List, ListOrdered, AlignLeft, AlignCenter, AlignRight, AlignJustify, Link, Image, Table, Upload, Minus
Navigation: ChevronLeft, ChevronRight, ChevronDown, ChevronUp, ArrowLeft
Actions: Cross, Check, Search, More, Eye, Send, Save, Plus, ExternalLink
Social / Status: Heart, MessageCircle, Bell, Settings, AlertCircle
Hooks
useDebouncedInput
File: src/hooks/useDebouncedInput.ts
Provides an immediate value (for the input field) and a debounced value (for API calls). Built on top of useMergeState for performance.
const { immediateValue, debouncedValue, setInput, clearDebounce } =
useDebouncedInput("", 300);
// In a search input:
<Input value={immediateValue} onChangeText={setInput} />;
// Use debouncedValue for API calls:
useEffect(() => {
searchAPI(debouncedValue);
}, [debouncedValue]);| Param | Type | Default | Description |
| -------------- | -------- | ------- | -------------- |
| initialValue | string | "" | Starting value |
| delay | number | 300 | Debounce ms |
Returns:
| Property | Type | Description |
| ---------------- | ------------------------- | -------------------------------------- |
| immediateValue | string | Updates instantly on every keystroke |
| debouncedValue | string | Updates after delay ms of inactivity |
| setInput | (value: string) => void | Updates both values |
| clearDebounce | () => void | Cancels pending debounce timer |
useDisclosure
File: src/hooks/useDisclosure.ts
Manages open/close boolean state with optional callbacks. Useful for modals, drawers, dropdowns.
const { isOpen, onOpen, onClose, onToggle } = useDisclosure({
defaultIsOpen: false,
onOpen: () => console.log("Opened"),
onClose: () => console.log("Closed"),
});
<Button onPress={onToggle}>Toggle Menu</Button>;
{
isOpen && <MyModal onClose={onClose} />;
}| Option | Type | Default |
| --------------- | ------------ | ------- |
| defaultIsOpen | boolean | false |
| onOpen | () => void | — |
| onClose | () => void | — |
| onToggle | () => void | — |
useMergeState
File: src/hooks/useMergeState.ts
A useState replacement that shallow-merges partial updates instead of replacing. Similar to class component this.setState().
const [state, setState] = useMergeState({ name: "", age: 0, loading: false });
// Update only one field:
setState({ loading: true });
// Functional update:
setState((prev) => ({ age: prev.age + 1 }));Signature: useMergeState<T>(initialState: T) => [T, (partial: Partial<T> | (prev: T) => Partial<T>) => void]
Types
File: src/types/toolbar.ts
| Export | Kind | Description |
| ----------------------- | --------- | ----------------------------------------------------------------------------------- |
| DefaultToolbarItemId | type | Union of all 22 built-in toolbar button IDs |
| ToolbarItem | interface | Shape for custom toolbar items: { id, type?, label?, icon?, onPress?, disabled? } |
| DEFAULT_TOOLBAR_ITEMS | const | Array of all default IDs in display order |
import { DEFAULT_TOOLBAR_ITEMS } from "rn-markdown-editor/types";
import type { ToolbarItem, DefaultToolbarItemId } from "rn-markdown-editor/types";
// Create a custom item:
const myItem: ToolbarItem = {
id: "my-btn",
label: "My Action",
icon: (color) => <Icons.Plus size={16} color={color} />,
onPress: () => doSomething(),
};File Structure
src/
├── index.ts # Main entry — re-exports everything
├── theme.ts # ThemeColors interface + light/dark defaults
├── components/
│ ├── index.ts # Barrel: all components
│ ├── Badge.tsx # Badge component (7 variants)
│ ├── Button.tsx # Button component (5 variants × 5 sizes)
│ ├── Icons.tsx # 42 SVG icons via react-native-svg
│ ├── ImageViewer.tsx # Full-screen pinch-zoom image viewer
│ ├── Input.tsx # Themed TextInput wrapper
│ ├── MarkdownEditor.tsx # Full editor with toolbar + dialogs
│ ├── MarkdownRenderer.tsx # Markdown → native view renderer
│ ├── MarkdownTable.tsx # Scrollable themed table
│ └── Skeleton.tsx # Animated loading placeholder
├── contexts/
│ ├── index.ts # Barrel: ThemeProvider, useTheme
│ └── ThemeContext.tsx # Theme context + AsyncStorage persistence
├── hooks/
│ ├── index.ts # Barrel: all hooks
│ ├── useDebouncedInput.ts # Debounced input hook
│ ├── useDisclosure.ts # Open/close state hook
│ ├── useMergeState.ts # Shallow-merge useState
│ └── useTheme.ts # useColors hook
└── types/
├── index.ts # Barrel: all types + toolbar constants
└── toolbar.ts # Toolbar item IDs, ToolbarItem interfaceLicense
MIT
