https://github.com/ensunluer/react-native-markdown-stream
A lightweight React Native renderer that turns streaming Markdown (chat responses, AI completions, docs) into polished UI in real time.
https://github.com/ensunluer/react-native-markdown-stream
markdown react-native stream
Last synced: 6 months ago
JSON representation
A lightweight React Native renderer that turns streaming Markdown (chat responses, AI completions, docs) into polished UI in real time.
- Host: GitHub
- URL: https://github.com/ensunluer/react-native-markdown-stream
- Owner: ensunluer
- License: mit
- Created: 2025-10-15T18:39:18.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2026-01-10T21:41:39.000Z (6 months ago)
- Last Synced: 2026-01-11T06:31:04.632Z (6 months ago)
- Topics: markdown, react-native, stream
- Language: TypeScript
- Homepage: https://rnms.enesunluer.com/
- Size: 1.59 MB
- Stars: 2
- Watchers: 0
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# react-native-markdown-stream
A lightweight React Native renderer that turns streaming Markdown (chat responses, AI completions, docs) into polished UI in real time.
## Highlights
- **Streaming-first:** `AsyncIterable`, `ReadableStream`, generators, or manual chunk pushes all work out of the box.
- **Reveal controls:** Animate output per chunk, word, or character with configurable delays.
- **Theming without repainting the screen:** ship light/dark presets, or merge your own colors with a simple config object.
- **Code & math aware:** inline/code blocks with optional copy actions, math rendering via the optional `react-native-math-view` peer.
- **Zero native code:** works in the classic architecture and the New Architecture (Fabric/TurboModules) without extra steps.
Contents
- [Installation](#installation)
- [Metro configuration](#metro-configuration)
- [Quick start](#quick-start)
- [Streaming sources](#supported-stream-sources)
- [Component props](#markdownstream-props)
- [Customising the renderer](#customising-the-renderer)
- [Theming](#theming)
- [Hook API](#using-usemarkdownstream-directly)
- [Type exports](#other-exports)
- [Running the example](#running-the-example-app)
- [Contributing](#contributing)
## Installation
```sh
# yarn
yarn add react-native-markdown-stream
# npm
npm install react-native-markdown-stream
```
### Optional math support
Install the optional peer if you want LaTeX blocks to render with `react-native-math-view`:
```sh
yarn add react-native-math-view
```
Without it installed, math blocks gracefully fall back to styled text.
## Metro configuration
The library bundles modern ESM packages from the `remark/unified` ecosystem. Metro 0.72+ understands them as long as package exports are enabled.
Add or update `metro.config.js` in your app:
```js
// metro.config.js
const {getDefaultConfig} = require('metro-config');
module.exports = (() => {
const config = getDefaultConfig(__dirname);
config.resolver.unstable_enablePackageExports = true;
return config;
})();
```
Using Expo or a monorepo? Mirror the example app:
```js
// example/metro.config.js
const path = require('path');
const {getDefaultConfig} = require('@expo/metro-config');
const {withMetroConfig} = require('react-native-monorepo-config');
const root = path.resolve(__dirname, '..');
const config = withMetroConfig(getDefaultConfig(__dirname), {
root,
dirname: __dirname,
});
config.resolver.unstable_enablePackageExports = true;
module.exports = config;
```
## Quick start
```tsx
import {MarkdownStream} from 'react-native-markdown-stream';
const STREAM_URL = 'https://example.com/chat-stream';
export function ChatMessage() {
return (
);
}
function* staticExample() {
yield '# Hello\n';
yield 'Streaming markdown arrives chunk by chunk.\n';
}
async function* listenToSSE(url: string) {
const response = await fetch(url);
if (!response.body) {
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {value, done} = await reader.read();
if (done) break;
yield decoder.decode(value);
}
reader.releaseLock();
}
```
### Controlled usage
Pass the `content` prop to render pre-computed Markdown (and still opt into streaming later if you call `onReady` controls).
```tsx
controls.start()} />
```
## Supported stream sources
`source` accepts any of the following:
- `AsyncIterable` or `Iterable` (generators, async generators).
- A function returning one of the above (lazy initialisation).
- A `ReadableStream` (SSE, `fetch` with streaming responses).
- Call `controls.appendChunk()` manually via the `onReady` callback for complete control.
All chunks (strings, `Uint8Array`, objects with `toString`) are normalised to strings before parsing.
## `MarkdownStream` props
| Prop | Type | Description |
| --- | --- | --- |
| `source` | `MarkdownStreamSource` | Primary stream. Optional if you control content manually. |
| `content` | `string` | Fully rendered markdown. Bypasses streaming until you call `start`. |
| `initialValue` | `string` | Markdown shown before the first chunk arrives. |
| `theme` | `'light' \| 'dark' \| MarkdownTheme \| MarkdownThemeConfig` | Pick a preset or merge custom colors; defaults to light theme with transparent background. |
| `textColor` | `string` | Override the resolved theme's primary text color without redefining the whole palette. |
| `mutedTextColor` | `string` | Override the resolved theme's muted/secondary text color. |
| `revealMode` | `'chunk' \| 'word' \| 'character'` | Controls how new content animates in. |
| `revealDelay` | `number` | Delay (ms) between reveals; ignored when `revealMode="chunk"`. |
| `autoStart` | `boolean` | Start streaming as soon as `source` exists (default `true`). |
| `onReady` | `(controls: UseMarkdownStreamResult) => void` | Exposes stream controls (append, reset, start, stop). |
| `onChunk` / `onEnd` / `onError` | callbacks | Tap into stream lifecycle events. |
| `showCodeLineNumbers` | `boolean` | Adds line numbers to fenced code blocks. |
| `enableCodeCopy` | `boolean` | Shows a copy action on code blocks (uses clipboard when available). |
| `codeCopyLabel` | `string` | Custom label for the copy button. |
| `onCodeCopy` | `({value, language}) => boolean \| void` | Return `true` to mark the copy action as handled. Fires before the built-in clipboard logic. |
| `onImagePress` | `({url, alt}) => boolean \| void` | Intercept image taps. Return `true` to skip built-in lightbox. |
| `enableImageLightbox` | `boolean` | Presents a modal preview when images are tapped. |
| `onBlockLongPress` | `({node}) => void` | Receive long-press events for any block node. |
| `blockLongPressDelay` | `number` | Milliseconds before the long-press fires (default `300`). |
| `components` | `Partial` | Override individual renderers (`codeBlock`, `inlineCode`, `mathBlock`, `image`, …). |
## Customising the renderer
Every major element exposes a customization hook:
- Provide `components` to override code/math/image rendering with your own component tree.
- Style code blocks via `CodeBlock` `containerStyle`/`codeStyle` props if you bring your own.
- Hook into `onImagePress`, `enableImageLightbox`, and `onBlockLongPress` for richer media UX.
- Implement your own copy logic with `onCodeCopy` (e.g. analytics or custom tooltips).
## Theming
Light and dark themes ship by default, both with transparent container backgrounds so they blend into your layout. Override a single token or a whole palette by passing a `MarkdownTheme` or `MarkdownThemeConfig`, or use the `textColor` / `mutedTextColor` props for quick tweaks.
```tsx
import {MarkdownStream} from 'react-native-markdown-stream';
;
```
Call `resolveTheme` if you need the concrete palette outside the component.
## Handling incomplete markdown
Streaming text often arrives with half-typed emphasis or unfinished links. Before parsing we run the content through a small sanitizer that auto-closes common Markdown markers and neutralises broken URLs. The implementation is adapted from [Vercel's streamdown `parse-incomplete-markdown`](https://github.com/vercel/streamdown/blob/main/packages/streamdown/lib/parse-incomplete-markdown.ts) (Apache-2.0).
## Using `useMarkdownStream` directly
The hook powers the component and can run headless when you need custom rendering.
```tsx
import {useMarkdownStream} from 'react-native-markdown-stream';
export function CustomRenderer({source}) {
const stream = useMarkdownStream({
source,
revealMode: 'character',
onChunk: (chunk) => console.log('chunk', chunk.length),
});
return (
{stream.content}
stream.setRevealMode('chunk')} />
);
}
```
The hook returns the current `content`, the accumulated `fullContent`, status flags, and control helpers (`appendChunk`, `reset`, `start`, `stop`, `setRevealMode`, `setRevealDelay`).
## Other exports
```ts
import {
MarkdownRenderer,
parseMarkdown,
useMarkdownStream,
lightTheme,
darkTheme,
resolveTheme,
type MarkdownTheme,
type MarkdownThemeConfig,
type ThemeMode,
type MarkdownRendererComponents,
} from 'react-native-markdown-stream';
```
Use `MarkdownRenderer` when you already have an mdast `Root`, or to wrap custom parsed content.
## Running the example app
```sh
yarn install
yarn example # starts Expo in the example/ workspace
# or target a platform directly
yarn workspace react-native-markdown-stream-example android
yarn workspace react-native-markdown-stream-example ios
```
The example showcases reveal modes, code copy actions, image lightbox, and long-press handlers.
## Architecture support
The package is JavaScript-only and works unchanged in both the classic bridge and the New Architecture (Fabric/TurboModules). No native modules, pods, or Gradle steps are required.
## Contributing
- [Development workflow](CONTRIBUTING.md#development-workflow)
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
- [Code of conduct](CODE_OF_CONDUCT.md)
---
© [Enes Ünlüer](https://github.com/ensunluer)